[
  {
    "path": ".agents/skills/README.md",
    "content": "# Agent Skills\n\n本目录存放用于辅助 Sa-Token 项目开发的 Agent Skills。这些 skills 封装了项目特定的知识和操作规范，可在对话中直接调用。\n\n## Skill 列表\n\n| Skill 名称 | 功能描述 | 使用场景 | 入口文件 |\n|-----------|---------|---------|---------|\n| `commit-message` | 根据 git 变更生成符合 Sa-Token 项目风格的 commit message | 生成提交信息、写 commit message | [SKILL.md](commit-message/SKILL.md) |\n| `organize-update-log` | 根据 git 提交记录生成符合项目规范的更新日志内容 | 整理更新日志、分析版本变更 | [SKILL.md](organize-update-log/SKILL.md) |\n| `remove-redundancy-import` | 检查并移除 Java 类中未被引用的冗余 import | 清理冗余导包、优化 import | [SKILL.md](remove-redundancy-import/SKILL.md) |\n| `upgrade-version` | 将项目版本号从旧版本升级到新版本，批量修改 pom、常量、Demo 及文档 | 升级版本、修改版本号、version bump | [SKILL.md](upgrade-version/SKILL.md) |\n\n### 详细说明\n\n#### commit-message\n根据当前 git 变更（staged 或 unstaged），生成符合 [Conventional Commits](https://www.conventionalcommits.org/) 格式、以中文为主的 commit message。支持识别新增文件、修复 bug、重构等多种变更类型。\n\n#### organize-update-log\n根据 git 提交记录，生成符合 `sa-token-doc/more/update-log.md` 格式的更新日志内容。自动分类到插件、starter、重构、Solon、示例、文档等版块。\n\n#### remove-redundancy-import\n扫描项目中所有 Java 类，检测未被引用的冗余 import，生成清理计划供审阅，确认后执行移除。支持通过内置 Python 脚本快速扫描。\n\n#### upgrade-version\n将 Sa-Token 项目版本号从旧版本升级到新版本。批量修改根 POM、BOM、SaTokenConsts、所有 Demo 子项目 pom.xml 及文档（README、index.html、doc.html、new-version.md 等）中的版本引用。明确排除历史记录、@since 标注、更新日志等不应修改的文件。\n\n## 快速使用\n\n在 AI 对话中，直接描述你的需求即可自动触发相应 skill：\n\n```\n用户：帮我生成 commit message\n→ 自动使用 commit-message skill 分析 git 变更并生成提交信息\n\n用户：整理一下更新日志\n→ 自动使用 organize-update-log skill 生成更新日志\n\n用户：清理一下冗余 import\n→ 自动使用 remove-redundancy-import skill 扫描并清理未使用的 import\n\n用户：把版本从 v1.44.0 升级到 v1.45.0\n→ 自动使用 upgrade-version skill 批量修改版本号\n```\n\n## 新增 Skill 维护指南\n\n当新增 skill 时，请同步更新本 README 文件，保持 skill 列表的完整性。\n\n### Skill 目录结构规范\n\n每个 skill 应创建独立的子目录，结构如下：\n\n```\n.agents/skills/\n├── README.md              # 本文件\n└── skill-name/            # skill 目录（小写，短横线分隔）\n    ├── SKILL.md           # skill 主文件（必须包含 YAML 元数据和使用说明）\n    ├── examples.md        # 使用示例（可选）\n    ├── reference.md       # 参考文档（可选）\n    └── scan_redundant_imports.py  # 辅助脚本（如需要）\n```\n\n### SKILL.md 文件格式\n\n每个 `SKILL.md` 必须包含 YAML Front Matter：\n\n```yaml\n---\nname: skill-name\ndescription: 简要描述 skill 的功能和使用场景\n---\n```\n\n### 更新 README 清单\n\n新增 skill 后，请在本文件中：\n\n1. 在 **Skill 列表** 表格中添加新行\n2. 在 **详细说明** 小节添加对应的描述段落\n3. （可选）在 **快速使用** 中添加使用示例\n\n## 注意事项\n\n- 所有 skill 遵循 Sa-Token 项目特定的规范和风格\n- 部分 skill（如 `remove-redundancy-import`）在执行前需要用户确认\n- 可参考每个 skill 目录下的 `examples.md` 或 `reference.md` 获取更多使用帮助\n"
  },
  {
    "path": ".agents/skills/commit-message/SKILL.md",
    "content": "---\nname: commit-message\ndescription: 根据 git 变更生成符合 Sa-Token 项目风格的 commit message。遵循 Conventional Commits 格式，以中文为主。当用户要求生成提交信息、写 commit message、或根据变更生成提交说明时使用。\n---\n\n# 生成 Commit Message\n\n根据当前 git 变更（staged 或 unstaged），生成符合 Sa-Token 项目规范的 commit message。\n\n## 使用时机\n\n- 用户要求生成 commit message\n- 用户要求根据变更写提交说明\n- 用户说「帮我写个 commit」「生成提交信息」等\n\n## 工作流程\n\n### 第一步：获取变更内容\n\n```bash\ngit status\ngit diff --staged\ngit diff\n```\n\n**必须包含的变更范围**：\n- **staged 变更**：`git diff --staged`\n- **unstaged 变更**：若无 staged，则用 `git diff` 查看工作区修改\n- **未跟踪文件**：`git status` 中的 Untracked files 也要纳入分析，生成 commit message 时需一并考虑\n\n若存在未跟踪的新增文件（如新 skill、新配置等），应在 message 中体现，或给出「包含全部变更」与「仅已修改文件」两种方案供用户选择。\n\n### 第二步：分析变更类型\n\n根据变更内容选择 type 前缀：\n\n| type | 适用场景 |\n|------|----------|\n| feat | 新增功能、新模块、新插件 |\n| fix | 修复 bug、修正错误 |\n| refactor | 重构、优化结构、重命名、移除冗余 |\n| perf | 性能优化（与 refactor 区分：侧重性能） |\n| docs | 文档更新、README、错别字、同步链接 |\n| style | 代码格式调整（缩进、空格等，不影响逻辑） |\n| chore | 构建配置、.gitignore、注释修复、依赖更新 |\n| test | 单元测试、测试用例 |\n| demo | 示例项目、demo 相关 |\n| memo | 备忘录、内部记录 |\n| revert | 回滚某次提交 |\n| AI | AI 创建的 skill、规则等 |\n\n### 第三步：撰写描述\n\n**基础格式**：`type: 简短描述` 或 `type(scope): 简短描述`\n\n**scope 可选**：涉及特定模块时使用，如 `feat(sign)`、`fix(oauth2)`、`refactor(dependencies)`。\n\n**规范**：\n- **50 字规则**：subject 不超过 50 字符，保证在 git log 中完整显示\n- **命令式语气**：用「修复」「新增」「优化」，不用「修复了」「新增了」\n- **说明「做了什么」**：清晰表达变更内容，必要时说明「为什么」\n- **以中文为主**：技术术语可保留英文（如 `StrFormatter`、`sa-token-jackson3`）\n- **动词开头**：新增、修复、优化、重构、移除、同步、订正 等\n\n**可选 Body/Footer**（重要变更时）：\n- Body：详细说明变更背景、动机，每行不超过 72 字符\n- Footer：关联 Issue，如 `Fixes #123` 或 `merge: [pr N](url)`\n\n### 第四步：输出\n\n直接输出可复制的 commit message。若有多条合理方案，可给出 1～2 个备选。\n\n## 格式示例\n\n**简单提交**（常用）：\n```\nfeat: 添加 sa-token-jackson3 插件\nfix(sign): 修复签名校验在空参数时的空指针\n```\n\n**带 scope**：\n```\nrefactor(dependencies): 重构模块依赖层级\nperf(oauth2): 优化 Client 信息读取算法\n```\n\n**带 body**（复杂变更）：\n```\nfeat: 新增重复登录处理策略\n\n当同一账号不允许多客户端同时登录时，支持选择踢人下线或拦截本次登录。\n```\n\n## 参考资源\n\n- 示例：详见 [examples.md](examples.md)\n- 规范详解：详见 [reference.md](reference.md)\n\n## 快速对照\n\n| 变更内容 | 示例输出 |\n|----------|----------|\n| 新增插件 | `feat: 添加 sa-token-jackson3 插件` |\n| 修复 bug | `fix: 修复 StpUtil.getLoginIdByTokenNotThinkFreeze 方法缺少 static 的问题` |\n| 性能优化 | `perf: 优化 StrFormatter 常量封装` |\n| 重构模块 | `refactor: 重构模块依赖层级` |\n| 移除冗余 | `refactor: 移除冗余导包` |\n| 文档更新 | `docs: 同步最新文章列表、赞助者名单` |\n| 注释修复 | `chore: 修复注释错别字` |\n| 新增 skill | `AI: 新增 skills/commit-message/SKILL.md，用于根据 git 变更生成符合项目风格的 commit message` |\n"
  },
  {
    "path": ".agents/skills/commit-message/examples.md",
    "content": "# Commit Message 示例\n\n基于 Sa-Token 项目近期提交整理，遵循 Conventional Commits + 50/72 规则。\n\n## feat - 新增功能\n\n```\nfeat: 添加 sa-token-jackson3 插件\nfeat: 新增 sa-token-spring-boot4-starter 集成包\nfeat: 新增 sa-token-reactor-spring-boot4-starter 集成包\nfeat(sign): 新增签名模板自定义能力\n```\n\n## fix - 修复问题\n\n```\nfix: 修复 StpUtil.getLoginIdByTokenNotThinkFreeze 方法缺少 static 的问题\nfix: 修正一处代码注释错误：SaTokenDao 注释中 数据有效期 应为 小于等于-2 (掉了等于)\nfix: Bearer 全局统一大小写\nfix: SaOAuth2Strategy中removeGrantTypeHandler的引用有误\n```\n\n## refactor - 重构/优化\n\n```\nrefactor: 移除冗余导包\nrefactor: 重命名 SaRepeatLoginsMode -> SaReplacedLoginExitMode\nrefactor: 优化项目构建配置\nrefactor: 优化 OAuth2 模块在请求中读取 Client 信息算法\nrefactor: 优化模块依赖关系\nrefactor: 重构模块依赖层级\nrefactor: sa-token-dependencies 重构为 sa-token-basic-dependencies\nrefactor: SaTokenDubboContextFilter 改为使用 SaTokenContextDubboUtil 清理上下文\n```\n\n## perf - 性能优化\n\n```\nperf: 优化 StrFormatter 常量规范与封装\nperf: 优化 pattern 缓存，消除魔法值\n```\n\n## docs - 文档\n\n```\ndocs: 订正文档错别字\ndocs: 同步最新文章列表、赞助者名单\ndocs: 为 sa-token-sso 模块定义 STS 协议\ndocs: 优化 readme\ndocs: 同步最新博客链接\n```\n\n## chore - 杂项\n\n```\nchore: 修复注释错别字\nchore: 增加忽略 .vscode 目录\n```\n\n## demo - 示例\n\n```\ndemo: 新增 sa-token-demo-webflux-springboot4 示例\ndemo: 新增 SpringBoot4 整合 demo 示例\n```\n\n## test - 测试\n\n```\ntest: 新增 sa-token-jackson3 单元测试\n```\n\n## memo - 备忘录\n\n```\nmemo: 备忘录重构为专门的文件夹\n```\n\n## style - 代码格式\n\n```\nstyle: 统一代码缩进与空格\nstyle: 修复 ESLint 警告\n```\n\n## revert - 回滚\n\n```\nrevert: feat(sign): 新增签名模板自定义能力\n```\n\n## AI - AI 相关\n\n```\nAI: 新增 skills/remove-redundancy-import/SKILL.md，用于检查项目中的java类无效冗余导包信息并移除\nAI: 新增 SKILL: organize-update-log ，用于格式化整理版本更新日志信息\n```\n"
  },
  {
    "path": ".agents/skills/commit-message/reference.md",
    "content": "# Commit Message 规范参考\n\n基于 Conventional Commits 与业界最佳实践整理。\n\n## 核心规则\n\n| 规则 | 说明 |\n|------|------|\n| 50 字规则 | subject 不超过 50 字符，便于 git log 完整显示 |\n| 72 字规则 | body 每行不超过 72 字符，便于阅读与 diff |\n| 命令式语气 | 用「修复」「新增」而非「修复了」「新增了」 |\n| 说明动机 | 重要变更在 body 中说明「为什么」而不仅是「做了什么」 |\n\n## 格式结构\n\n```\n<type>[(<scope>)]: <subject>\n\n[optional body]\n\n[optional footer(s)]\n```\n\n- **subject**：必填，简明扼要\n- **body**：可选，详细说明\n- **footer**：可选，如 `Fixes #123`、`BREAKING CHANGE: xxx`\n\n## 类型速查\n\n- **feat**：新功能\n- **fix**：修复 bug\n- **refactor**：重构（结构、逻辑）\n- **perf**：性能优化\n- **docs**：文档\n- **style**：格式（不影响逻辑）\n- **chore**：构建、配置、杂项\n- **test**：测试\n- **revert**：回滚\n"
  },
  {
    "path": ".agents/skills/organize-update-log/SKILL.md",
    "content": "---\nname: organize-update-log\ndescription: 根据 git 提交记录生成符合 Sa-Token 项目规范的更新日志内容。适用于分析指定版本之后的提交、提取变更并格式化为 update-log.md 风格。当用户需要生成更新日志、整理版本变更、或分析 release 之后的提交时使用。\n---\n\n# 整理更新日志\n\n根据 git 提交记录，生成符合 `sa-token-doc/more/update-log.md` 格式的更新日志内容。\n\n## 使用时机\n\n- 用户要求生成/整理更新日志\n- 用户要求分析「某版本之后」的提交变更\n- 用户要求将 git 提交格式化为更新日志风格\n- 准备发布新版本前整理 changelog\n\n## 工作流程\n\n### 第一步：确定基准版本\n\n1. 询问用户基准版本（如 `v1.44.0`），或从上下文推断\n2. 查找该版本的发布提交：\n   - 在 `SaTokenConsts.java` 或 `pom.xml` 中搜索版本号\n   - 或执行：`git log --oneline --all -- sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java` 查找含 `release vX.X.X` 的提交\n3. 记录基准提交 hash（如 `7bde74bc`）\n\n### 第二步：获取提交列表\n\n执行：\n```bash\ngit log <基准提交>..HEAD --oneline --format=\"%h %s\"\n```\n\n可选，获取更详细的变更文件：\n```bash\ngit log <基准提交>..HEAD --stat --format=\"=== %h %s ===\"\n```\n\n### 第三步：分类与映射\n\n将每条提交按以下规则归类到对应板块：\n\n| 提交关键词/内容 | 归属板块 |\n|----------------|----------|\n| feat.*jackson、plugin、插件 | 插件 |\n| feat.*starter、spring-boot、reactor | starter |\n| refactor.*依赖、dependencies、模块 | 重构 |\n| refactor.*solon、gateway | Solon（单独列出） |\n| fix.*dubbo、dubbo3 | 插件 |\n| demo.*、示例 | 示例 或 starter |\n| docs.*、文档 | 文档 |\n| chore、.gitignore、.vscode | 其它 |\n| merge.*loveqq、maven-pull | 其它（含 PR 链接） |\n\n### 第四步：动作词映射\n\n根据提交类型选择正确的动作词：\n\n| 提交类型 | 动作词 | 示例 |\n|----------|--------|------|\n| feat、新增 | 新增 | 新增 `sa-token-jackson3` 插件 |\n| fix、修复 | 修复 | 修复 Maven 父子项目依赖下载问题 |\n| refactor、重构 | 重构 / 移除 | 重构模块依赖层级；移除 xxx 模块 |\n| 优化、perf | 优化 | 优化 Gateway 接口处理 |\n| 拆分 | 拆分 | （少见） |\n| 文档更新 | 同步/新增/优化/修复 | 按具体内容选择 |\n\n### 第五步：按格式输出\n\n使用下方模板生成最终内容。详见 [format-reference.md](format-reference.md)。\n\n## 输出模板\n\n```markdown\n### vX.X.X @YYYY-M-D（或：开发中 / 未发布）\n\n- 插件：\n\t- 新增：xxx。  **[重要]**（如适用）\n\t- 修复：xxx。merge: [pr N](https://gitee.com/dromara/sa-token/pulls/N)（如适用）\n- starter：\n\t- 新增：xxx。\n- 重构：\n\t- 重构：xxx。\n\t- 移除：xxx。\n- Solon：（如有）\n\t- 优化：xxx。merge: [pr N](url)\n- 示例：（如有）\n\t- 新增：xxx。\n- 文档：\n\t- 同步：xxx。\n\t- 新增：xxx。\n\t- 优化：xxx。\n\t- 修复：xxx。\n- 其它：\n\t- 新增/修复/优化：xxx。\n```\n\n## 格式规则\n\n1. **层级**：一级用 `-`，二级用 `\t-`（Tab + 短横线）\n2. **动作词**：每条以「新增」「修复」「重构」「优化」「移除」「同步」等开头\n3. **重要标记**：对用户影响大的变更加 `**[重要]**`\n4. **PR/Issue 链接**：提交信息含 `!358`、`pr 340` 等时，补充 `merge: [pr N](https://gitee.com/dromara/sa-token/pulls/N)`\n5. **代码/模块名**：用反引号包裹，如 `` `sa-token-jackson3` ``\n6. **合并同类**：多条相似文档类提交可合并为一条（如「同步公众号、博客、赞助者名单」）\n\n## 常见板块\n\n- **core**：核心逻辑、API、配置变更\n- **SSO**：单点登录相关\n- **OAuth2**：OAuth2 相关\n- **插件**：插件包（jackson、dubbo、redis 等）\n- **starter**：Spring Boot / Reactor 等 starter\n- **示例**：demo 项目\n- **文档**：文档、README、错别字\n- **其它**：其它杂项\n\n## 注意事项\n\n- 合并提交（Merge branch）可忽略，只保留实际变更的提交\n- 纯文档/错别字可适度合并，避免条目过多\n- 版本号未发布时，可写 `v1.45.0（开发中）` 或 `未发布`\n- 输出为可直接粘贴到 `update-log.md` 的 Markdown 片段\n"
  },
  {
    "path": ".agents/skills/organize-update-log/format-reference.md",
    "content": "# 更新日志格式参考\n\n本文档提供 `sa-token-doc/more/update-log.md` 的格式细节与示例，供生成更新日志时参考。\n\n## 版本标题格式\n\n```markdown\n### v1.44.0 @2025-6-7\n```\n\n- 版本号：`v` + 主.次.修订\n- 日期：`@YYYY-M-D` 或 `@YYYY-M-DD`\n- 未发布时：`v1.45.0（开发中）` 或 `v1.45.0（未发布）`\n\n## 板块结构\n\n```\n- 板块名：\n\t- 动作词：具体描述。  **[重要]**（可选） merge: [pr N](url)（可选）\n```\n\n- 一级：`- 板块名：`\n- 二级：`\t- 动作词：描述。`（Tab 缩进）\n\n## 动作词\n\n| 动作词 | 含义 | 使用场景 |\n|--------|------|----------|\n| 新增 | 新功能、新模块 | feat、新增插件、新 starter |\n| 修复 | Bug 修复 | fix |\n| 重构 | 结构调整 | refactor |\n| 优化 | 改进、优化 | 优化、perf |\n| 移除 | 删除模块/功能 | 删除、移除 |\n| 拆分 | 模块拆分 | 拆分 |\n| 同步 | 内容同步 | 文档、赞助者、博客列表 |\n| 补全 | 补充内容 | 补全文档、测试 |\n| 升级 | 升级、变更 | 升级 API、模块 |\n\n## 链接格式\n\n**PR：**\n```markdown\nmerge: [pr 340](https://gitee.com/dromara/sa-token/pulls/340)\n```\n\n**Issue：**\n```markdown\nfix: [#IA6ZK0](https://gitee.com/dromara/sa-token/issues/IA6ZK0)\n```\n\n## 重要标记\n\n对用户影响较大的变更加 `**[重要]**`，通常放在句末、链接前：\n\n```markdown\n- 新增：新增 `sa-token-spring-boot4-starter` 集成包。  **[重要]**\n- 新增：loveqq-framework 启动器集成。merge: [pr 340](url)\n```\n\n## 完整示例\n\n```markdown\n### v1.45.0（开发中）\n\n- 插件：\n\t- 新增：新增 `sa-token-jackson3` 插件，用于 Jackson 3 的 JSON 解析。  **[重要]**\n\t- 新增：新增 `sa-token-jackson3` 单元测试。\n- starter：\n\t- 新增：新增 `sa-token-spring-boot4-starter` 集成包，支持 Spring Boot 4。  **[重要]**\n\t- 新增：新增 `sa-token-reactor-spring-boot4-starter` 集成包，支持 WebFlux + Spring Boot 4。  **[重要]**\n\t- 新增：新增 `sa-token-demo-webflux-springboot4` 示例。\n\t- 新增：新增 Spring Boot 4 整合 demo 示例。\n- 重构：\n\t- 重构：`sa-token-dependencies` 重构为 `sa-token-basic-dependencies`。  **[重要]**\n\t- 重构：重构 Spring Boot 相关集成包，优化依赖关系。\n\t- 移除：移除 `sa-token-spring-boot-autoconfig` 模块，相关逻辑迁移至各 starter 内。  **[重要]**\n\t- 重构：重构模块依赖层级，新增 `sa-token-special-dependencies`。\n- Solon：\n\t- 优化：`sa-token-solon-plugin` 优化 Gateway 接口的处理，避免使用路由接口。merge: [pr 348](https://gitee.com/dromara/sa-token/pulls/348)\n- 其它：\n\t- 新增：loveqq-framework 启动器集成。merge: [pr 340](https://gitee.com/dromara/sa-token/pulls/340)\n\t- 修复：修复 Maven 父子项目无法下载依赖的问题。merge: [pr 358](https://gitee.com/dromara/sa-token/pulls/358)\n- 文档：\n\t- 同步：同步公众号文章列表、博客列表、赞助者名单。\n\t- 新增：新增《Gitee 2025年度开源项目 Web应用开发 Top 2》证书展示。\n\t- 优化：优化框架 Slogan、README、案例库展示。\n\t- 修复：错别字修复；文档图片地址更换为本地文件。\n- 其它：\n\t- 新增：增加忽略 .vscode 目录。\n\t- 优化：注释优化。\n```\n\n## 提交信息到条目的映射示例\n\n| 提交信息 | 生成条目 |\n|----------|----------|\n| `feat: 添加 sa-token-jackson3 插件` | 新增：新增 `sa-token-jackson3` 插件，用于 Jackson 3 的 JSON 解析。  **[重要]** |\n| `refactor: 移除 sa-token-spring-boot-autoconfig 模块` | 移除：移除 `sa-token-spring-boot-autoconfig` 模块，相关逻辑迁移至各 starter 内。  **[重要]** |\n| `docs: 同步最新赞助者名单` | 同步：同步赞助者名单。 |\n| `!358 update maven-pull.md` | 修复：修复 Maven 父子项目无法下载依赖的问题。merge: [pr 358](url) |\n\n## 文档类合并建议\n\n以下类型可合并为一条：\n\n- 同步公众号、博客、赞助者名单 → 「同步：同步公众号文章列表、博客列表、赞助者名单。」\n- 多条例错别字修复 → 「修复：错别字修复。」\n- 多篇文档图片本地化 → 「修复：文档图片地址更换为本地文件（基础篇、深入篇、SSO篇等）。」\n"
  },
  {
    "path": ".agents/skills/remove-redundancy-import/SKILL.md",
    "content": "---\nname: remove-redundancy-import\ndescription: 检查 Java 类中未被引用的冗余 import 并移除。先输出待审阅计划，用户确认后执行。适用于用户要求清理冗余导包、优化 import、或执行 remove-redundancy-import 时使用。\n---\n\n# 移除冗余 import\n\n检查项目中所有 Java 类的未使用 import，生成清理计划供用户审阅，确认后执行移除。\n\n## 使用时机\n\n- 用户要求清理冗余导包\n- 用户要求优化 Java import\n- 用户明确执行 `remove-redundancy-import` 或提及本 Skill 名称\n\n## 强制流程\n\n**必须先输出计划，用户确认后再执行移除。** 不得在未审阅的情况下直接修改文件。\n\n## 工作流程\n\n### 第一步：扫描与解析\n\n**优先使用内置脚本**：在 Skill 目录下的 [scan_redundant_imports.py](scan_redundant_imports.py) 已实现完整扫描逻辑，可直接复用。\n\n```bash\n# 在项目根目录执行\npython .agents/skills/remove-redundancy-import/scan_redundant_imports.py\n# 或指定扫描根路径\npython .agents/skills/remove-redundancy-import/scan_redundant_imports.py .\n```\n\n脚本输出格式：`文件路径 | 冗余import1; import2 | 数量`，末尾两行为 `TOTAL_FILES:N` 和 `TOTAL_IMPORTS:M`。\n\n**若无 Python 环境**，可手动执行：\n1. 使用 `Glob` 查找项目内所有 `**/*.java` 文件\n2. 对每个文件：提取 `package`、`import`，按 [reference.md](reference.md) 判定是否被使用\n3. 汇总存在冗余 import 的文件及列表\n\n### 第二步：输出计划\n\n使用下方模板生成计划报告，等待用户确认：\n\n```markdown\n## 冗余 import 清理计划\n\n| 文件 | 待移除 import | 数量 |\n|------|---------------|------|\n| path/to/Foo.java | `java.util.Date`, `java.sql.Timestamp` | 2 |\n| ... | ... | ... |\n\n**共 N 个文件，M 处冗余 import。确认后执行移除。**\n```\n\n### 第三步：执行移除\n\n用户确认后，对计划中的每个文件使用 `StrReplace` 移除对应 import 行：\n\n- 逐行移除，每行格式为 `import ...;` 或 `import static ...;`\n- 若某 import 后紧跟空行，可一并移除空行以保持格式整洁\n- 移除后确认文件无语法错误\n\n## 检测规则概要\n\n- **普通 import**：取最后一段类名（如 `java.util.List` → `List`），在类体中搜索 `\\bList\\b`\n- **static import**：取方法/字段名，在类体中搜索\n- **同包冗余**：import 的包与当前文件 `package` 相同则视为冗余\n- **通配符**：`import pkg.*` 跳过，不自动处理\n\n详见 [reference.md](reference.md)。\n\n## 注意事项\n\n- 通配符 import 无法可靠判断，一律跳过\n- 注解中的类型引用采用保守策略，宁可漏检不误删\n- 移除后建议用户运行 `mvn compile` 验证\n"
  },
  {
    "path": ".agents/skills/remove-redundancy-import/reference.md",
    "content": "# 冗余 import 检测规则\n\n## 解析步骤\n\n### 1. 提取 package\n\n匹配 `package\\s+([\\w.]+)\\s*;`，得到当前文件所在包。\n\n### 2. 提取 import\n\n匹配以下模式（每行一条）：\n\n- `import\\s+([\\w.]+)\\s*;` — 普通 import\n- `import\\s+static\\s+([\\w.]+)\\s*;` — static 导入类\n- `import\\s+static\\s+([\\w.]+)\\.(\\w+)\\s*;` — static 导入成员（方法/字段）\n- `import\\s+[\\w.]+\\s*\\.\\s*\\*\\s*;` — 通配符，**跳过不处理**\n\n### 3. 确定简单名（Simple Name）\n\n| import 类型 | 示例 | 简单名 |\n|-------------|------|--------|\n| 普通类 | `import java.util.List;` | `List` |\n| 内部类 | `import pkg.Outer.Inner;` | `Inner` |\n| static 类 | `import static pkg.Utils;` | `Utils` |\n| static 成员 | `import static pkg.Utils.foo;` | `foo` |\n\n### 4. 同包冗余\n\n若 `import x.y.Z` 的包 `x.y` 与当前文件 `package x.y` 相同，则该 import 冗余（同包无需导入）。\n\n### 5. 使用检测\n\n在**类体**（`package` 和所有 `import` 之后）中搜索：\n\n- 使用正则 `\\bSimpleName\\b` 匹配整词，避免误匹配子串\n- 排除：注释、字符串字面量中的出现\n- 若未找到匹配，则该 import 视为未使用\n\n## 边界情况\n\n| 情况 | 处理方式 |\n|------|----------|\n| `import pkg.*;` | 跳过，不自动移除 |\n| 注解中的类型 `@Foo` | 若 `Foo` 为 import 的简单名，视为已使用 |\n| 泛型 `List<String>` | `List` 会匹配，视为已使用 |\n| 同名类（如 `java.util.Date` 与 `java.sql.Date`） | 两 import 都保留；若仅一个被使用，只移除未使用的 |\n| Javadoc `@param` 中的类型 | 保守：若不确定则保留 |\n\n## 正则参考\n\n```\n// package\npackage\\s+([\\w.]+)\\s*;\n\n// 普通 import（非通配符）\nimport\\s+(?!static)([\\w.]+)\\s*;\n\n// static import 成员\nimport\\s+static\\s+[\\w.]+\\.(\\w+)\\s*;\n\n// static import 类\nimport\\s+static\\s+([\\w.]+)\\s*;\n```\n"
  },
  {
    "path": ".agents/skills/remove-redundancy-import/scan_redundant_imports.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n冗余 import 扫描脚本\n按 reference.md 规则扫描项目内 Java 文件，输出待移除的冗余 import 列表。\n用法：在项目根目录执行 python scan_redundant_imports.py\n\"\"\"\nimport os\nimport re\nimport sys\n\n\ndef get_simple_name(imp: str) -> str | None:\n    \"\"\"从 import 行提取简单名（用于类体搜索）\"\"\"\n    m = re.match(r'import\\s+static\\s+[\\w.]+\\.(\\w+)\\s*;', imp)\n    if m:\n        return m.group(1)\n    m = re.match(r'import\\s+(?:static\\s+)?([\\w.]+)\\s*;', imp)\n    if m:\n        return m.group(1).split('.')[-1]\n    return None\n\n\ndef get_import_full(imp: str) -> str:\n    \"\"\"提取 import 的完整限定名\"\"\"\n    m = re.match(r'import\\s+(?:static\\s+)?([\\w.]+)\\s*;', imp)\n    return m.group(1).strip() if m else ''\n\n\ndef get_import_package(imp: str) -> str:\n    \"\"\"提取 import 所在包（用于同包冗余判断）\"\"\"\n    m = re.match(r'import\\s+(?:static\\s+)?([\\w.]+)\\s*;', imp)\n    if m:\n        parts = m.group(1).split('.')\n        return '.'.join(parts[:-1]) if len(parts) > 1 else ''\n    return ''\n\n\ndef find_class_body_start(content: str) -> int:\n    \"\"\"找到类体起始位置（最后一个 import 之后）\"\"\"\n    last = 0\n    for m in re.finditer(r'import\\s+(?:static\\s+)?[\\w.]+\\s*;', content):\n        last = m.end()\n    return last\n\n\ndef main() -> None:\n    root = sys.argv[1] if len(sys.argv) > 1 else '.'\n    skip_dirs = {'target', 'build', '.git', 'node_modules'}\n\n    results = []\n    for dirpath, dirnames, filenames in os.walk(root):\n        dirnames[:] = [d for d in dirnames if d not in skip_dirs]\n        for f in filenames:\n            if not f.endswith('.java'):\n                continue\n            path = os.path.join(dirpath, f).replace('\\\\', '/')\n            try:\n                with open(path, 'r', encoding='utf-8', errors='ignore') as fp:\n                    content = fp.read()\n            except OSError:\n                continue\n\n            pkg_match = re.search(r'package\\s+([\\w.]+)\\s*;', content)\n            file_pkg = pkg_match.group(1) if pkg_match else ''\n\n            imports = re.findall(r'import\\s+(?:static\\s+)?[\\w.]+\\s*;', content)\n            imports = [i for i in imports if '*;' not in i and '.*' not in i]\n\n            body_start = find_class_body_start(content)\n            body = content[body_start:]\n\n            redundant = []\n            for imp in imports:\n                simple = get_simple_name(imp)\n                if not simple:\n                    continue\n                imp_full = get_import_full(imp)\n                imp_pkg = get_import_package(imp)\n                if imp_pkg and imp_pkg == file_pkg:\n                    redundant.append(imp_full)\n                    continue\n                if not re.search(r'\\b' + re.escape(simple) + r'\\b', body):\n                    redundant.append(imp_full)\n\n            if redundant:\n                results.append((path, redundant))\n\n    for path, red in results:\n        print(f\"{path} | {'; '.join(red)} | {len(red)}\")\n    print(\"TOTAL_FILES:\" + str(len(results)))\n    print(\"TOTAL_IMPORTS:\" + str(sum(len(r[1]) for r in results)))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": ".agents/skills/upgrade-version/SKILL.md",
    "content": "---\nname: upgrade-version\ndescription: 将 Sa-Token 项目版本号升级到指定新版本。每次调用时先读取当前版本并提示用户，待用户输入目标版本后再执行批量修改。修改范围：pom.xml、核心常量、Demo 子项目及文档。当用户要求升级版本、修改版本号、或 version bump 时使用。\n---\n\n# Sa-Token 版本升级\n\n将项目版本号升级到用户指定的新版本。每次调用时**先读取当前版本并询问目标版本**，用户确认后再批量修改核心构建、Demo 项目及文档中的版本引用。\n\n## 使用时机\n\n- 用户要求升级项目版本、修改版本号\n- 用户说「版本从 vX.Y.Z 升级到 vX.Y.Z」「bump version」等\n\n## 工作流程\n\n### 第零步：询问目标版本（必须执行，不得跳过）\n\n1. **读取当前版本**：从 `pom.xml` 的 `<revision>` 或 `SaTokenConsts.java` 的 `VERSION_NO` 中读取当前版本号\n2. **提示用户**：明确告知「当前版本号是：xxx」\n3. **等待输入**：询问「请输入要升级到的目标版本号（如 1.46.0）：」\n4. **确认后再执行**：**必须**等用户明确回复目标版本号后，才能执行后续修改步骤。若用户仅说「升级版本」而未给出目标版本，先完成本步骤再继续\n\n### 第一步：核心构建配置（3 个文件）\n\n使用「当前版本」「目标版本」进行替换：\n\n| 文件 | 修改内容 |\n|------|----------|\n| `pom.xml` | `<revision>当前版本</revision>` → 目标版本 |\n| `sa-token-bom/pom.xml` | `<revision>当前版本</revision>` → 目标版本 |\n| `sa-token-core/.../SaTokenConsts.java` | `VERSION_NO = \"v当前版本\"` → `\"v目标版本\"` |\n\n### 第二步：Demo 项目（sa-token-demo 下所有 pom.xml）\n\n- 将 `<sa-token.version>当前版本</sa-token.version>` 改为目标版本\n- **sa-token-demo-bom-import** 额外修改：`<dependencyManagement>` 内 `sa-token-bom` 的 `<version>当前版本</version>` → 目标版本\n\n**查找方式**：`grep \"当前版本\" sa-token-demo --output-mode files_with_matches` 定位所有需修改的 pom.xml。\n\n### 第三步：文档（6 个文件）\n\n| 文件 | 修改内容 |\n|------|----------|\n| `README.md` | 标题 `v当前版本` → `v目标版本`；Maven 依赖 `<version>当前版本</version>` → 目标版本 |\n| `sa-token-doc/README.md` | 标题 `v当前版本` → `v目标版本` |\n| `sa-token-doc/index.html` | `<small>v当前版本</small>` → `v目标版本` |\n| `sa-token-doc/doc.html` | `<sub>v当前版本</sub>` 和 `saTokenTopVersion = '当前版本'` → 目标版本 |\n| `sa-token-doc/start/new-version.md` | 文案及 Maven 示例中的当前版本 → 目标版本 |\n\n### 第四步：不修改的文件\n\n以下为历史记录或示例，**保持原样**：\n\n- `.agents/skills/` 下的示例（format-reference.md、SKILL.md 等）\n- `MEMO/` 下的历史备忘录\n- `sa-token-core/.../*.java` 中的 `@since X.Y.Z`（表示 API 引入版本，不随发布升级）\n- `sa-token-doc/more/update-log.md`：更新日志应**新增**新版本条目，而非修改旧条目\n- `sa-token-doc/more/blog.md`：历史博客链接\n\n## 替换规则\n\n- **pom.xml**：`当前版本` → `目标版本`\n- **Java**：`\"v当前版本\"` → `\"v目标版本\"`\n- **HTML/MD**：`v当前版本` 和 `当前版本` 按上下文分别替换为目标版本\n\n## 执行顺序建议\n\n1. 第零步：读取当前版本 → 提示用户 → 等待用户输入目标版本\n2. 根 POM、sa-token-bom\n3. SaTokenConsts.java\n4. 批量修改 Demo pom.xml\n5. 修改文档\n\n## 验证\n\n执行完成后，用 `grep \"被替换的版本号\"` 在项目根目录搜索，确认仅剩「不修改」列表中的文件仍含该版本（即修改已生效）。\n"
  },
  {
    "path": ".gitee/ISSUE_TEMPLATE.md",
    "content": "请在以下地址复制 issue 模板进行提交：\nhttps://sa-token.cc/doc.html#/fun/issue-template\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug反馈.md",
    "content": "---\nname: bug反馈\nabout: 当你明确框架存在 bug 时，选择这个模板提交\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n### 使用版本:\n\n\n### 报错信息：\n\n\n### 希望结果：\n\n\n### 复现步骤：\n\n\n< 备注：如果复现步骤比较复杂，请将 demo 上传到 gitee 并留下地址 >\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/功能提问.md",
    "content": "---\nname: 功能提问\nabout: 对框架的某个功能看不明白时，选择这个模板提交\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n### 对以下问题有疑问:\n\n\n< 备注：请尽量详细描述问题所在 >\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/建议增加新功能.md",
    "content": "---\nname: 建议增加新功能\nabout: 当你有一个好 idea 时，选择这个模板提交\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n### 建议增加的新功能:\n\n\n### 应用场景阐述：\n\n\n< 备注：请尽量详细描述功能应用场景 >\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/预期不符.md",
    "content": "---\nname: 预期不符\nabout: 当框架的运行结果和你的预期不一致时，选择这个模板提交\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n### 使用版本:\n\n\n### 涉及的功能模块：\n\n\n### 测试步骤：\n+ 我经过以下步骤测试：\n\n+ 得出以下结果：\n\n+ 其中第 xx 行的代码输出表现 和文档上描述的不一致：\n\n+ 我的理解是：\n\n请问，是我的理解不对，还是文档出了问题？\n"
  },
  {
    "path": ".gitignore",
    "content": "target/\n\nnode_modules/\nbin/\n.settings/\nunpackage/\n.classpath\n.project\n*.iml\n\n.factorypath\n/.factorypath\n\n.idea/\n.vscode/\n\nsa-token-three-plugin/\nsa-token-doc/big-file/\n\n.flattened-pom.xml\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        https://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2011-Present hubin.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       https://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MEMO/1--统一定义properties尝试失败.md",
    "content": "\n## 未完成目标\n\t\n\t\n### 1、尝试将所有 `<properties>` 依赖版本号定义在同一个 pom.xml 里。 **[❌失败]**\n\n**尝试1：将所有 `<properties>` 定义在 `sa-token-dependencies` 里：**\n\n结果： 无法在 `sa-token-spring-boot2/3/4-dependencies` 中引用这些 `<properties>`，因为 `<dependencyManagement> <dependencies> <scope>import</scope>` 只会导入目标的 `<dependencyManagement>` 版本号定义，不会导入目标的 `<properties>` 属性。\n\n`<properties>` 只会在 父子结构中向下传递，不会在  `<dependencyManagement> <dependencies> <scope>import</scope>` 中传递。\n\n\n**尝试2：将所有 `<properties>` 定义在 `sa-token-parent` 里：**\n\n结果：在 `sa-token-dependencies` 里无法引用这些 `<properties>`，因为 `sa-token-parent` 不是 `sa-token-dependencies` 的父模块。\n\n将 `sa-token-parent` 定义为 `sa-token-dependencies` 的父模块行吗？\n\n不行，因为在 `sa-token-parent` 通过  `<dependencyManagement> <dependencies> <scope>import</scope>` 导入了 `sa-token-dependencies`，如果再把 `sa-token-parent` 定义为 `sa-token-dependencies` 的父模块，会造成循环依赖。\n\n执行 `mvn package` 打包时，maven 会直接报错：\n\t\n```\n[ERROR] [ERROR] Some problems were encountered while processing the POMs:\n[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\n @\n[ERROR] The build could not read 1 project -> [Help 1]\n[ERROR]\n[ERROR]   The project cn.dev33:sa-token-parent:1.44.0 (E:\\work\\project-yun\\sa-token\\pom.xml) has 1 error\n[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\n[ERROR]\n[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.\n[ERROR] Re-run Maven using the -X switch to enable full debug logging.\n[ERROR]\n[ERROR] For more information about the errors and possible solutions, please read the following articles:\n[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException\n```\n\n\n\n"
  },
  {
    "path": "MEMO/2--2026-3-1_诡异调试记录.txt",
    "content": "\n2026-3-1 调试记录\n\n启动 SaOAuth2ServerApplication，报错空指针：SaOAuth2ServerController 文件的 SaOAuth2Strategy.instance.notLoginView 空指针，SaOAuth2Strategy.instance 为 null \n\n\nSaOAuth2Strategy.instance 的定义为：\npublic static final SaOAuth2Strategy instance = new SaOAuth2Strategy();\n\n看代码是无论如何也不可能空指针的，诡异。\n\n在 main 方法第一句加上测试\n\n@SpringBootApplication\npublic class SaOAuth2ServerApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSystem.out.println(SaOAuth2Strategy.instance);\n\t\tSpringApplication.run(SaOAuth2ServerApplication.class, args);\n\t\tSystem.out.println(\"\\nSa-Token-OAuth2 Server端启动成功，配置如下：\");\n\t\tSystem.out.println(SaOAuth2Manager.getServerConfig());\n\t}\n\t\n}\n\n打印居然为 null。\n\n询问 AI，解释的乱七八糟，没有参考价值。\n\n然后在根目录执行 mvn clean，居然无法成功。sa-token-test 模块无法 clean 。\n\n报错 test 依赖不存在\n\n<dependency>\n\t<groupId>org.springframework.boot</groupId>\n\t<artifactId>spring-boot-starter-test</artifactId>\n\t<scope>test</scope>\n</dependency>\n\n最后必须在 dependencyManagement 加上这个才行\n\n<dependency>\n\t<groupId>org.springframework.boot</groupId>\n\t<artifactId>spring-boot-starter-test</artifactId>\n\t<version>2.7.18</version>\n</dependency>\n\n可是就算不加，我也已经在 sa-token-spring-boot2-dependencies 中定义这个依赖了呀，为什么 在 sa-token-test 中无法 import spring-boot-starter-test ？\n\n加了后，mvn clean 执行成功了\n\n但是 mvn package 又开始无法打包。\n\n可以昨天我明明能打包成功的啊？今天好像就变动了一下 sa-token-test 中的依赖配置。这有什么影响吗？\n\n而且打包报错信息居然是：sa-token-jboot-plugin 插件中 javax.servlet.http.HttpServletRequest 无法转换为 HttpServletRequest\n\n什么东西啊。\n\n抓头挠腮解决不了。\n\n这个插件已经十几个版本没有变动过代码了，代码不变，打包环境不变，命令不变，今天就突然报这种莫名其妙的错误，无奈，只能先去除这个插件，不让它参与打包。\n\n继续打包，又开始报错：\n\nsa-token-jfinal-plugin 中 cn.dev33.satoken.context.SaTokenContext 无法转换为 SaTokenContext。\n\n这一瞬间我怀疑自己正处于梦中。\n\n纠结了半分钟，继续去除此插件，继续打包。\n\n打包成功了。\n\n启动 SaOAuth2ServerApplication，启动成功，SaOAuth2ServerController 文件的 SaOAuth2Strategy.instance.notLoginView 空指针问题，消失了。\n\n请问中间的这几个报错和这个空指针有任何关联吗？我请问呢？\n\n\n\n注：以上所有叙述均为最后打包成功后进行回忆，可能细节上略有偏差。\n\n\n\n\n\n\n\n两小时后：\n本来可以运行成功的代码，只要一改子模块的代码就无法再运行成功，报错：java: 无法访问SaRequest。\n试了好多解决方案，不行。\n\n--- \n吃了两份炉盖香酥鸡饼，原来人在压力大的时候真的需要补充能量。\n---\n\n继续报错：\nMaven 资源编译器: 模块 'sa-token-oauth2' 所需的 Maven 项目配置不可用。仅当从 IDE 启动外部构建时，才支持 Maven 项目编译。\nsa-token-jwt、sso、sign 等模块均出现此问题 \n\n最后：\n把项目删掉，重新下载一份，导入\n项目可以运行成功了，但是每次修改子模块，在 demo 示例里无法实时起作用。需要 mvn clean install 才能看到效果。\n\n最后：\n取消勾选 maven 配置项：Delegate IDE build/run actions to Maven\n\n一切问题解决，包括最上面的诡异调试现象也消失了。\n\nidea，你给老子爬 \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "MEMO/3--sa-token_最新版所有依赖.txt",
    "content": "        <!-- sa-token-bom -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-bom</artifactId>\n            <version>1.45.0</version>\n            <type>pom</type>\n            <scope>import</scope>\n        </dependency>\n\n        <!-- sa-token-core -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-boot-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-boot3-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot3-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-boot4-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot4-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-reactor-spring-boot-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-reactor-spring-boot3-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-reactor-spring-boot4-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-reactor-spring-boot4-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-jboot-plugin -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jboot-plugin</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-jfinal-plugin -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jfinal-plugin</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-loveqq-boot-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-loveqq-boot-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-servlet -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-servlet</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-jakarta-servlet -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jakarta-servlet</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-plugin -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-plugin</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-alone-redis -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-alone-redis</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-redis-template -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-redis-template-jdk-serializer -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template-jdk-serializer</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-redis-jackson -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-redisson -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisson</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-redisson-spring-boot-starter -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisson-spring-boot-starter</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-redisx -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisx</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-hutool-timed-cache -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-hutool-timed-cache</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-caffeine -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-caffeine</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-jackson -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jackson</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-jackson3 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jackson3</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-fastjson -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-fastjson</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-fastjson2 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-fastjson2</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-snack3 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-snack3</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-snack4 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-snack4</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-serializer-features -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-serializer-features</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-thymeleaf -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-thymeleaf</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-freemarker -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-freemarker</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-dubbo -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-dubbo</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-dubbo3 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-dubbo3</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-grpc -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-grpc</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-forest -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-forest</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-okhttps -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-okhttps</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-jwt -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jwt</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-temp-jwt -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-temp-jwt</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-oauth2 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-oauth2</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-apikey -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-apikey</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-sign -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sign</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-quick-login -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-quick-login</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-sso -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-aop -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-aop</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-el -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-el</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-boot-webmvc-reactor-v2v3v4-common -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-boot-reactor-v2v3v4-common -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-reactor-v2v3v4-common</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n\n        <!-- sa-token-spring-boot-webmvc-v3v4-common -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-webmvc-v3v4-common</artifactId>\n            <version>1.45.0</version>\n        </dependency>\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n\t<img alt=\"logo\" src=\"https://sa-token.cc/logo.png\" width=\"150\" height=\"150\">\n</p>\n<h1 align=\"center\" style=\"margin: 30px 0 30px; font-weight: bold;\">Sa-Token v1.45.0</h1>\n<h4 align=\"center\">✨ 开源、免费、一站式 java 权限认证框架，让鉴权变得简单、优雅！ </h4>\n<p align=\"center\">\n\t<a href=\"https://gitee.com/dromara/sa-token/stargazers\"><img src=\"https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp\"></a>\n\t<a href=\"https://gitee.com/dromara/sa-token/members\"><img src=\"https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp\"></a>\n\t<a href=\"https://atomgit.com/dromara/sa-token/stargazers\"><img src=\"https://atomgit.com/dromara/Sa-Token/star/badge.svg\"></a>\n\t<a href=\"https://github.com/dromara/sa-token/stargazers\"><img src=\"https://img.shields.io/github/stars/dromara/sa-token?style=flat-square&logo=GitHub\"></a>\n\t<a href=\"https://github.com/dromara/sa-token/network/members\"><img src=\"https://img.shields.io/github/forks/dromara/sa-token?style=flat-square&logo=GitHub\"></a>\n\t<!-- <a href=\"https://github.com/dromara/sa-token/watchers\"><img src=\"https://img.shields.io/github/watchers/dromara/sa-token?style=flat-square&logo=GitHub\"></a> -->\n\t<!-- <a href=\"https://github.com/dromara/sa-token/issues\"><img src=\"https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub\"></a> -->\n\t<a href=\"https://github.com/dromara/sa-token/blob/master/LICENSE\"><img src=\"https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square\"></a>\n</p>\n<!-- <p align=\"center\">学习测试请拉取 master 分支，dev 是在开发分支 (在根目录执行 `git checkout master`)</p> -->\n<p align=\"center\"><a href=\"https://sa-token.cc?way=readme\" target=\"_blank\">在线文档：https://sa-token.cc</a></p>\n\n\n---\n\n### 📝 前言：\n\n回望 2020 年初，我为 Sa-Token 提交第一行代码之际，彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架，更是一整套清晰、自洽的权限架构设计思想。\n\n因此，这几年间我将大量时间倾注在 Sa-Token 的文档编写，几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨，以求做到最清晰、干练、易懂的表述。用心阅读文档，你学习到的将不止是 Sa-Token 框架本身，更是绝大多数场景下权限设计的最佳实践。\n\n\n\n### 🛠️ Sa-Token 介绍\n\nSa-Token 是一个轻量级 Java 权限认证框架，目前拥有五大核心模块：登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。\n\n![sa-token-jss](https://sa-token.cc/big-file/index/intro/sa-token-jss--tran.png)\n\n要在 SpringBoot 项目中使用 Sa-Token，你只需要在 pom.xml 中引入依赖：\n\n``` xml\n<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>1.45.0</version>\n</dependency>\n```\n\n除了支持 SpringBoot2、Sa-Token 还为 SpringBoot3/4、Solon、JFinal 等常见 Web 框架提供集成包，做到真正的开箱即用。\n\n\n<details>\n<summary><b>简单示例展示：</b>（点击展开 / 折叠）</summary>\n\nSa-Token 旨在以简单、优雅的方式完成系统的权限认证部分，以登录认证为例，你只需要：\n\n``` java\n// 会话登录，参数填登录人的账号id \nStpUtil.login(10001);\n```\n\n无需实现任何接口，无需创建任何配置文件，只需要这一句静态代码的调用，便可以完成会话登录认证。\n\n如果一个接口需要登录后才能访问，我们只需调用以下代码：\n\n``` java\n// 校验当前客户端是否已经登录，如果未登录则抛出 `NotLoginException` 异常\nStpUtil.checkLogin();\n```\n\n在 Sa-Token 中，大多数功能都可以一行代码解决：\n\n踢人下线：\n\n``` java\n// 将账号id为 10077 的会话踢下线 \nStpUtil.kickout(10077);\n```\n\n权限认证：\n\n``` java\n// 注解鉴权：只有具备 `user:add` 权限的会话才可以进入方法\n@SaCheckPermission(\"user:add\")\npublic String insert(SysUser user) {\n    // ... \n    return \"用户增加\";\n}\n```\n\n路由拦截鉴权：\n\n``` java\n// 根据路由划分模块，不同模块不同鉴权 \nregistry.addInterceptor(new SaInterceptor(handler -> {\n\tSaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n\tSaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n\tSaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n\tSaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n\tSaRouter.match(\"/notice/**\", r -> StpUtil.checkPermission(\"notice\"));\n\t// 更多模块... \n})).addPathPatterns(\"/**\");\n```\n\n**如果您曾经使用过 Shiro、SpringSecurity，在切换到 Sa-Token 后，您将体会到质的飞跃。**\n\n<!-- 当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后，你就会明白，相对于这些传统老牌框架，Sa-Token 的 API 设计是多么的简单、优雅！ -->\n\n</details>\n\n\n<details>\n<summary> <b>核心模块一览：</b>（点击展开 / 折叠） </summary>\n\n- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。\n- **权限认证** —— 权限认证、角色认证、会话二级认证。\n- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线。\n- **注解式鉴权** —— 优雅的将鉴权与业务代码分离。\n- **路由拦截式鉴权** —— 根据路由拦截鉴权，可适配 restful 模式。\n- **Session会话** —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。\n- **持久层扩展** —— 可集成 Redis，重启数据不丢失。\n- **前后台分离** —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。\n- **Token风格定制** —— 内置六种 Token 风格，还可：自定义 Token 生成策略。\n- **记住我模式** —— 适配 [记住我] 模式，重启浏览器免验证。\n- **二级认证** —— 在已登录的基础上再次认证，保证安全性。 \n- **模拟他人账号** —— 实时操作任意用户状态数据。\n- **临时身份切换** —— 将会话身份临时切换为其它账号。\n- **同端互斥登录** —— 像QQ一样手机电脑同时在线，但是两个手机上互斥登录。\n- **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。\n- **密码加密** —— 提供基础加密算法，可快速 MD5、SHA1、SHA256、AES 加密。\n- **会话查询** —— 提供方便灵活的会话查询接口。\n- **Http Basic认证** —— 一行代码接入 Http Basic、Digest 认证。\n- **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。\n- **全局过滤器** —— 方便的处理跨域，全局设置安全响应头等操作。\n- **多账号体系认证** —— 一个系统多套账号分开鉴权（比如商城的 User 表和 Admin 表）\n- **单点登录** —— 内置三种单点登录模式：同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。\n- **单点注销** —— 任意子系统内发起注销，即可全端下线。\n- **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务，支持openid模式 。\n- **分布式会话** —— 提供共享数据中心分布式会话方案。\n- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。\n- **RPC调用鉴权** —— 网关转发鉴权，RPC调用鉴权，让服务调用不再裸奔\n- **临时Token认证** —— 解决短时间的 Token 授权问题。\n- **独立Redis** —— 将权限缓存与业务缓存分离。\n- **Quick快速登录认证** —— 为项目零代码注入一个登录页面。\n- **标签方言** —— 提供 Thymeleaf 标签方言集成包，提供 beetl 集成示例。\n- **jwt集成** —— 提供三种模式的 jwt 集成方案，提供 token 扩展参数能力。\n- **RPC调用状态传递** —— 提供 dubbo、grpc 等集成包，在RPC调用时登录状态不丢失。\n- **参数签名** —— 提供跨系统API调用签名校验模块，防参数篡改，防请求重放。\n- **自动续签** —— 提供两种Token过期策略，灵活搭配使用，还可自动续签。\n- **开箱即用** —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包，开箱即用。\n- **最新技术栈** —— 适配最新技术栈：支持 SpringBoot 3.x，jdk 17。\n\n</details>\n\n\n\n### 🍃 SSO 单点登录\n\nSa-Token SSO 分为三种模式，可解决：`同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目` 等架构下的 SSO 认证需求：\n\n![sa-token-jss](https://sa-token.cc/big-file/doc/sso/sa-token-sso--white.png)\n\n\n| 系统架构\t\t\t\t\t\t| 采用模式\t| 简介\t\t\t\t\t\t        |  文档链接\t|\n| :--------\t\t\t\t\t\t| :--------\t|:----------------| :--------\t|\n| 前端同域 + 后端同 Redis\t\t\t| 模式一\t\t| 共享Cookie同步会话\t\t\t | [文档](https://sa-token.cc/doc.html#/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client)\t|\n| 前端不同域 + 后端同 Redis\t\t| 模式二\t\t| URL重定向传播会话 \t\t\t  | [文档](https://sa-token.cc/doc.html#/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client)\t|\n| 前端不同域 + 后端 不同Redis\t\t| 模式三\t\t| HTTP请求获取会话\t\t\t   | [文档](https://sa-token.cc/doc.html#/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso3-client)\t|\n\n\n1. 前端同域：就是指多个系统可以部署在同一个主域名之下，比如：`c1.domain.com`、`c2.domain.com`、`c3.domain.com`\n2. 后端同 Redis：就是指多个系统可以连接同一个 Redis，共享会话数据。\n3. 如果无法做到前端同域、后端同 Redis，可以走托底的模式三：Http请求校验 ticket 获取会话。\n4. 提供：NoSdk 模式示例 + sso-server 接口文档，非 Sa-Token 项目、非 java 项目也可以对接。\n5. 提供：多重安全校验：域名校验、ticket校验、参数签名校验，有效防 ticket 劫持，防请求重放等攻击。\n6. 提供：大量实战痛点教学：sso-server 前后端分离设计、sso-client 前后端分离设计、用户数据同步/迁移方案设计。\n7. 提供：直接可运行的 demo 示例，助你快速熟悉 SSO 大致登录流程。\n8. 提供：深度细节优化，参数防丢：笔者曾试验多个SSO框架，均有参数丢失情况，比如登录前是：`http://a.com?id=1&name=2`，登录成功后就变成了：`http://a.com?id=1`，Sa-Token-SSO 内有专门算法保证了参数不丢失，登录成功后精准原路返回。\n\n\n\n\n### 🍂 OAuth2 授权认证\nSa-Token OAuth2 模块分为四种授权模式，解决不同场景下的授权需求 \n\n| 授权模式\t\t\t\t\t| 简介\t\t\t\t\t\t|\n| :--------\t\t\t\t\t| :--------\t\t\t\t\t|\n| 授权码式\t\t\t\t\t| OAuth2 标准授权步骤，server 端下放 code，client 端获取 code 码兑换 access_token\t\t\t|\n| 隐藏式\t\t\t\t\t| 备用选择，server 端使用 URL 重定向方式直接将 access_token 下放到 client 端页面 \t\t\t|\n| 密码式\t\t\t\t\t| client 直接拿着用户的账号密码换取授权 access_token\t\t\t\t|\n| 客户端凭证式\t\t\t\t| server 端针对 client 级别的 client_token，代表应用自身的资源授权\t\t|\n\n详细参考文档：[https://sa-token.cc/doc.html#/oauth2/readme](https://sa-token.cc/doc.html#/oauth2/readme)\n\n\n### 📖❓ 疑问解答\n\n**1、Sa-Token 功能全不全？** \n\n七年磨一剑：五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证，我们提供的是一站式解决方案。\n\n\n**2、Sa-Token 好不好学？** \n\n中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。\n\n\n**3、Sa-Token 用的人多不多？** \n\n截止统计日 (2026-1-25) 起，Sa-Token 在：\n\n- Gitee 关注量达到 48627 Star，位列平台所有推荐项目排行榜第一名。\n- GitHub 关注量达到 18523 Star，是主要竞争框架 Spring Security 的 1.97 倍，Apache Shiro 的 4.19 倍。\n- 25+ 微信粉丝群 (500人)，8+ QQ粉丝群 (1000人 or 2000人) ，在线文档访问量月PV 20万+。\n\n这是众多开发者用脚投票的数据，相信这些数据比任何言语都能证明 Sa-Token 的热度。\n\n\n**4、Sa-Token 有哪些权威认证？** \n\n曾获荣誉包括但不限于：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。\n\n\n**5、Sa-Token 收费吗？** \n\nSa-Token 采用 Apache-2.0 开源协议，承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token，我们也不回避：[赞助链接](https://sa-token.cc/doc.html#/more/sa-token-donate)。\n我们将定期同步赞助者名单到在线文档展示。（您需要注意的一点是：该赞助仅为友情赞助，不提供任何商业交换）\n\n\n**6、Sa-Token 是封装的 SpringSecurity 吗？是套壳 ApacheShiro 吗？** \n\n不是。Sa-Token 不是一个后台模板，也不是针对 xx 框架的二次封装套壳，而是从 0 开始的纯血自研框架，核心包零依赖，完全自主可控的架构内核 + 众多主流框架的集成适配。\n\t\t\t\t\t\t\n\n\n### 🚀 优秀开源集成案例\n\n- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy)：国内首个国密前后分离快速开发平台，采用 Vue3 + Vite + SpringBoot + Mp + HuTool + SaToken。\n- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus)：重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Xxl-Job、knife4j、OSS 定期同步。\n- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin)：SmartAdmin 国内首个以「高质量代码」为核心，「简洁、高效、安全」中后台快速开发平台。\n- [[ 橙单 ]](https://gitee.com/orangeform/orange-admin)： 橙单中台化低代码生成器。可完整支持多应用、多租户、多渠道、工作流、框架技术栈自由组合等。\n- [[ 灯灯 ]](https://gitee.com/dromara/lamp-cloud)： 专注于多租户解决方案的中后台快速开发平台。支持独立数据库、共享数据架构 和 非租户模式 ✨\n- [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog)：一款 vue + springboot 前后端分离的博客系统。\n\n\n\n还有更多优秀开源案例无法逐一展示，请参考：[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token)\n\n\n### 🔗 友情链接\n- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps)：轻量级 http 通信框架，API无比优雅，支持 WebSocket、Stomp 协议\n- [[ Forest ]](https://gitee.com/dromara/forest)：声明式与编程式双修，让天下没有难以发送的 HTTP 请求\n- [[ Bean Searcher ]](https://github.com/ejlchina/bean-searcher)：专注高级查询的只读 ORM，使一行代码实现复杂列表检索！\n- [[ Jpom ]](https://gitee.com/dromara/Jpom)：简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件。\n- [[ TLog ]](https://gitee.com/dromara/TLog)：一个轻量级的分布式日志标记追踪神器。\n- [[ hippo4j ]](https://gitee.com/agentart/hippo4j)：强大的动态线程池框架，附带监控报警功能。\n- [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat)：易用友好的开源实时监控告警系统，无需Agent，高性能集群，强大自定义监控能力。\n- [[ Solon ]](https://gitee.com/noear/solon)：一个更现代感的应用开发框架：更快、更小、更自由。\n- [[ Chat2DB ]](https://github.com/chat2db/Chat2DB)：一个AI驱动的数据库管理和BI工具，支持Mysql、pg、Oracle、Redis等22种数据库的管理。\n\n\n\n### 📦 代码托管\n- Gitee：[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token)\n- GitHub：[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token)\n- AtomGit：[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token)\n\n\n\n### 💬 交流群\n<!-- QQ交流群：685792424 [点击加入](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Y05Ld4125W92YSwZ0gA8e3RhG9Q4Vsfx&authKey=IomXuIuhP9g8G7l%2ByfkrRsS7i%2Fna0lIBpkTXxx%2BQEaz0NNEyJq00kgeiC4dUyNLS&noverify=0&group_code=685792424)-->\n\nQQ交流群：1081649142 [点击加入](https://qm.qq.com/q/SCAaZ6Ros2) \n\n微信交流群：\n\n<!-- <img src=\"https://oss.dev33.cn/sa-token/qr/wx-qr-m-400k.png\" width=\"230px\" title=\"微信群\" /> -->\n\n<img src=\"https://sa-token.cc/big-file/contact/i-wx-qr2.jpg\" width=\"230px\" title=\"微信群\" />\n\nPS：扫码添加微信 (备注：sa-token)，邀您加入群聊。\n\n<br>\n\n<img class=\"s-w\" src=\"https://sa-token.cc/big-file/contact/show/wx-group-show3--liubai.png\" style=\"max-width: 50%;\" alt=\"微信群\" />\n\n\n加入群聊的好处：\n- 第一时间收到框架更新通知。\n- 第一时间收到框架 bug 通知。\n- 第一时间收到新增开源案例通知。\n- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。\n\n"
  },
  {
    "path": "mvn clean.bat",
    "content": "\n:: 整体clean\ncall mvn clean\n\n\n:: demo模块clean\ncd sa-token-demo\n\ncd sa-token-demo-alone-redis & call mvn clean & cd ..\ncd sa-token-demo-alone-redis-cluster & call mvn clean & cd ..\ncd sa-token-demo-apikey & call mvn clean & cd ..\ncd sa-token-demo-async & call mvn clean & cd ..\ncd sa-token-demo-beetl & call mvn clean & cd ..\ncd sa-token-demo-bom-import & call mvn clean & cd ..\ncd sa-token-demo-case & call mvn clean & cd ..\ncd sa-token-demo-device-lock & call mvn clean & cd ..\ncd sa-token-demo-grpc & call mvn clean & cd ..\ncd sa-token-demo-hutool-timed-cache & call mvn clean & cd ..\ncd sa-token-demo-caffeine & call mvn clean & cd ..\ncd sa-token-demo-jwt & call mvn clean & cd ..\ncd sa-token-demo-quick-login & call mvn clean & cd ..\ncd sa-token-demo-quick-login-sb3 & call mvn clean & cd ..\ncd sa-token-demo-solon & call mvn clean & cd ..\ncd sa-token-demo-solon-redisson & call mvn clean & cd ..\ncd sa-token-demo-springboot & call mvn clean & cd ..\ncd sa-token-demo-springboot3-redis & call mvn clean & cd ..\ncd sa-token-demo-springboot4-redis & call mvn clean & cd ..\ncd sa-token-demo-springboot-low-version & call mvn clean & cd ..\ncd sa-token-demo-springboot-redis & call mvn clean & cd ..\ncd sa-token-demo-springboot-redisson & call mvn clean & cd ..\ncd sa-token-demo-sse & call mvn clean & cd ..\ncd sa-token-demo-ssm & call mvn clean & cd ..\ncd sa-token-demo-test & call mvn clean & cd ..\ncd sa-token-demo-thymeleaf & call mvn clean & cd ..\ncd sa-token-demo-freemarker & call mvn clean & cd ..\ncd sa-token-demo-webflux & call mvn clean & cd ..\ncd sa-token-demo-webflux-springboot3 & call mvn clean & cd ..\ncd sa-token-demo-websocket & call mvn clean & cd ..\ncd sa-token-demo-websocket-spring & call mvn clean & cd ..\n\ncd sa-token-demo-dubbo\ncd sa-token-demo-dubbo-consumer & call mvn clean & cd ..\ncd sa-token-demo-dubbo-provider & call mvn clean & cd ..\ncd sa-token-demo-dubbo3-consumer & call mvn clean & cd ..\ncd sa-token-demo-dubbo3-provider & call mvn clean & cd ..\ncd ..\n\ncd sa-token-demo-oauth2\ncd sa-token-demo-oauth2-client & call mvn clean & cd ..\ncd sa-token-demo-oauth2-server & call mvn clean & cd ..\ncd ..\n\ncd sa-token-demo-remember-me\ncd sa-token-demo-remember-me-server & call mvn clean & cd ..\ncd ..\n\ncd sa-token-demo-sso\ncd sa-token-demo-sso-server & call mvn clean & cd ..\ncd sa-token-demo-sso1-client & call mvn clean & cd ..\ncd sa-token-demo-sso2-client & call mvn clean & cd ..\ncd sa-token-demo-sso3-client & call mvn clean & cd ..\ncd sa-token-demo-sso3-client-nosdk & call mvn clean & cd ..\ncd sa-token-demo-sso3-client-resdk & call mvn clean & cd ..\ncd sa-token-demo-sso3-client-anon & call mvn clean & cd ..\ncd ..\n\ncd sa-token-demo-sso-for-solon\ncd sa-token-demo-sso1-client-solon & call mvn clean & cd ..\ncd sa-token-demo-sso2-client-solon & call mvn clean & cd ..\ncd sa-token-demo-sso3-client-solon & call mvn clean & cd ..\ncd sa-token-demo-sso-server-solon & call mvn clean & cd ..\ncd ..\n\n\n\ncd ..\n\n:: test clean \n\ncd sa-token-test\ncall mvn clean\ncd ..\n\n\n\n\n:: 最后打印\necho;\necho;\necho ----------- clean end ----------- \necho;\npause"
  },
  {
    "path": "mvn test.bat",
    "content": "\n:: 整体test\ncall mvn clean test\n\n\n:: 最后打印\necho;\necho;\necho ----------- test end ----------- \necho;\npause"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<!-- 基础信息 -->\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-parent</artifactId>\n\t<packaging>pom</packaging>\n\t<version>${revision}</version>\n\t\n\t<!-- 项目介绍 -->\n\t<name>sa-token</name>\n\t<description>An open-source, free, and one-stop Java authentication framework that makes authentication simple and elegant!</description>\n\t<url>https://github.com/dromara/sa-token</url>\n\n\t\n\t<!-- 所有模块 -->\n\t<modules>\n\t\t<module>sa-token-dependencies</module>\n\t\t<module>sa-token-special-dependencies</module>\n\t\t<module>sa-token-bom</module>\n\t\t<module>sa-token-core</module>\n\t\t<module>sa-token-starter</module>\n\t\t<module>sa-token-plugin</module>\n\t</modules>\n\n\t<!-- 开源协议 apache 2.0 -->\n\t<licenses>\n\t\t<license>\n\t\t\t<name>Apache 2</name>\n\t\t\t<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\n\t\t\t<distribution>repo</distribution>\n\t\t\t<comments>A business-friendly OSS license</comments>\n\t\t</license>\n\t</licenses>\n\t\n\t<!-- 一些属性 -->\n\t<properties>\n        <revision>1.45.0</revision>\n        <jdk.version>1.8</jdk.version>\n\t\t<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>\n\t\t<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>\n\n\t\t<!-- Maven GPG Plugin & Maven Central Portal -->\n\t\t<maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>\n\t\t<central.publishing.maven.version>0.10.0</central.publishing.maven.version>\n\t</properties>\n\n\t<!-- 仓库信息 -->\n\t<scm>\n\t\t<tag>master</tag>\n\t\t<url>https://github.com/dromara/sa-token.git</url>\n\t\t<connection>scm:git:https://github.com/dromara/sa-token.git</connection>\n\t\t<developerConnection>scm:git:https://github.com/dromara/sa-token.git</developerConnection>\n\t</scm>\n\t\n\t<!-- 作者信息 -->\n\t<developers>\n\t\t<developer>\n\t\t\t<name>click33</name>\n\t\t\t<email>2393584716@qq.com</email>\n\t\t</developer>\n\t</developers>\n\t\n\t<!-- 仓库依赖 -->\n\t<dependencies>\n\t\t\n\t</dependencies>\n\t\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<!--\n\t\t\t \t导入 sa-token-dependencies 所有版本定义，并传导到每个子项目。\n\t\t\t \t需要注意的是：该 import 只会导入 <dependencyManagement> 部分，而不会导入 <dependencies> 部分和 <properties> 部分。\n\t\t\t -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-dependencies</artifactId>\n                <version>${project.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n\t\t\t</dependency>\n\t\t\t\n\t\t</dependencies>\n\t</dependencyManagement>\n\t\n\t<!-- 项目构建 -->\n\t<build>\n\t\t<plugins>\n\n\t\t\t<!-- Source -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-source-plugin</artifactId>\n\t\t\t\t<version>3.4.0</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<attach>true</attach>\n\t\t\t\t</configuration>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<phase>compile</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>jar</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\n\t\t\t<!-- 源码编译 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-compiler-plugin</artifactId>\n\t\t\t\t<version>3.15.0</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<source>1.8</source>\n\t\t\t\t\t<target>1.8</target>\n\t\t\t\t\t<encoding>UTF-8</encoding>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\n\t\t\t<!-- API 文档 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-javadoc-plugin</artifactId>\n\t\t\t\t<version>3.12.0</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<!-- 统一生成聚合文档，解决 mvn package 时控制台发出 javadoc 警告的问题 -->\n\t\t\t\t\t<!-- <aggregate>true</aggregate> -->\n\t\t\t\t\t<!-- 忽略部分 error 和 warning -->\n\t\t\t\t\t<failOnError>false</failOnError>\n\t\t\t\t\t<failOnWarnings>false</failOnWarnings>\n\t\t\t\t\t<additionalOptions>-Xdoclint:none</additionalOptions>\n\t\t\t\t\t<detectLinks>false</detectLinks>\n\t\t\t\t</configuration>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<phase>package</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>aggregate</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t</execution>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<id>attach-javadocs</id>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>jar</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t<configuration>\n\t\t\t\t\t\t\t<doclint>none</doclint>\n\t\t\t\t\t\t</configuration>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\n\t\t\t<!-- flatten 统一版本号管理 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.codehaus.mojo</groupId>\n\t\t\t\t<artifactId>flatten-maven-plugin</artifactId>\n\t\t\t\t<version>1.7.3</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<updatePomFile>true</updatePomFile>\n\t\t\t\t\t<flattenMode>resolveCiFriendliesOnly</flattenMode>\n\t\t\t\t</configuration>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<id>flatten</id>\n\t\t\t\t\t\t<phase>process-resources</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>flatten</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t</execution>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<id>flatten.clean</id>\n\t\t\t\t\t\t<phase>clean</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>clean</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\n\t\t\t<!-- gpg 签名 -->\n\t\t   \t<plugin>\n\t\t\t   <groupId>org.apache.maven.plugins</groupId>\n\t\t\t   <artifactId>maven-gpg-plugin</artifactId>\n\t\t\t   <version>${maven-gpg-plugin.version}</version>\n\t\t\t   <executions>\n\t\t\t\t   <execution>\n\t\t\t\t\t   <id>sign-artifacts</id>\n\t\t\t\t\t   <phase>verify</phase>\n\t\t\t\t\t   <goals>\n\t\t\t\t\t\t   <goal>sign</goal>\n\t\t\t\t\t   </goals>\n\t\t\t\t   </execution>\n\t\t\t   </executions>\n\t\t   \t</plugin>\n\n\t\t\t<!-- 新版 Central Portal 中央仓库上传   -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.sonatype.central</groupId>\n\t\t\t\t<artifactId>central-publishing-maven-plugin</artifactId>\n\t\t\t\t<version>${central.publishing.maven.version}</version>\n\t\t\t\t<extensions>true</extensions>\n\t\t\t\t<configuration>\n\t\t\t\t\t<!-- 必须与 settings.xml 中 server 的 id 一致 -->\n\t\t\t\t\t<publishingServerId>central</publishingServerId>\n\t\t\t\t\t<!-- 是否自动发布。设为 true 后，上传完成无需手动点击发布 -->\n\t\t\t\t\t<!-- <autoPublish>true</autoPublish> -->\n\t\t\t\t\t<!-- 等待直到发布完成，让构建过程等待最终结果 -->\n\t\t\t\t\t<!-- <waitUntil>published</waitUntil> -->\n\t\t\t\t</configuration>\n\t\t   \t</plugin>\n\n\t   \t</plugins>\n\n\t   \t<pluginManagement>\n\t\t   \t<plugins>\n\t\t\t\t<plugin>\n\t\t\t\t\t<groupId>org.eclipse.m2e</groupId>\n\t\t\t\t\t<artifactId>lifecycle-mapping</artifactId>\n\t\t\t\t\t<version>1.0.0</version>\n\t\t\t\t\t<configuration>\n\t\t\t\t\t\t<lifecycleMappingMetadata>\n\t\t\t\t\t\t\t<pluginExecutions>\n\t\t\t\t\t\t\t\t<pluginExecution>\n\t\t\t\t\t\t\t\t\t<pluginExecutionFilter>\n\t\t\t\t\t\t\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t\t\t\t\t\t\t<artifactId>maven-enforcer-plugin</artifactId>\n\t\t\t\t\t\t\t\t\t\t<versionRange>[1.0.0,)</versionRange>\n\t\t\t\t\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t\t\t\t\t<goal>enforce</goal>\n\t\t\t\t\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t\t\t\t</pluginExecutionFilter>\n\t\t\t\t\t\t\t\t\t<action>\n\t\t\t\t\t\t\t\t\t\t<ignore />\n\t\t\t\t\t\t\t\t\t</action>\n\t\t\t\t\t\t\t\t</pluginExecution>\n\t\t\t\t\t\t\t</pluginExecutions>\n\t\t\t\t\t\t</lifecycleMappingMetadata>\n\t\t\t\t\t</configuration>\n\t\t\t\t</plugin>\n\t\t\t</plugins>\n\t   \t</pluginManagement>\n\n\t</build>\n\t\n</project>\n"
  },
  {
    "path": "preview-doc.bat",
    "content": ":: 运行前需要安装 browser-sync: \n:: npm install -g browser-sync\n\ncd sa-token-doc & browser-sync start --server --files \"\"\n"
  },
  {
    "path": "sa-token-bom/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-bom</artifactId>\n    <version>${revision}</version>\n    <packaging>pom</packaging>\n    <name>sa-token-bom</name>\n    <description>Sa-Token Bom</description>\n    <url>https://github.com/dromara/sa-token</url>\n\n    <properties>\n        <revision>1.45.0</revision>\n\n        <!-- Maven GPG Plugin & Maven Central Portal -->\n        <maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>\n        <central.publishing.maven.version>0.10.0</central.publishing.maven.version>\n    </properties>\n\n    <!-- 开源协议 apache 2.0 -->\n    <licenses>\n        <license>\n            <name>Apache 2</name>\n            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\n            <distribution>repo</distribution>\n            <comments>A business-friendly OSS license</comments>\n        </license>\n    </licenses>\n\n    <!-- 仓库信息 -->\n    <scm>\n        <tag>master</tag>\n        <url>https://github.com/dromara/sa-token.git</url>\n        <connection>scm:git:https://github.com/dromara/sa-token.git</connection>\n        <developerConnection>scm:git:https://github.com/dromara/sa-token.git</developerConnection>\n    </scm>\n\n    <!-- 作者信息 -->\n    <developers>\n        <developer>\n            <name>click33</name>\n            <email>2393584716@qq.com</email>\n        </developer>\n    </developers>\n\n    <dependencyManagement>\n        <dependencies>\n            <!-- sa-token 核心 -->\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-core</artifactId>\n                <version>${revision}</version>\n            </dependency>\n\n            <!-- region sa-token-starter -->\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-jboot-plugin</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-jfinal-plugin</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-reactor-spring-boot4-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-servlet</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-jakarta-servlet</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-solon-plugin</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-boot-reactor-v2v3v4-common</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-boot-webmvc-v3v4-common</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-boot-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-boot3-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-boot4-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <!-- endregion-->\n\n            <!-- region sa-token-plugin -->\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-plugin</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-alone-redis</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-alone-redis-by-spring-boot4</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-dubbo</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-dubbo3</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-grpc</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-redis-template</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-jackson</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-jackson3</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-fastjson</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-fastjson2</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-snack3</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-redis-jackson</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-forest</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-okhttps</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-redisson-spring-boot-starter</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-redisson</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-redisx</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-hutool-timed-cache</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-thymeleaf</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-freemarker</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-jwt</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-oauth2</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-apikey</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-sign</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-quick-login</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-aop</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-el</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-sso</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-temp-jwt</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-redis-template-jdk-serializer</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-serializer-features</artifactId>\n                <version>${revision}</version>\n            </dependency>\n            <!-- endregion-->\n\n        </dependencies>\n    </dependencyManagement>\n\n    <!-- 项目构建 -->\n    <build>\n        <plugins>\n\n            <!-- 源码编译 -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.15.0</version>\n                <configuration>\n                    <source>1.8</source>\n                    <target>1.8</target>\n                    <encoding>UTF-8</encoding>\n                </configuration>\n            </plugin>\n\n            <!-- flatten 统一版本号管理 -->\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>flatten-maven-plugin</artifactId>\n                <version>1.7.3</version>\n                <configuration>\n                    <updatePomFile>true</updatePomFile>\n                    <flattenMode>resolveCiFriendliesOnly</flattenMode>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>flatten</id>\n                        <phase>process-resources</phase>\n                        <goals>\n                            <goal>flatten</goal>\n                        </goals>\n                    </execution>\n                    <execution>\n                        <id>flatten.clean</id>\n                        <phase>clean</phase>\n                        <goals>\n                            <goal>clean</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <!-- gpg 签名 -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-gpg-plugin</artifactId>\n                <version>${maven-gpg-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <id>sign-artifacts</id>\n                        <phase>verify</phase>\n                        <goals>\n                            <goal>sign</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <!-- 新版 Central Portal 中央仓库上传   -->\n            <plugin>\n                <groupId>org.sonatype.central</groupId>\n                <artifactId>central-publishing-maven-plugin</artifactId>\n                <version>${central.publishing.maven.version}</version>\n                <extensions>true</extensions>\n                <configuration>\n                    <!-- 必须与 settings.xml 中 server 的 id 一致 -->\n                    <publishingServerId>central</publishingServerId>\n                    <!-- 是否自动发布。设为 true 后，上传完成无需手动点击发布 -->\n                    <!-- <autoPublish>true</autoPublish> -->\n                    <!-- 等待直到发布完成，让构建过程等待最终结果 -->\n                    <!-- <waitUntil>published</waitUntil> -->\n                </configuration>\n            </plugin>\n\n        </plugins>\n\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.eclipse.m2e</groupId>\n                    <artifactId>lifecycle-mapping</artifactId>\n                    <version>1.0.0</version>\n                    <configuration>\n                        <lifecycleMappingMetadata>\n                            <pluginExecutions>\n                                <pluginExecution>\n                                    <pluginExecutionFilter>\n                                        <groupId>org.apache.maven.plugins</groupId>\n                                        <artifactId>maven-enforcer-plugin</artifactId>\n                                        <versionRange>[1.0.0,)</versionRange>\n                                        <goals>\n                                            <goal>enforce</goal>\n                                        </goals>\n                                    </pluginExecutionFilter>\n                                    <action>\n                                        <ignore />\n                                    </action>\n                                </pluginExecution>\n                            </pluginExecutions>\n                        </lifecycleMappingMetadata>\n                    </configuration>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n    </build>\n\n</project>\n"
  },
  {
    "path": "sa-token-core/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-parent</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-core</name>\n    <artifactId>sa-token-core</artifactId>\n\t<description>An open-source, free, and one-stop Java authentication framework that makes authentication simple and elegant!</description>\n\n\t<dependencies>\n\t\t<!-- Zero Dependence -->\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken;\n\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.config.SaTokenConfigFactory;\nimport cn.dev33.satoken.context.SaTokenContext;\nimport cn.dev33.satoken.context.SaTokenContextForThreadLocal;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoDefaultImpl;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.http.SaHttpTemplate;\nimport cn.dev33.satoken.http.SaHttpTemplateDefaultImpl;\nimport cn.dev33.satoken.json.SaJsonTemplate;\nimport cn.dev33.satoken.json.SaJsonTemplateDefaultImpl;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.log.SaLogForConsole;\nimport cn.dev33.satoken.same.SaSameTemplate;\nimport cn.dev33.satoken.secure.totp.SaTotpTemplate;\nimport cn.dev33.satoken.serializer.SaSerializerTemplate;\nimport cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJson;\nimport cn.dev33.satoken.stp.StpInterface;\nimport cn.dev33.satoken.stp.StpInterfaceDefaultImpl;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.temp.SaTempTemplate;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 管理 Sa-Token 所有全局组件，可通过此类快速获取、写入各种全局组件对象\n *\n * @author click33\n * @since 1.18.0\n */\npublic class SaManager {\n\n\t/**\n\t * 全局配置对象\n\t */\n\tpublic volatile static SaTokenConfig config;\t\n\tpublic static void setConfig(SaTokenConfig config) {\n\t\tsetConfigMethod(config);\n\t\t\n\t\t// 打印 banner \n\t\tif(config !=null && config.getIsPrint()) {\n\t\t\tSaFoxUtil.printSaToken();\n\t\t}\n\n\t\t// 如果此 config 对象没有配置 isColorLog 的值，则框架为它自动判断一下\n\t\tif(config != null && config.getIsLog() != null && config.getIsLog() && config.getIsColorLog() == null) {\n\t\t\tconfig.setIsColorLog(SaFoxUtil.isCanColorLog());\n\t\t}\n\n\t\t// $$ 全局事件 \n\t\tSaTokenEventCenter.doSetConfig(config);\n\t\t\n\t\t// 调用一次 StpUtil 中的方法，保证其可以尽早的初始化 StpLogic\n\t\tStpUtil.getLoginType();\n\t}\n\tprivate static void setConfigMethod(SaTokenConfig config) {\n\t\tSaManager.config = config;\n\t}\n\n\t/**\n\t * 获取 Sa-Token 的全局配置信息\n\t * @return 全局配置信息\n\t */\n\tpublic static SaTokenConfig getConfig() {\n\t\tif (config == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (config == null) {\n\t\t\t\t\tsetConfigMethod(SaTokenConfigFactory.createConfig());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn config;\n\t}\n\t\n\t/**\n\t * 持久化组件\n\t */\n\tprivate volatile static SaTokenDao saTokenDao;\n\tpublic static void setSaTokenDao(SaTokenDao saTokenDao) {\n\t\tsetSaTokenDaoMethod(saTokenDao);\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaTokenDao\", saTokenDao);\n\t}\n\tprivate static void setSaTokenDaoMethod(SaTokenDao saTokenDao) {\n\t\tif (SaManager.saTokenDao != null) {\n\t\t\tSaManager.saTokenDao.destroy();\n\t\t}\n\t\tSaManager.saTokenDao = saTokenDao;\n\t\tif (SaManager.saTokenDao != null) {\n\t\t\tSaManager.saTokenDao.init();\n\t\t}\n\t}\n\tpublic static SaTokenDao getSaTokenDao() {\n\t\tif (saTokenDao == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saTokenDao == null) {\n\t\t\t\t\tsetSaTokenDaoMethod(new SaTokenDaoDefaultImpl());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saTokenDao;\n\t}\n\t\n\t/**\n\t * 权限数据源组件\n\t */\n\tprivate volatile static StpInterface stpInterface;\n\tpublic static void setStpInterface(StpInterface stpInterface) {\n\t\tSaManager.stpInterface = stpInterface;\n\t\tSaTokenEventCenter.doRegisterComponent(\"StpInterface\", stpInterface);\n\t}\n\tpublic static StpInterface getStpInterface() {\n\t\tif (stpInterface == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (stpInterface == null) {\n\t\t\t\t\tSaManager.stpInterface = new StpInterfaceDefaultImpl();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn stpInterface;\n\t}\n\t\n\t/**\n\t * 上下文 SaTokenContext\n\t */\n\tprivate volatile static SaTokenContext saTokenContext;\n\tpublic static void setSaTokenContext(SaTokenContext saTokenContext) {\n\t\tSaManager.saTokenContext = saTokenContext;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaTokenContext\", saTokenContext);\n\t}\n\tpublic static SaTokenContext getSaTokenContext() {\n\t\tif (saTokenContext == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saTokenContext == null) {\n\t\t\t\t\tSaManager.saTokenContext = new SaTokenContextForThreadLocal();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saTokenContext;\n\t}\n\n\t/**\n\t * 临时 token 认证模块\n\t */\n\tprivate volatile static SaTempTemplate saTempTemplate;\n\tpublic static void setSaTempTemplate(SaTempTemplate saTempTemplate) {\n\t\tSaManager.saTempTemplate = saTempTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaTempTemplate\", saTempTemplate);\n\t}\n\tpublic static SaTempTemplate getSaTempTemplate() {\n\t\tif (saTempTemplate == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saTempTemplate == null) {\n\t\t\t\t\tSaManager.saTempTemplate = new SaTempTemplate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saTempTemplate;\n\t}\n\n\t/**\n\t * JSON 转换器\n\t */\n\tprivate volatile static SaJsonTemplate saJsonTemplate;\n\tpublic static void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) {\n\t\tSaManager.saJsonTemplate = saJsonTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaJsonTemplate\", saJsonTemplate);\n\t}\n\tpublic static SaJsonTemplate getSaJsonTemplate() {\n\t\tif (saJsonTemplate == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saJsonTemplate == null) {\n\t\t\t\t\tSaManager.saJsonTemplate = new SaJsonTemplateDefaultImpl();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saJsonTemplate;\n\t}\n\n\t/**\n\t * HTTP 转换器\n\t */\n\tprivate volatile static SaHttpTemplate saHttpTemplate;\n\tpublic static void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) {\n\t\tSaManager.saHttpTemplate = saHttpTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaHttpTemplate\", saHttpTemplate);\n\t}\n\tpublic static SaHttpTemplate getSaHttpTemplate() {\n\t\tif (saHttpTemplate == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saHttpTemplate == null) {\n\t\t\t\t\tSaManager.saHttpTemplate = new SaHttpTemplateDefaultImpl();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saHttpTemplate;\n\t}\n\n\t/**\n\t * 序列化器\n\t */\n\tprivate volatile static SaSerializerTemplate saSerializerTemplate;\n\tpublic static void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) {\n\t\tSaManager.saSerializerTemplate = saSerializerTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaSerializerTemplate\", saSerializerTemplate);\n\t}\n\tpublic static SaSerializerTemplate getSaSerializerTemplate() {\n\t\tif (saSerializerTemplate == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saSerializerTemplate == null) {\n\t\t\t\t\tSaManager.saSerializerTemplate = new SaSerializerTemplateForJson();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saSerializerTemplate;\n\t}\n\n\t/**\n\t * Same-Token 同源系统认证模块\n\t */\n\tprivate volatile static SaSameTemplate saSameTemplate;\n\tpublic static void setSaSameTemplate(SaSameTemplate saSameTemplate) {\n\t\tSaManager.saSameTemplate = saSameTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaSameTemplate\", saSameTemplate);\n\t}\n\tpublic static SaSameTemplate getSaSameTemplate() {\n\t\tif (saSameTemplate == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saSameTemplate == null) {\n\t\t\t\t\tSaManager.saSameTemplate = new SaSameTemplate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saSameTemplate;\n\t}\n\n\t/**\n\t * 日志输出器 \n\t */\n\tpublic volatile static SaLog log = new SaLogForConsole();\n\tpublic static void setLog(SaLog log) {\n\t\tSaManager.log = log;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaLog\", log);\n\t}\n\tpublic static SaLog getLog() {\n\t\treturn SaManager.log;\n\t}\n\n\t/**\n\t * TOTP 算法类，支持 生成/验证 动态一次性密码\n\t */\n\tprivate volatile static SaTotpTemplate totpTemplate;\n\tpublic static void setSaTotpTemplate(SaTotpTemplate totpTemplate) {\n\t\tSaManager.totpTemplate = totpTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaTotpTemplate\", totpTemplate);\n\t}\n\tpublic static SaTotpTemplate getSaTotpTemplate() {\n\t\tif (totpTemplate == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (totpTemplate == null) {\n\t\t\t\t\tSaManager.totpTemplate = new SaTotpTemplate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn totpTemplate;\n\t}\n\n\n\t// ------------------- StpLogic 相关 -------------------\n\n\t/**\n\t * StpLogic 集合, 记录框架所有成功初始化的 StpLogic\n\t */\n\tpublic static Map<String, StpLogic> stpLogicMap = new LinkedHashMap<>();\n\t\n\t/**\n\t * 向全局集合中 put 一个 StpLogic \n\t * @param stpLogic StpLogic\n\t */\n\tpublic static void putStpLogic(StpLogic stpLogic) {\n\t\tstpLogicMap.put(stpLogic.getLoginType(), stpLogic);\n\t}\n\n\t/**\n\t * 在全局集合中 移除 一个 StpLogic\n\t */\n\tpublic static void removeStpLogic(String loginType) {\n\t\tstpLogicMap.remove(loginType);\n\t}\n\n\t/**\n\t * 根据 LoginType 获取对应的StpLogic，如果不存在则新建并返回 \n\t * @param loginType 对应的账号类型 \n\t * @return 对应的StpLogic\n\t */\n\tpublic static StpLogic getStpLogic(String loginType) {\n\t\treturn getStpLogic(loginType, true);\n\t}\n\t\n\t/**\n\t * 根据 LoginType 获取对应的StpLogic，如果不存在，isCreate = 是否自动创建并返回\n\t * @param loginType 对应的账号类型 \n\t * @param isCreate 在 StpLogic 不存在时，true=新建并返回，false=抛出异常\n\t * @return 对应的StpLogic\n\t */\n\tpublic static StpLogic getStpLogic(String loginType, boolean isCreate) {\n\t\t// 如果type为空则返回框架默认内置的 \n\t\tif(loginType == null || loginType.isEmpty()) {\n\t\t\treturn StpUtil.stpLogic;\n\t\t}\n\t\t\n\t\t// 从集合中获取 \n\t\tStpLogic stpLogic = stpLogicMap.get(loginType);\n\t\tif(stpLogic == null) {\n\t\t\t\n\t\t\t// isCreate=true时，自创建模式：自动创建并返回 \n\t\t\tif(isCreate) {\n\t\t\t\tsynchronized (SaManager.class) {\n\t\t\t\t\tstpLogic = stpLogicMap.get(loginType);\n\t\t\t\t\tif(stpLogic == null) {\n\t\t\t\t\t\tstpLogic = SaStrategy.instance.createStpLogic.apply(loginType);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} \n\t\t\t// isCreate=false时，严格校验模式：抛出异常 \n\t\t\telse {\n\t\t\t\t/*\n\t\t\t\t * 此时有两种情况会造成 StpLogic == null \n\t\t\t\t * 1. loginType拼写错误，请改正 （建议使用常量） \n\t\t\t\t * 2. 自定义StpUtil尚未初始化（静态类中的属性至少一次调用后才会初始化），解决方法两种\n\t\t\t\t * \t\t(1) 从main方法里调用一次\n\t\t\t\t * \t\t(2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 \n\t\t\t\t */\n\t\t\t\tthrow new SaTokenException(\"未能获取对应StpLogic，type=\"+ loginType).setCode(SaErrorCode.CODE_10002);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 返回 \n\t\treturn stpLogic;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckDisable.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 服务禁用校验：判断当前账号是否被禁用了指定服务，如果被禁用，会抛出异常，没有被禁用才能进入方法。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author videomonster\n * @since 1.31.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaCheckDisable {\n\n    /**\n     * 多账号体系下所属的账号体系标识，非多账号体系无需关注此值\n     *\n     * @return /\n     */\n    String type() default \"\";\n    \n    /**\n     * 服务标识 （具体你要校验是否禁用的服务名称）\n     * \n     * @return /\n     */\n    String[] value() default { SaTokenConsts.DEFAULT_DISABLE_SERVICE };\n\n    /**\n     * 封禁等级（如果当前账号的被封禁等级 ≥ 此值，请求就无法进入方法）\n     * \n     * @return / \n     */\n    int level() default SaTokenConsts.DEFAULT_DISABLE_LEVEL;\n    \n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpBasic.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Http Basic 认证校验：只有通过 Http Basic 认证后才能进入该方法，否则抛出异常。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.26.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaCheckHttpBasic {\n\n    /**\n     * 领域 \n     * @return /\n     */\n    String realm() default SaHttpBasicTemplate.DEFAULT_REALM;\n\n    /**\n     * 需要校验的账号密码，格式形如 sa:123456 \n     * @return /\n     */\n    String account() default \"\";\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestModel;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Http Digest 认证校验：只有通过 Http Digest 认证后才能进入该方法，否则抛出异常。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.38.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaCheckHttpDigest {\n\n    /**\n     * 用户名\n     * @return /\n     */\n    String username() default \"\";\n\n    /**\n     * 密码\n     * @return /\n     */\n    String password() default \"\";\n\n    /**\n     * 领域 \n     * @return /\n     */\n    String realm() default SaHttpDigestModel.DEFAULT_REALM;\n\n    /**\n     * 需要校验的用户名和密码，格式形如 sa:123456\n     * @return /\n     */\n    String value() default \"\";\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckLogin.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 登录认证校验：只有登录之后才能进入该方法。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.10.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaCheckLogin {\n\n    /**\n     * 多账号体系下所属的账号体系标识，非多账号体系无需关注此值\n     *\n     * @return /\n     */\n    String type() default \"\";\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckOr.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport java.lang.annotation.*;\n\n/**\n * 批量注解鉴权：只要满足其中一个注解即可通过验证\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.35.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaCheckOr {\n\n    /**\n     * 设定 @SaCheckLogin，参考 {@link SaCheckLogin}\n     *\n     * @return /\n     */\n    SaCheckLogin[] login() default {};\n\n    /**\n     * 设定 @SaCheckRole，参考 {@link SaCheckRole}\n     *\n     * @return /\n     */\n    SaCheckRole[] role() default {};\n\n    /**\n     * 设定 @SaCheckPermission，参考 {@link SaCheckPermission}\n     *\n     * @return /\n     */\n    SaCheckPermission[] permission() default {};\n\n    /**\n     * 设定 @SaCheckSafe，参考 {@link SaCheckSafe}\n     *\n     * @return /\n     */\n    SaCheckSafe[] safe() default {};\n\n    /**\n     * 设定 @SaCheckHttpBasic，参考 {@link SaCheckHttpBasic}\n     *\n     * @return /\n     */\n    SaCheckHttpBasic[] httpBasic() default {};\n\n    /**\n     * 设定 @SaCheckBasic，参考 {@link SaCheckHttpDigest}\n     *\n     * @return /\n     */\n    SaCheckHttpDigest[] httpDigest() default {};\n\n    /**\n     * 设定 @SaCheckDisable，参考 {@link SaCheckDisable}\n     *\n     * @return /\n     */\n    SaCheckDisable[] disable() default {};\n\n    /**\n     * 需要追加抓取的注解 Class (只能填写 Sa-Token 相关注解类型)\n     *\n     * @return /\n     */\n    Class<? extends Annotation>[] append() default {};\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckPermission.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 权限认证校验：必须具有指定权限才能进入该方法。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.10.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface SaCheckPermission {\n\n\t/**\n\t * 多账号体系下所属的账号体系标识，非多账号体系无需关注此值\n\t *\n\t * @return /\n\t */\n\tString type() default \"\";\n\n\t/**\n\t * 需要校验的权限码 [ 数组 ]\n\t *\n\t * @return /\n\t */\n\tString [] value() default {};\n\n\t/**\n\t * 验证模式：AND | OR，默认AND\n\t *\n\t * @return /\n\t */\n\tSaMode mode() default SaMode.AND;\n\n\t/**\n\t * 在权限校验不通过时的次要选择，两者只要其一校验成功即可通过校验\n\t * \n\t * <p> \n\t * \t例1：@SaCheckPermission(value=\"user-add\", orRole=\"admin\")，\n\t * \t代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。\n\t * </p>\n\t * \n\t * <p> \n\t * \t例2： orRole = {\"admin\", \"manager\", \"staff\"}，具有三个角色其一即可。 <br>\n\t * \t例3： orRole = {\"admin, manager, staff\"}，必须三个角色同时具备。\n\t * </p>\n\t * \n\t * @return /\n\t */\n\tString[] orRole() default {};\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckRole.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 角色认证校验：必须具有指定角色标识才能进入该方法。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.10.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface SaCheckRole {\n\n\t/**\n\t * 多账号体系下所属的账号体系标识，非多账号体系无需关注此值\n\t *\n\t * @return /\n\t */\n\tString type() default \"\";\n\n\t/**\n\t * 需要校验的角色标识 [ 数组 ]\n\t *\n\t * @return /\n\t */\n\tString [] value() default {};\n\n\t/**\n\t * 验证模式：AND | OR，默认AND\n\t *\n\t * @return /\n\t */\n\tSaMode mode() default SaMode.AND;\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckSafe.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\n\n/**\n * 二级认证校验：客户端必须完成二级认证之后，才能进入该方法，否则将被抛出异常。\n * \n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）。\n *\n * @author click33\n * @since 1.21.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaCheckSafe {\n\n\t/**\n\t * 多账号体系下所属的账号体系标识，非多账号体系无需关注此值\n\t *\n\t * @return /\n\t */\n\tString type() default \"\";\n\n\t/**\n\t * 要校验的服务\n\t *\n\t * @return /\n\t */\n\tString value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE;\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaIgnore.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 忽略认证：表示被修饰的方法或类无需进行注解认证和路由拦截认证。\n * \n * <h3> 请注意：此注解的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效，对自定义拦截器与过滤器不生效。 </h3>\n * \n * @author click33\n * @since 1.31.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaIgnore {\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaMode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\n/**\n * 注解鉴权的验证模式\n *\n * @author click33\n * @since 1.10.0\n */\npublic enum SaMode {\n\n\t/**\n\t * 必须具有所有的元素 \n\t */\n\tAND,\n\n\t/**\n\t * 只需具有其中一个元素\n\t */\n\tOR\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaAnnotationHandlerInterface.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 所有注解处理器的父接口\n *\n * @author click33\n * @since 2024/8/2\n */\npublic interface SaAnnotationHandlerInterface<T extends Annotation> {\n\n    /**\n     * 获取所要处理的注解类型\n     * @return /\n     */\n    Class<T> getHandlerAnnotationClass();\n\n    /**\n     * 所需要执行的校验方法\n     * @param at 注解对象\n     * @param element 被标注的注解的元素(方法/类)引用\n     */\n    @SuppressWarnings(\"unchecked\")\n    default void check(Annotation at, AnnotatedElement element) {\n        checkMethod((T) at, element);\n    }\n\n    /**\n     * 所需要执行的校验方法（转换类型后）\n     * @param at 注解对象\n     * @param element 被标注的注解的元素(方法/类)引用\n     */\n    void checkMethod(T at, AnnotatedElement element);\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckDisableHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.SaCheckDisable;\nimport cn.dev33.satoken.stp.StpLogic;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckDisable 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckDisableHandler implements SaAnnotationHandlerInterface<SaCheckDisable> {\n\n    @Override\n    public Class<SaCheckDisable> getHandlerAnnotationClass() {\n        return SaCheckDisable.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckDisable at, AnnotatedElement element) {\n        _checkMethod(at.type(), at.value(), at.level());\n    }\n\n    public static void _checkMethod(String type, String[] value, int level) {\n        StpLogic stpLogic = SaManager.getStpLogic(type, false);\n\n        Object loginId = stpLogic.getLoginId();\n        for (String service : value) {\n            stpLogic.checkDisableLevel(loginId, service, level);\n        }\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpBasicHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpBasic;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckHttpBasic 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckHttpBasicHandler implements SaAnnotationHandlerInterface<SaCheckHttpBasic> {\n\n    @Override\n    public Class<SaCheckHttpBasic> getHandlerAnnotationClass() {\n        return SaCheckHttpBasic.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckHttpBasic at, AnnotatedElement element) {\n        _checkMethod(at.realm(), at.account());\n    }\n\n    public static void _checkMethod(String realm, String account) {\n        SaHttpBasicUtil.check(realm, account);\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckHttpDigestHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpDigest;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckHttpDigest 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckHttpDigestHandler implements SaAnnotationHandlerInterface<SaCheckHttpDigest> {\n\n    @Override\n    public Class<SaCheckHttpDigest> getHandlerAnnotationClass() {\n        return SaCheckHttpDigest.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckHttpDigest at, AnnotatedElement element) {\n        _checkMethod(at.username(), at.password(), at.realm(), at.value());\n    }\n\n    public static void _checkMethod(String username, String password, String realm, String value) {\n        // 如果配置了 value，则以 value 优先\n        if(SaFoxUtil.isNotEmpty(value)){\n            String[] arr = value.split(\":\");\n            if(arr.length != 2){\n                throw new SaTokenException(\"注解参数配置错误，格式应如：username:password\");\n            }\n            SaHttpDigestUtil.check(arr[0], arr[1]);\n            return;\n        }\n\n        // 如果配置了 username，则分别获取参数\n        if(SaFoxUtil.isNotEmpty(username)){\n            SaHttpDigestUtil.check(username, password, realm);\n            return;\n        }\n\n        // 都没有配置，则根据全局配置参数进行校验\n        SaHttpDigestUtil.check();\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckLoginHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.stp.StpLogic;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckLogin 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckLoginHandler implements SaAnnotationHandlerInterface<SaCheckLogin> {\n\n    @Override\n    public Class<SaCheckLogin> getHandlerAnnotationClass() {\n        return SaCheckLogin.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckLogin at, AnnotatedElement element) {\n        _checkMethod(at.type());\n    }\n\n    public static void _checkMethod(String type) {\n        StpLogic stpLogic = SaManager.getStpLogic(type, false);\n\n        stpLogic.checkLogin();\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckOrHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.annotation.*;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 注解 SaCheckOr 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckOrHandler implements SaAnnotationHandlerInterface<SaCheckOr> {\n\n    @Override\n    public Class<SaCheckOr> getHandlerAnnotationClass() {\n        return SaCheckOr.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckOr at, AnnotatedElement element) {\n        _checkMethod(at.login(), at.role(), at.permission(), at.safe(), at.httpBasic(), at.httpDigest(), at.disable(), at.append(), element);\n    }\n\n    public static void _checkMethod(\n            SaCheckLogin[] login,\n            SaCheckRole[] role,\n            SaCheckPermission[] permission,\n            SaCheckSafe[] safe,\n            SaCheckHttpBasic[] httpBasic,\n            SaCheckHttpDigest[] httpDigest,\n            SaCheckDisable[] disable,\n            Class<? extends Annotation>[] append,\n            AnnotatedElement element\n    ) {\n        // 先把所有注解塞到一个 list 里\n        List<Annotation> annotationList = new ArrayList<>();\n        annotationList.addAll(Arrays.asList(login));\n        annotationList.addAll(Arrays.asList(role));\n        annotationList.addAll(Arrays.asList(permission));\n        annotationList.addAll(Arrays.asList(safe));\n        annotationList.addAll(Arrays.asList(disable));\n        annotationList.addAll(Arrays.asList(httpBasic));\n        annotationList.addAll(Arrays.asList(httpDigest));\n        for (Class<? extends Annotation> annotationClass : append) {\n            Annotation annotation = SaAnnotationStrategy.instance.getAnnotation.apply(element, annotationClass);\n            if(annotation != null) {\n                annotationList.add(annotation);\n            }\n        }\n\n        // 如果 atList 为空，说明 SaCheckOr 上不包含任何注解校验，我们直接跳过即可\n        if(annotationList.isEmpty()) {\n            return;\n        }\n\n        // 逐个开始校验 >>>\n        List<SaTokenException> errorList = new ArrayList<>();\n        for (Annotation item : annotationList) {\n            try {\n                SaAnnotationStrategy.instance.annotationHandlerMap.get(item.annotationType()).check(item, element);\n                // 只要有一个校验通过，就可以直接返回了\n                return;\n            } catch (SaTokenException e) {\n                errorList.add(e);\n            }\n        }\n\n        // 执行至此，说明所有注解校验都通过不了，此时 errorList 里面会有多个异常，我们随便抛出一个即可\n        throw errorList.get(0);\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckPermissionHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckPermission 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckPermissionHandler implements SaAnnotationHandlerInterface<SaCheckPermission> {\n\n    @Override\n    public Class<SaCheckPermission> getHandlerAnnotationClass() {\n        return SaCheckPermission.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckPermission at, AnnotatedElement element) {\n        _checkMethod(at.type(), at.value(), at.mode(), at.orRole());\n    }\n\n    public static void _checkMethod(String type, String[] value, SaMode mode, String[] orRole) {\n        StpLogic stpLogic = SaManager.getStpLogic(type, false);\n\n        String[] permissionArray = value;\n        try {\n            if(mode == SaMode.AND) {\n                stpLogic.checkPermissionAnd(permissionArray);\n            } else {\n                stpLogic.checkPermissionOr(permissionArray);\n            }\n        } catch (NotPermissionException e) {\n            // 权限认证校验未通过，再开始角色认证校验\n            for (String role : orRole) {\n                String[] rArr = SaFoxUtil.convertStringToArray(role);\n                // 某一项 role 认证通过，则可以提前退出了，代表通过\n                if (stpLogic.hasRoleAnd(rArr)) {\n                    return;\n                }\n            }\n            throw e;\n        }\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckRoleHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.stp.StpLogic;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckRole 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckRoleHandler implements SaAnnotationHandlerInterface<SaCheckRole> {\n\n    @Override\n    public Class<SaCheckRole> getHandlerAnnotationClass() {\n        return SaCheckRole.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckRole at, AnnotatedElement element) {\n        _checkMethod(at.type(), at.value(), at.mode());\n    }\n\n    public static void _checkMethod(String type, String[] value, SaMode mode) {\n        StpLogic stpLogic = SaManager.getStpLogic(type, false);\n\n        String[] roleArray = value;\n        if(mode == SaMode.AND) {\n            stpLogic.checkRoleAnd(roleArray);\n        } else {\n            stpLogic.checkRoleOr(roleArray);\n        }\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaCheckSafeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.SaCheckSafe;\nimport cn.dev33.satoken.stp.StpLogic;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckSafe 的处理器\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaCheckSafeHandler implements SaAnnotationHandlerInterface<SaCheckSafe> {\n\n    @Override\n    public Class<SaCheckSafe> getHandlerAnnotationClass() {\n        return SaCheckSafe.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckSafe at, AnnotatedElement element) {\n        _checkMethod(at.type(), at.value());\n    }\n\n    public static void _checkMethod(String type, String value) {\n        StpLogic stpLogic = SaManager.getStpLogic(type, false);\n\n        stpLogic.checkSafe(value);\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/annotation/handler/SaIgnoreHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation.handler;\n\nimport cn.dev33.satoken.annotation.SaIgnore;\nimport cn.dev33.satoken.router.SaRouter;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaIgnore 的处理器\n * <h2> v1.43.0 版本起，SaIgnore 注解处理逻辑已转移到全局策略中，此处理器代码仅做留档 </h2>\n *\n * @author click33\n * @since 2024/8/2\n */\npublic class SaIgnoreHandler implements SaAnnotationHandlerInterface<SaIgnore> {\n\n    @Override\n    public Class<SaIgnore> getHandlerAnnotationClass() {\n        return SaIgnore.class;\n    }\n\n    @Override\n    public void checkMethod(SaIgnore at, AnnotatedElement element) {\n        _checkMethod();\n    }\n\n    public static void _checkMethod() {\n        SaRouter.stop();\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/application/ApplicationInfo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.application;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 应用全局信息\n *\n * @author click33\n * @since 1.31.0\n */\npublic class ApplicationInfo {\n\n    /**\n     * 应用前缀\n     */\n    public static String routePrefix;\n\n    /**\n     * 为指定 path 裁剪掉 routePrefix 前缀\n     * @param path 指定 path\n     * @return /\n     */\n    public static String cutPathPrefix(String path) {\n        if(! SaFoxUtil.isEmpty(routePrefix) && ! routePrefix.equals(\"/\") && path.startsWith(routePrefix)){\n            path = path.substring(routePrefix.length());\n        }\n        return path;\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/application/SaApplication.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.application;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\n\n/**\n * Application Model，全局作用域的读取值对象。\n *\n * <p> 在应用全局范围内: 存值、取值。数据在应用重启后失效，如果集成了 Redis，则在 Redis 重启后失效。\n * \n * @author click33\n * @since 1.31.0\n */\npublic class SaApplication implements SaSetValueInterface {\n\n\t/**\n\t * 默认实例 \n\t */\n\tpublic static SaApplication defaultInstance = new SaApplication();\n\n\t// ---- 实现接口存取值方法 \n\n\t/** 取值 */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn SaManager.getSaTokenDao().getObject(splicingDataKey(key));\n\t}\n\n\t/** 写值 */\n\t@Override\n\tpublic SaApplication set(String key, Object value) {\n\t\treturn set(key, value, SaTokenDao.NEVER_EXPIRE);\n\t}\n\n\t/** 删值 */\n\t@Override\n\tpublic SaApplication delete(String key) {\n\t\tSaManager.getSaTokenDao().deleteObject(splicingDataKey(key));\n\t\treturn this;\n\t}\n\n\t\n\t// ---- 其它方法 \n\n\t/**\n\t * 写值\n\t * @param key   名称\n\t * @param value 值\n\t * @param ttl 有效时间（单位：秒）\n\t * @return 对象自身\n\t */\n\tpublic SaApplication set(String key, Object value, long ttl) {\n\t\tSaManager.getSaTokenDao().setObject(splicingDataKey(key), value, ttl);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回当前存入的所有 key\n\t * @return / \n\t */\n\tpublic List<String> keys() {\n\t\t// 从缓存中查询出所有此前缀的 key\n\t\tString prefix = splicingDataKey(\"\");\n\t\tList<String> list = SaManager.getSaTokenDao().searchData(prefix, \"\", 0, -1, true);\n\t\t\n\t\t// 裁减掉固定前缀，保留 key 名称，塞入新集合\n\t\tint prefixLength = prefix.length();\n\t\tList<String> list2 = new ArrayList<>();\n\t\tif(list != null) {\n\t\t\tfor (String key : list) {\n\t\t\t\tlist2.add(key.substring(prefixLength));\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 返回 \n\t\treturn list2;\n\t}\n\t\n\t/**\n\t * 清空当前存入的所有 key\n\t */\n\tpublic void clear() {\n\t\tList<String> keys = keys();\n\t\tfor (String key : keys) {\n\t\t\tdelete(key);\n\t\t}\n\t}\n\n\t/**  \n\t * 拼接key：当存入一个变量时，应该使用的 key\n\t *\n\t * @param key 原始 key \n\t * @return 拼接后的 key 值 \n\t */\n\tpublic String splicingDataKey(String key) {\n\t\treturn SaManager.getConfig().getTokenName() + \":var:\" + key;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/application/SaGetValueInterface.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.application;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 对取值的一组方法封装\n * <p> 封装 SaStorage、SaSession、SaApplication 等存取值的一些固定方法，减少重复编码 </p>\n * \n * @author click33\n * @since 1.31.0\n */\npublic interface SaGetValueInterface {\n\n\t// --------- 需要子类实现的方法 \n\t\n\t/**\n\t * 取值 \n\t * @param key key \n\t * @return 值 \n\t */\n\tObject get(String key);\n\t\n\t\n\t// --------- 接口提供封装的方法 \n\n\t/**\n\t * 取值 (指定默认值)\n\t *\n\t * @param <T> 默认值的类型\n\t * @param key key \n\t * @param defaultValue 取不到值时返回的默认值 \n\t * @return 值 \n\t */\n\tdefault <T> T get(String key, T defaultValue) {\n\t\treturn getValueByDefaultValue(get(key), defaultValue);\n\t}\n\n\t/**\n\t * 取值 (转String类型) \n\t * @param key key \n\t * @return 值 \n\t */\n\tdefault String getString(String key) {\n\t\tObject value = get(key);\n\t\tif(value == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn String.valueOf(value);\n\t}\n\n\t/**\n\t * 取值 (转int类型) \n\t * @param key key \n\t * @return 值 \n\t */\n\tdefault int getInt(String key) {\n\t\treturn getValueByDefaultValue(get(key), 0);\n\t}\n\n\t/**\n\t * 取值 (转long类型) \n\t * @param key key \n\t * @return 值 \n\t */\n\tdefault long getLong(String key) {\n\t\treturn getValueByDefaultValue(get(key), 0L);\n\t}\n\n\t/**\n\t * 取值 (转double类型) \n\t * @param key key \n\t * @return 值 \n\t */\n\tdefault double getDouble(String key) {\n\t\treturn getValueByDefaultValue(get(key), 0.0);\n\t}\n\n\t/**\n\t * 取值 (转float类型) \n\t * @param key key \n\t * @return 值 \n\t */\n\tdefault float getFloat(String key) {\n\t\treturn getValueByDefaultValue(get(key), 0.0f);\n\t}\n\n\t/**\n\t * 取值 (指定转换类型)\n\t * @param <T> 泛型\n\t * @param key key \n\t * @param cs 指定转换类型 \n\t * @return 值 \n\t */\n\tdefault <T> T getModel(String key, Class<T> cs) {\n\t\treturn SaFoxUtil.getValueByType(get(key), cs);\n\t}\n\n\t/**\n\t * 取值 (指定转换类型, 并指定值为 null 时返回的默认值)\n\t * @param <T> 泛型\n\t * @param key key \n\t * @param cs 指定转换类型 \n\t * @param defaultValue 值为Null时返回的默认值\n\t * @return 值 \n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tdefault <T> T getModel(String key, Class<T> cs, Object defaultValue) {\n\t\tT model = getModel(key, cs);\n\t\treturn valueIsNull(model) ? (T)defaultValue : model;\n\t}\n\n\t/**\n\t * 是否含有某个 key\n\t * @param key 指定 key\n\t * @return 是否含有\n\t */\n\tdefault boolean has(String key) {\n\t\treturn !valueIsNull(get(key));\n\t}\n\n\t\n\t// --------- 内部工具方法 \n\n\t/**\n\t * 判断一个值是否为null \n\t * @param value 指定值 \n\t * @return 此value是否为null \n\t */\n\tdefault boolean valueIsNull(Object value) {\n\t\treturn value == null || value.equals(\"\");\n\t}\n\n\t/**\n\t * 根据默认值来获取值\n\t * @param <T> 泛型\n\t * @param value 值 \n\t * @param defaultValue 默认值\n\t * @return 转换后的值 \n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tdefault <T> T getValueByDefaultValue(Object value, T defaultValue) {\n\t\t\n\t\t// 如果 obj 为 null，则直接返回默认值 \n\t\tif(valueIsNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\t\n\t\t// 开始转换类型\n\t\tClass<T> cs = (Class<T>) defaultValue.getClass();\n\t\treturn SaFoxUtil.getValueByType(value, cs);\n\t}\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.application;\n\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\n\n/**\n * 对写值的一组方法封装\n * <p> 封装 SaStorage、SaSession、SaApplication 等存取值的一些固定方法，减少重复编码 </p>\n * \n * @author click33\n * @since 1.31.0\n */\npublic interface SaSetValueInterface extends SaGetValueInterface {\n\n\t// --------- 需要子类实现的方法 \n\t\n\t/**\n\t * 写值 \n\t * @param key   名称\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\tSaSetValueInterface set(String key, Object value);\n\t\n\t/**\n\t * 删值 \n\t * @param key 要删除的key\n\t * @return 对象自身\n\t */\n\tSaSetValueInterface delete(String key);\n\n\t\n\t// --------- 接口提供封装的方法 \n\n\t/**\n\t * \n\t * 取值 (如果值为 null，则执行 fun 函数获取值，并把函数返回值写入缓存) \n\t * @param <T> 返回值的类型 \n\t * @param key key \n\t * @param fun 值为null时执行的函数 \n\t * @return 值 \n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tdefault <T> T get(String key, SaRetGenericFunction<T> fun) {\n\t\tObject value = get(key);\n\t\tif(value == null) {\n\t\t\tvalue = fun.run();\n\t\t\tset(key, value);\n\t\t}\n\t\treturn (T) value;\n\t}\n\t\n\t/**\n\t * 写值 (只有在此 key 原本无值的情况下才会写入)\n\t * @param key   名称\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\tdefault SaSetValueInterface setByNull(String key, Object value) {\n\t\tif( ! has(key)) {\n\t\t\tset(key, value);\n\t\t}\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/config/SaCookieConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.config;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token Cookie写入 相关配置\n *\n * @author click33\n * @since 1.27.0\n */\npublic class SaCookieConfig {\n\n\t/*\n\t\tCookie 功能为浏览器通用标准，建议大家自行搜索文章了解各个属性的功能含义，此处源码仅做简单解释。\n\t */\n\n    /**\n     * 作用域\n\t * <p> 写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。 </p>\n\t * <p> 一般情况下你不需要设置此值，因为浏览器默认会把 Cookie 写到当前域名下。 </p>\n     */\n    private String domain; \n\n    /**\n     * 路径 （一般只有当你在一个域名下部署多个项目时才会用到此值。）\n     */\n    private String path;\n\n    /**\n     * 是否只在 https 协议下有效\n     */\n    private Boolean secure = false; \n    \n    /**\n     * 是否禁止 js 操作 Cookie \n     */\n    private Boolean httpOnly = false; \n    \n    /**\n     * 第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\n     */\n\tprivate String sameSite;\n\n\t/**\n\t * 额外扩展属性\n\t */\n\tprivate Map<String, String> extraAttrs = new LinkedHashMap<>();\n\n\n\t/**\n\t * 获取：Cookie 作用域\n\t * \t<p> 写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。 </p>\n\t * \t<p> 一般情况下你不需要设置此值，因为浏览器默认会把 Cookie 写到当前域名下。 </p>\n\t * @return /\n\t */\n\tpublic String getDomain() {\n\t\treturn domain;\n\t}\n\n\t/**\n\t * 写入：Cookie 作用域\n\t * \t<p> 写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。 </p>\n\t * \t<p> 一般情况下你不需要设置此值，因为浏览器默认会把 Cookie 写到当前域名下。 </p>\n\t * @param domain /\n\t * @return 对象自身 \n\t */\n\tpublic SaCookieConfig setDomain(String domain) {\n\t\tthis.domain = domain;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 路径  （一般只有当你在一个域名下部署多个项目时才会用到此值。）\n\t */\n\tpublic String getPath() {\n\t\treturn path;\n\t}\n\n\t/**\n\t * @param path 路径  （一般只有当你在一个域名下部署多个项目时才会用到此值。）\n\t * @return 对象自身 \n\t */\n\tpublic SaCookieConfig setPath(String path) {\n\t\tthis.path = path;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否只在 https 协议下有效 \n\t */\n\tpublic Boolean getSecure() {\n\t\treturn secure;\n\t}\n\n\t/**\n\t * @param secure 是否只在 https 协议下有效 \n\t * @return 对象自身 \n\t */\n\tpublic SaCookieConfig setSecure(Boolean secure) {\n\t\tthis.secure = secure;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否禁止 js 操作 Cookie \n\t */\n\tpublic Boolean getHttpOnly() {\n\t\treturn httpOnly;\n\t}\n\n\t/**\n\t * @param httpOnly 是否禁止 js 操作 Cookie \n\t * @return 对象自身 \n\t */\n\tpublic SaCookieConfig setHttpOnly(Boolean httpOnly) {\n\t\tthis.httpOnly = httpOnly;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\n\t */\n\tpublic String getSameSite() {\n\t\treturn sameSite;\n\t}\n\n\t/**\n\t * @param sameSite 第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\n\t * @return 对象自身 \n\t */\n\tpublic SaCookieConfig setSameSite(String sameSite) {\n\t\tthis.sameSite = sameSite;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 获取额外扩展属性\n\t */\n\tpublic Map<String, String> getExtraAttrs() {\n\t\treturn extraAttrs;\n\t}\n\n\t/**\n\t * 写入额外扩展属性\n\t * @param extraAttrs /\n\t * @return 对象自身\n\t */\n\tpublic SaCookieConfig setExtraAttrs(Map<String, String> extraAttrs) {\n\t\tthis.extraAttrs = extraAttrs;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加扩展属性\n\t * @param name /\n\t * @param value /\n\t * @return 对象自身\n\t */\n\tpublic SaCookieConfig addExtraAttr(String name, String value) {\n\t\tif (extraAttrs == null) {\n\t\t\textraAttrs = new LinkedHashMap<>();\n\t\t}\n\t\tthis.extraAttrs.put(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加扩展属性\n\t * @param name /\n\t * @return 对象自身\n\t */\n\tpublic SaCookieConfig addExtraAttr(String name) {\n\t\treturn this.addExtraAttr(name, null);\n\t}\n\n\t/**\n\t * 移除指定扩展属性\n\t * @param name /\n\t * @return 对象自身\n\t */\n\tpublic SaCookieConfig removeExtraAttr(String name) {\n\t\tif(extraAttrs != null) {\n\t\t\tthis.extraAttrs.remove(name);\n\t\t}\n\t\treturn this;\n\t}\n\n\t// toString \n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaCookieConfig [\" +\n\t\t\t\t\"domain=\" + domain +\n\t\t\t\t\", path=\" + path +\n\t\t\t\t\", secure=\" + secure +\n\t\t\t\t\", httpOnly=\" + httpOnly +\n\t\t\t\t\", sameSite=\" + sameSite +\n\t\t\t\t\", extraAttrs=\" + extraAttrs +\n\t\t\t\t\"]\";\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.config;\n\nimport cn.dev33.satoken.stp.parameter.enums.SaLogoutMode;\nimport cn.dev33.satoken.stp.parameter.enums.SaLogoutRange;\nimport cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode;\nimport cn.dev33.satoken.stp.parameter.enums.SaReplacedRange;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\n\n/**\n * Sa-Token 配置类 Model\n *\n * <p>\n *     你可以通过yml、properties、java代码等形式配置本类参数，具体请查阅官方文档:\n *     <a href=\"https://sa-token.cc\">https://sa-token.cc</a>\n * </p>\n *\n * @author click33\n * @since 1.10.0\n */\npublic class SaTokenConfig implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/** token 名称 （同时也是： cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀） */\n\tprivate String tokenName = \"satoken\";\n\n\t/** token 有效期（单位：秒） 默认30天，-1 代表永久有效 */\n\tprivate long timeout = 60 * 60 * 24 * 30;\n\n\t/**\n\t * token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\t * （例如可以设置为 1800 代表 30 分钟内无操作就冻结）\n\t */\n\tprivate long activeTimeout = -1;\n\n\t/**\n\t * 是否启用动态 activeTimeout 功能，如不需要请设置为 false，节省缓存请求次数\n\t */\n\tprivate Boolean dynamicActiveTimeout = false;\n\n\t/**\n\t * 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\t */\n\tprivate Boolean isConcurrent = true;\n\n\t/**\n\t * 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n\t */\n\tprivate Boolean isShare = false;\n\n\t/**\n\t * 在 isConcurrent=false 时，决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线，新设备登录成功, NEW_DEVICE=新设备登录失败，旧设备维持在线)\n\t */\n\tprivate SaReplacedLoginExitMode replacedLoginExitMode = SaReplacedLoginExitMode.OLD_DEVICE;\n\n\t/**\n\t * 当 isConcurrent=false 时，顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端)\n\t */\n\tprivate SaReplacedRange replacedRange = SaReplacedRange.CURR_DEVICE_TYPE;\n\n\t/**\n\t * 同一账号最大登录数量，-1代表不限 （只有在 isConcurrent=true, isShare=false 时此配置项才有意义）\n\t */\n\tprivate int maxLoginCount = 12;\n\n\t/**\n\t * 溢出 maxLoginCount 的客户端，将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线)\n\t */\n\tprivate SaLogoutMode overflowLogoutMode = SaLogoutMode.LOGOUT;\n\n\t/**\n\t * 在每次创建 token 时的最高循环次数，用于保证 token 唯一性（-1=不循环尝试，直接使用）\n\t */\n\tprivate int maxTryTimes = 12;\n\n\t/**\n\t * 是否尝试从请求体里读取 token\n\t */\n\tprivate Boolean isReadBody = true;\n\n\t/**\n\t * 是否尝试从 header 里读取 token\n\t */\n\tprivate Boolean isReadHeader = true;\n\n\t/**\n\t * 是否尝试从 cookie 里读取 token\n\t */\n\tprivate Boolean isReadCookie = true;\n\n\t/**\n\t * 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t */\n\tprivate Boolean isLastingCookie = true;\n\n\t/**\n\t * 是否在登录后将 token 写入到响应头\n\t */\n\tprivate Boolean isWriteHeader = false;\n\n\t/**\n\t * 注销范围 (TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)\n\t * <br/> (此参数只在调用 StpUtil.logout() 时有效)\n\t */\n\tprivate SaLogoutRange logoutRange = SaLogoutRange.TOKEN;\n\n\t/**\n\t * 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)\n\t * <br/> (此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue(\"token\") 时有效)\n\t */\n\tprivate Boolean isLogoutKeepFreezeOps = false;\n\n\t/**\n\t * 在注销 token 后，是否保留其对应的 Token-Session\n\t */\n\tprivate Boolean isLogoutKeepTokenSession = false;\n\n\t/**\n\t * 在登录时，是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\n\t */\n\tprivate Boolean rightNowCreateTokenSession = false;\n\n\t/**\n\t * token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n\t */\n\tprivate String tokenStyle = \"uuid\";\n\n\t/**\n\t * 默认 SaTokenDao 实现类中，每次清理过期数据间隔的时间（单位: 秒），默认值30秒，设置为 -1 代表不启动定时清理\n\t */\n\tprivate int dataRefreshPeriod = 30;\n\n\t/**\n\t * 获取 Token-Session 时是否必须登录（如果配置为true，会在每次获取 getTokenSession() 时校验当前是否登录）\n\t */\n\tprivate Boolean tokenSessionCheckLogin = true;\n\n\t/**\n\t * 是否打开自动续签 activeTimeout （如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作）\n\t */\n\tprivate Boolean autoRenew = true;\n\n\t/**\n\t * token 前缀, 前端提交 token 时应该填写的固定前缀，格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)\n\t */\n\tprivate String tokenPrefix;\n\n\t/**\n\t * cookie 模式是否自动填充 token 前缀\n\t */\n\tprivate Boolean cookieAutoFillPrefix = false;\n\n\t/**\n\t * 是否在初始化配置时在控制台打印版本字符画\n\t */\n\tprivate Boolean isPrint = true;\n\n\t/**\n\t * 是否打印操作日志\n\t */\n\tprivate Boolean isLog = false;\n\n\t/**\n\t * 日志等级（trace、debug、info、warn、error、fatal），此值与 logLevelInt 联动\n\t */\n\tprivate String logLevel = \"trace\";\n\n\t/**\n\t * 日志等级 int 值（1=trace、2=debug、3=info、4=warn、5=error、6=fatal），此值与 logLevel 联动\n\t */\n\tprivate int logLevelInt = 1;\n\n\t/**\n\t * 是否打印彩色日志\n\t */\n\tprivate Boolean isColorLog = null;\n\n\t/**\n\t * jwt秘钥（只有集成 jwt 相关模块时此参数才会生效）\n\t */\n\tprivate String jwtSecretKey;\n\n\t/**\n\t * Http Basic 认证的默认账号和密码，冒号隔开，例如：sa:123456\n\t */\n\tprivate String httpBasic = \"\";\n\n\t/**\n\t * Http Digest 认证的默认账号和密码，冒号隔开，例如：sa:123456\n\t */\n\tprivate String httpDigest = \"\";\n\n\t/**\n\t * 配置当前项目的网络访问地址\n\t */\n\tprivate String currDomain;\n\n\t/**\n\t * Same-Token 的有效期 (单位: 秒)\n\t */\n\tprivate long sameTokenTimeout = 60 * 60 * 24;\n\n\t/**\n\t * 是否校验 Same-Token（部分rpc插件有效）\n\t */\n\tprivate Boolean checkSameToken = false;\n\n\t/**\n\t * Cookie配置对象 \n\t */\n\tpublic SaCookieConfig cookie = new SaCookieConfig();\n\n\t/**\n\t * @return token 名称 （同时也是： cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀）\n\t */\n\tpublic String getTokenName() {\n\t\treturn tokenName;\n\t}\n\n\t/**\n\t * @param tokenName token 名称 （同时也是： cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setTokenName(String tokenName) {\n\t\tthis.tokenName = tokenName;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return token 有效期（单位：秒） 默认30天，-1 代表永久有效\n\t */\n\tpublic long getTimeout() {\n\t\treturn timeout;\n\t}\n\n\t/**\n\t * @param timeout token 有效期（单位：秒） 默认30天，-1 代表永久有效\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setTimeout(long timeout) {\n\t\tthis.timeout = timeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\t * \t\t\t\t\t\t\t（例如可以设置为 1800 代表 30 分钟内无操作就冻结）\n\t */\n\tpublic long getActiveTimeout() {\n\t\treturn activeTimeout;\n\t}\n\n\t/**\n\t * @param activeTimeout token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\t * \t\t\t\t\t\t\t\t（例如可以设置为 1800 代表 30 分钟内无操作就冻结）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setActiveTimeout(long activeTimeout) {\n\t\tthis.activeTimeout = activeTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否启用动态 activeTimeout 功能，如不需要请设置为 false，节省缓存请求次数\n\t */\n\tpublic Boolean getDynamicActiveTimeout() {\n\t\treturn dynamicActiveTimeout;\n\t}\n\n\t/**\n\t * @param dynamicActiveTimeout 是否启用动态 activeTimeout 功能，如不需要请设置为 false，节省缓存请求次数\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setDynamicActiveTimeout(Boolean dynamicActiveTimeout) {\n\t\tthis.dynamicActiveTimeout = dynamicActiveTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\t */\n\tpublic Boolean getIsConcurrent() {\n\t\treturn isConcurrent;\n\t}\n\n\t/**\n\t * @param isConcurrent 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsConcurrent(Boolean isConcurrent) {\n\t\tthis.isConcurrent = isConcurrent;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token）\n\t */\n\tpublic Boolean getIsShare() {\n\t\treturn isShare;\n\t}\n\n\t/**\n\t * @param isShare 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsShare(Boolean isShare) {\n\t\tthis.isShare = isShare;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 同一账号最大登录数量，-1代表不限 （只有在 isConcurrent=true, isShare=false 时此配置项才有意义）\n\t */\n\tpublic int getMaxLoginCount() {\n\t\treturn maxLoginCount;\n\t}\n\n\t/**\n\t * @param maxLoginCount 同一账号最大登录数量，-1代表不限 （只有在 isConcurrent=true, isShare=false 时此配置项才有意义）\n\t * @return 对象自身 \n\t */\n\tpublic SaTokenConfig setMaxLoginCount(int maxLoginCount) {\n\t\tthis.maxLoginCount = maxLoginCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 在每次创建 token 时的最高循环次数，用于保证 token 唯一性（-1=不循环尝试，直接使用）\n\t */\n\tpublic int getMaxTryTimes() {\n\t\treturn maxTryTimes;\n\t}\n\n\t/**\n\t * @param maxTryTimes 在每次创建 token 时的最高循环次数，用于保证 token 唯一性（-1=不循环尝试，直接使用）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setMaxTryTimes(int maxTryTimes) {\n\t\tthis.maxTryTimes = maxTryTimes;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否尝试从请求体里读取 token\n\t */\n\tpublic Boolean getIsReadBody() {\n\t\treturn isReadBody;\n\t}\n\n\t/**\n\t * @param isReadBody 是否尝试从请求体里读取 token\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsReadBody(Boolean isReadBody) {\n\t\tthis.isReadBody = isReadBody;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否尝试从 header 里读取 token\n\t */\n\tpublic Boolean getIsReadHeader() {\n\t\treturn isReadHeader;\n\t}\n\n\t/**\n\t * @param isReadHeader 是否尝试从 header 里读取 token\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsReadHeader(Boolean isReadHeader) {\n\t\tthis.isReadHeader = isReadHeader;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否尝试从 cookie 里读取 token\n\t */\n\tpublic Boolean getIsReadCookie() {\n\t\treturn isReadCookie;\n\t}\n\n\t/**\n\t * @param isReadCookie 是否尝试从 cookie 里读取 token\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsReadCookie(Boolean isReadCookie) {\n\t\tthis.isReadCookie = isReadCookie;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t *\n\t * @return isLastingCookie /\n\t */\n\tpublic Boolean getIsLastingCookie() {\n\t\treturn this.isLastingCookie;\n\t}\n\n\t/**\n\t * 设置 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t *\n\t * @param isLastingCookie /\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsLastingCookie(Boolean isLastingCookie) {\n\t\tthis.isLastingCookie = isLastingCookie;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否在登录后将 token 写入到响应头\n\t */\n\tpublic Boolean getIsWriteHeader() {\n\t\treturn isWriteHeader;\n\t}\n\n\t/**\n\t * @param isWriteHeader 是否在登录后将 token 写入到响应头\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsWriteHeader(Boolean isWriteHeader) {\n\t\tthis.isWriteHeader = isWriteHeader;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n\t */\n\tpublic String getTokenStyle() {\n\t\treturn tokenStyle;\n\t}\n\n\t/**\n\t * @param tokenStyle token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setTokenStyle(String tokenStyle) {\n\t\tthis.tokenStyle = tokenStyle;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 默认 SaTokenDao 实现类中，每次清理过期数据间隔的时间（单位: 秒），默认值30秒，设置为 -1 代表不启动定时清理\n\t */\n\tpublic int getDataRefreshPeriod() {\n\t\treturn dataRefreshPeriod;\n\t}\n\n\t/**\n\t * @param dataRefreshPeriod 默认 SaTokenDao 实现类中，每次清理过期数据间隔的时间（单位: 秒），默认值30秒，设置为 -1 代表不启动定时清理\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setDataRefreshPeriod(int dataRefreshPeriod) {\n\t\tthis.dataRefreshPeriod = dataRefreshPeriod;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 获取 Token-Session 时是否必须登录（如果配置为true，会在每次获取 getTokenSession() 时校验当前是否登录）\n\t */\n\tpublic Boolean getTokenSessionCheckLogin() {\n\t\treturn tokenSessionCheckLogin;\n\t}\n\n\t/**\n\t * @param tokenSessionCheckLogin 获取 Token-Session 时是否必须登录（如果配置为true，会在每次获取 getTokenSession() 时校验当前是否登录）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {\n\t\tthis.tokenSessionCheckLogin = tokenSessionCheckLogin;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否打开自动续签 activeTimeout （如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作）\n\t */\n\tpublic Boolean getAutoRenew() {\n\t\treturn autoRenew;\n\t}\n\n\t/**\n\t * @param autoRenew 是否打开自动续签 activeTimeout （如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setAutoRenew(Boolean autoRenew) {\n\t\tthis.autoRenew = autoRenew;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return token 前缀, 前端提交 token 时应该填写的固定前缀，格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)\n\t */\n\tpublic String getTokenPrefix() {\n\t\treturn tokenPrefix;\n\t}\n\n\t/**\n\t * @param tokenPrefix token 前缀, 前端提交 token 时应该填写的固定前缀，格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setTokenPrefix(String tokenPrefix) {\n\t\tthis.tokenPrefix = tokenPrefix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return cookie 模式是否自动填充 token 前缀\n\t */\n\tpublic Boolean getCookieAutoFillPrefix() {\n\t\treturn cookieAutoFillPrefix;\n\t}\n\n\t/**\n\t * @param cookieAutoFillPrefix cookie 模式是否自动填充 token 前缀\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setCookieAutoFillPrefix(Boolean cookieAutoFillPrefix) {\n\t\tthis.cookieAutoFillPrefix = cookieAutoFillPrefix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否在初始化配置时在控制台打印版本字符画\n\t */\n\tpublic Boolean getIsPrint() {\n\t\treturn isPrint;\n\t}\n\n\t/**\n\t * @param isPrint 是否在初始化配置时在控制台打印版本字符画\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsPrint(Boolean isPrint) {\n\t\tthis.isPrint = isPrint;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否打印操作日志\n\t */\n\tpublic Boolean getIsLog() {\n\t\treturn isLog;\n\t}\n\n\t/**\n\t * @param isLog 是否打印操作日志\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsLog(Boolean isLog) {\n\t\tthis.isLog = isLog;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 日志等级（trace、debug、info、warn、error、fatal），此值与 logLevelInt 联动\n\t */\n\tpublic String getLogLevel() {\n\t\treturn logLevel;\n\t}\n\n\t/**\n\t * @param logLevel 日志等级（trace、debug、info、warn、error、fatal），此值与 logLevelInt 联动\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setLogLevel(String logLevel) {\n\t\tthis.logLevel = logLevel;\n\t\tthis.logLevelInt = SaFoxUtil.translateLogLevelToInt(logLevel);\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 日志等级 int 值（1=trace、2=debug、3=info、4=warn、5=error、6=fatal），此值与 logLevel 联动\n\t */\n\tpublic int getLogLevelInt() {\n\t\treturn logLevelInt;\n\t}\n\n\t/**\n\t * @param logLevelInt 日志等级 int 值（1=trace、2=debug、3=info、4=warn、5=error、6=fatal），此值与 logLevel 联动\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setLogLevelInt(int logLevelInt) {\n\t\tthis.logLevelInt = logLevelInt;\n\t\tthis.logLevel = SaFoxUtil.translateLogLevelToString(logLevelInt);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取：是否打印彩色日志\n\t *\n\t * @return isColorLog 是否打印彩色日志\n\t */\n\tpublic Boolean getIsColorLog() {\n\t\treturn this.isColorLog;\n\t}\n\n\t/**\n\t * 设置：是否打印彩色日志\n\t *\n\t * @param isColorLog 是否打印彩色日志\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsColorLog(Boolean isColorLog) {\n\t\tthis.isColorLog = isColorLog;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return jwt秘钥（只有集成 jwt 相关模块时此参数才会生效）\n\t */\n\tpublic String getJwtSecretKey() {\n\t\treturn jwtSecretKey;\n\t}\n\n\t/**\n\t * @param jwtSecretKey jwt秘钥（只有集成 jwt 相关模块时此参数才会生效）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setJwtSecretKey(String jwtSecretKey) {\n\t\tthis.jwtSecretKey = jwtSecretKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return Http Basic 认证的默认账号和密码，冒号隔开，例如：sa:123456\n\t */\n\tpublic String getHttpBasic() {\n\t\treturn httpBasic;\n\t}\n\n\t/**\n\t * @param httpBasic Http Basic 认证的默认账号和密码，冒号隔开，例如：sa:123456\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setHttpBasic(String httpBasic) {\n\t\tthis.httpBasic = httpBasic;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return Http Digest 认证的默认账号和密码，冒号隔开，例如：sa:123456\n\t */\n\tpublic String getHttpDigest() {\n\t\treturn httpDigest;\n\t}\n\n\t/**\n\t * @param httpDigest Http Digest 认证的默认账号和密码，冒号隔开，例如：sa:123456\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setHttpDigest(String httpDigest) {\n\t\tthis.httpDigest = httpDigest;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 配置当前项目的网络访问地址\n\t */\n\tpublic String getCurrDomain() {\n\t\treturn currDomain;\n\t}\n\n\t/**\n\t * @param currDomain 配置当前项目的网络访问地址\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setCurrDomain(String currDomain) {\n\t\tthis.currDomain = currDomain;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return Same-Token 的有效期 (单位: 秒)\n\t */\n\tpublic long getSameTokenTimeout() {\n\t\treturn sameTokenTimeout;\n\t}\n\n\t/**\n\t * @param sameTokenTimeout Same-Token 的有效期 (单位: 秒)\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setSameTokenTimeout(long sameTokenTimeout) {\n\t\tthis.sameTokenTimeout = sameTokenTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否校验Same-Token（部分rpc插件有效）\n\t */\n\tpublic Boolean getCheckSameToken() {\n\t\treturn checkSameToken;\n\t}\n\n\t/**\n\t * @param checkSameToken 是否校验Same-Token（部分rpc插件有效）\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setCheckSameToken(Boolean checkSameToken) {\n\t\tthis.checkSameToken = checkSameToken;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 在 isConcurrent=false 时，决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线，新设备登录成功, NEW_DEVICE=新设备登录失败，旧设备维持在线)\n\t */\n\tpublic SaReplacedLoginExitMode getReplacedLoginExitMode() {\n\t\treturn replacedLoginExitMode;\n\t}\n\n\t/**\n\t * @param replacedLoginExitMode 在 isConcurrent=false 时，决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线，新设备登录成功, NEW_DEVICE=新设备登录失败，旧设备维持在线)\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setReplacedLoginExitMode(SaReplacedLoginExitMode replacedLoginExitMode) {\n\t\tthis.replacedLoginExitMode = replacedLoginExitMode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 当 isConcurrent=false 时，顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端 ALL_DEVICE_TYPE=所有设备类型端)\n\t *\n\t * @return /\n\t */\n\tpublic SaReplacedRange getReplacedRange() {\n\t\treturn this.replacedRange;\n\t}\n\n\t/**\n\t * 设置 当 isConcurrent=false 时，顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端 ALL_DEVICE_TYPE=所有设备类型端)\n\t *\n\t * @param replacedRange /\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setReplacedRange(SaReplacedRange replacedRange) {\n\t\tthis.replacedRange = replacedRange;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 溢出 maxLoginCount 的客户端，将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线)\n\t *\n\t * @return /\n\t */\n\tpublic SaLogoutMode getOverflowLogoutMode() {\n\t\treturn this.overflowLogoutMode;\n\t}\n\n\t/**\n\t * 设置 溢出 maxLoginCount 的客户端，将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线)\n\t *\n\t * @param overflowLogoutMode /\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setOverflowLogoutMode(SaLogoutMode overflowLogoutMode) {\n\t\tthis.overflowLogoutMode = overflowLogoutMode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 注销范围 (TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)  <br> (此参数只在调用 StpUtil.logout() 时有效)\n\t *\n\t * @return /\n\t */\n\tpublic SaLogoutRange getLogoutRange() {\n\t\treturn this.logoutRange;\n\t}\n\n\t/**\n\t * 设置 注销范围 (TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)  <br> (此参数只在调用 StpUtil.logout() 时有效)\n\t *\n\t * @param logoutRange /\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setLogoutRange(SaLogoutRange logoutRange) {\n\t\tthis.logoutRange = logoutRange;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)  <br> (此参数只在调用 StpUtil.[logoutkickoutreplaced]ByTokenValue(\"token\") 时有效)\n\t *\n\t * @return isLogoutKeepFreezeOps /\n\t */\n\tpublic Boolean getIsLogoutKeepFreezeOps() {\n\t\treturn this.isLogoutKeepFreezeOps;\n\t}\n\n\t/**\n\t * 设置 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)  <br> (此参数只在调用 StpUtil.[logoutkickoutreplaced]ByTokenValue(\"token\") 时有效)\n\t *\n\t * @param isLogoutKeepFreezeOps /\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsLogoutKeepFreezeOps(Boolean isLogoutKeepFreezeOps) {\n\t\tthis.isLogoutKeepFreezeOps = isLogoutKeepFreezeOps;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 在注销 token 后，是否保留其对应的 Token-Session\n\t *\n\t * @return isLogoutKeepTokenSession /\n\t */\n\tpublic Boolean getIsLogoutKeepTokenSession() {\n\t\treturn this.isLogoutKeepTokenSession;\n\t}\n\n\t/**\n\t * 设置 在注销 token 后，是否保留其对应的 Token-Session\n\t *\n\t * @param isLogoutKeepTokenSession /\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setIsLogoutKeepTokenSession(Boolean isLogoutKeepTokenSession) {\n\t\tthis.isLogoutKeepTokenSession = isLogoutKeepTokenSession;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 在登录时，是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\n\t *\n\t * @return /\n\t */\n\tpublic Boolean getRightNowCreateTokenSession() {\n\t\treturn this.rightNowCreateTokenSession;\n\t}\n\n\t/**\n\t * 设置 在登录时，是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\n\t *\n\t * @param rightNowCreateTokenSession /\n\t * @return 对象自身\n\t */\n\tpublic SaTokenConfig setRightNowCreateTokenSession(Boolean rightNowCreateTokenSession) {\n\t\tthis.rightNowCreateTokenSession = rightNowCreateTokenSession;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return Cookie 全局配置对象\n\t */\n\tpublic SaCookieConfig getCookie() {\n\t\treturn cookie;\n\t}\n\n\t/**\n\t * @param cookie Cookie 全局配置对象\n\t * @return 对象自身 \n\t */\n\tpublic SaTokenConfig setCookie(SaCookieConfig cookie) {\n\t\tthis.cookie = cookie;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaTokenConfig [\"\n\t\t\t\t+ \"tokenName=\" + tokenName \n\t\t\t\t+ \", timeout=\" + timeout \n\t\t\t\t+ \", activeTimeout=\" + activeTimeout\n\t\t\t\t+ \", dynamicActiveTimeout=\" + dynamicActiveTimeout\n\t\t\t\t+ \", isConcurrent=\" + isConcurrent\n\t\t\t\t+ \", isShare=\" + isShare\n\t\t\t\t+ \", replacedRange=\" + replacedRange\n\t\t\t\t+ \", replacedLoginExitMode=\" + replacedLoginExitMode\n\t\t\t\t+ \", maxLoginCount=\" + maxLoginCount\n\t\t\t\t+ \", overflowLogoutMode=\" + overflowLogoutMode\n\t\t\t\t+ \", maxTryTimes=\" + maxTryTimes\n\t\t\t\t+ \", isReadBody=\" + isReadBody\n\t\t\t\t+ \", isReadHeader=\" + isReadHeader \n\t\t\t\t+ \", isReadCookie=\" + isReadCookie\n\t\t\t\t+ \", isLastingCookie=\" + isLastingCookie\n\t\t\t\t+ \", isWriteHeader=\" + isWriteHeader\n\t\t\t\t+ \", logoutRange=\" + logoutRange\n\t\t\t\t+ \", isLogoutKeepFreezeOps=\" + isLogoutKeepFreezeOps\n\t\t\t\t+ \", isLogoutKeepTokenSession=\" + isLogoutKeepTokenSession\n\t\t\t\t+ \", rightNowCreateTokenSession=\" + rightNowCreateTokenSession\n\t\t\t\t+ \", tokenStyle=\" + tokenStyle\n\t\t\t\t+ \", dataRefreshPeriod=\" + dataRefreshPeriod \n\t\t\t\t+ \", tokenSessionCheckLogin=\" + tokenSessionCheckLogin\n\t\t\t\t+ \", autoRenew=\" + autoRenew\n\t\t\t\t+ \", tokenPrefix=\" + tokenPrefix\n\t\t\t\t+ \", cookieAutoFillPrefix=\" + cookieAutoFillPrefix\n\t\t\t\t+ \", isPrint=\" + isPrint \n\t\t\t\t+ \", isLog=\" + isLog \n\t\t\t\t+ \", logLevel=\" + logLevel \n\t\t\t\t+ \", logLevelInt=\" + logLevelInt\n\t\t\t\t+ \", isColorLog=\" + isColorLog\n\t\t\t\t+ \", jwtSecretKey=\" + jwtSecretKey \n\t\t\t\t+ \", httpBasic=\" + httpBasic\n\t\t\t\t+ \", httpDigest=\" + httpDigest\n\t\t\t\t+ \", currDomain=\" + currDomain \n\t\t\t\t+ \", sameTokenTimeout=\" + sameTokenTimeout\n\t\t\t\t+ \", checkSameToken=\" + checkSameToken \n\t\t\t\t+ \", cookie=\" + cookie\n\t\t\t\t+ \"]\";\n\t}\n\n\n\n\t// ------------------- 过期方法 -------------------\n\n\t/**\n\t * <h2> 请更改为 getActiveTimeout() </h2>\n\t * @return token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\t * \t\t\t\t\t\t\t（例如可以设置为 1800 代表 30 分钟内无操作就冻结）\n\t */\n\t@Deprecated\n\tpublic long getActivityTimeout() {\n//\t\tSystem.err.println(\"配置项已过期，请更换：sa-token.activity-timeout -> sa-token.active-timeout\");\n\t\treturn activeTimeout;\n\t}\n\n\t/**\n\t * <h2> 请更改为 setActiveTimeout() </h2>\n\t * @param activityTimeout token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\t * \t\t\t\t\t\t\t\t（例如可以设置为 1800 代表 30 分钟内无操作就冻结）\n\t * @return 对象自身\n\t */\n\t@Deprecated\n\tpublic SaTokenConfig setActivityTimeout(long activityTimeout) {\n\t\tSystem.err.println(\"配置项已过期，请更换：sa-token.activity-timeout -> sa-token.active-timeout\");\n\t\tthis.activeTimeout = activityTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * <h2> 请更改为 getHttpBasic() </h2>\n\t * @return Http Basic 认证的默认账号和密码\n\t */\n\t@Deprecated\n\tpublic String getBasic() {\n\t\treturn httpBasic;\n\t}\n\n\t/**\n\t * <h2> 请更改为 setHttpBasic() </h2>\n\t * @param basic Http Basic 认证的默认账号和密码\n\t * @return 对象自身\n\t */\n\t@Deprecated\n\tpublic SaTokenConfig setBasic(String basic) {\n\t\tthis.httpBasic = basic;\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfigFactory.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.config;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.lang.reflect.Field;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * Sa-Token配置文件的构建工厂类\n *\n * <p> 用于手动读取配置文件初始化 SaTokenConfig 对象，只有在非IOC环境下你才会用到此类 </p>\n * \n * @author click33\n * @since 1.10.0\n */\npublic class SaTokenConfigFactory {\n\n\tprivate SaTokenConfigFactory() {\n\t}\n\t\n\t/**\n\t * 配置文件地址 \n\t */\n\tpublic static String configPath = \"sa-token.properties\";\n\n\t/**\n\t * 根据 configPath 路径获取配置信息\n\t * \n\t * @return 一个SaTokenConfig对象\n\t */\n\tpublic static SaTokenConfig createConfig() {\n\t\treturn createConfig(configPath);\n\t}\n\n\t/**\n\t * 根据指定路径路径获取配置信息 \n\t * \n\t * @param path 配置文件路径 \n\t * @return 一个 SaTokenConfig 对象\n\t */\n\tpublic static SaTokenConfig createConfig(String path) {\n\t\tMap<String, String> map = readPropToMap(path);\n\t\t// if (map == null) {\n\t\t\t// throw new RuntimeException(\"找不到配置文件：\" + configPath, null);\n\t\t// }\n\t\treturn (SaTokenConfig) initPropByMap(map, new SaTokenConfig());\n\t}\n\n\t/**\n\t * 工具方法: 将指定路径的properties配置文件读取到Map中 \n\t * \n\t * @param propertiesPath 配置文件地址\n\t * @return 一个Map\n\t */\n\tprivate static Map<String, String> readPropToMap(String propertiesPath) {\n\t\tMap<String, String> map = new HashMap<>(16);\n\t\ttry {\n\t\t\tInputStream is = SaTokenConfigFactory.class.getClassLoader().getResourceAsStream(propertiesPath);\n\t\t\tif (is == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tProperties prop = new Properties();\n\t\t\tprop.load(is);\n\t\t\tfor (String key : prop.stringPropertyNames()) {\n\t\t\t\tmap.put(key, prop.getProperty(key));\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tthrow new SaTokenException(\"配置文件(\" + propertiesPath + \")加载失败\", e).setCode(SaErrorCode.CODE_10021);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 工具方法: 将 Map 的值映射到一个 Model 上 \n\t * \n\t * @param map 属性集合\n\t * @param obj 对象, 或类型\n\t * @return 返回实例化后的对象\n\t */\n\tprivate static Object initPropByMap(Map<String, String> map, Object obj) {\n\n\t\tif (map == null) {\n\t\t\tmap = new HashMap<>(16);\n\t\t}\n\n\t\t// 1、取出类型\n\t\tClass<?> cs;\n\t\tif (obj instanceof Class) {\n\t\t\t// 如果是一个类型，则将obj=null，以便完成静态属性反射赋值\n\t\t\tcs = (Class<?>) obj;\n\t\t\tobj = null;\n\t\t} else {\n\t\t\t// 如果是一个对象，则取出其类型\n\t\t\tcs = obj.getClass();\n\t\t}\n\n\t\t// 2、遍历类型属性，反射赋值\n\t\tfor (Field field : cs.getDeclaredFields()) {\n\t\t\tString value = map.get(field.getName());\n\t\t\tif (value == null) {\n\t\t\t\t// 如果为空代表没有配置此项\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tObject valueConvert = SaFoxUtil.getValueByType(value, field.getType());\n\t\t\t\tfield.setAccessible(true);\n\t\t\t\tfield.set(obj, valueConvert);\n\t\t\t} catch (IllegalArgumentException | IllegalAccessException e) {\n\t\t\t\tthrow new SaTokenException(\"属性赋值出错：\" + field.getName(), e).setCode(SaErrorCode.CODE_10022);\n\t\t\t}\n\t\t}\n\t\treturn obj;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/SaHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.application.SaApplication;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\n\n/**\n * Sa-Token 上下文持有类，你可以通过此类快速获取当前环境下的 SaRequest、SaResponse、SaStorage、SaApplication 对象。\n *\n * @author click33\n * @since 1.18.0\n */\npublic class SaHolder {\n\t\n\t/**\n\t * 获取当前请求的 SaTokenContext 上下文对象\n\t * @see SaTokenContext\n\t * \n\t * @return /\n\t */\n\tpublic static SaTokenContext getContext() {\n\t\treturn SaManager.getSaTokenContext();\n\t}\n\n\t/**\n\t * 获取当前请求的 Request 包装对象\n\t * @see SaRequest\n\t * \n\t * @return /\n\t */\n\tpublic static SaRequest getRequest() {\n\t\treturn SaManager.getSaTokenContext().getRequest();\n\t}\n\n\t/**\n\t * 获取当前请求的 Response 包装对象\n\t * @see SaResponse\n\t * \n\t * @return /\n\t */\n\tpublic static SaResponse getResponse() {\n\t\treturn SaManager.getSaTokenContext().getResponse();\n\t}\n\n\t/**\n\t * 获取当前请求的 Storage 包装对象\n\t * @see SaStorage\n\t *\n\t * @return /\n\t */\n\tpublic static SaStorage getStorage() {\n\t\treturn SaManager.getSaTokenContext().getStorage();\n\t}\n\n\t/**\n\t * 获取全局 SaApplication 对象\n\t * @see SaApplication\n\t * \n\t * @return /\n\t */\n\tpublic static SaApplication getApplication() {\n\t\treturn SaApplication.defaultInstance;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContext.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\n\n/**\n * Sa-Token 上下文处理器\n *\n * <p> 上下文处理器封装了当前应用环境的底层操作，是 Sa-Token 对接不同 web 框架的关键，详细可参考在线文档 “自定义 SaTokenContext 指南”章节 </p>\n *\n * @author click33\n * @since 1.16.0\n */\npublic interface SaTokenContext {\n\n\t/**\n\t * 初始化上下文\n\t *\n\t * @param req /\n\t * @param res /\n\t * @param stg /\n\t */\n\tvoid setContext(SaRequest req, SaResponse res, SaStorage stg);\n\n\t/**\n\t * 清除化上下文\n\t */\n\tvoid clearContext();\n\n\t/**\n\t * 判断当前上下文是否可用\n\t *\n\t * @return /\n\t */\n\tboolean isValid();\n\n\t/**\n\t * 获取 Box 对象\n\t */\n\tSaTokenContextModelBox getModelBox();\n\n\t/**\n\t * 获取当前上下文的 Request 包装对象\n\t * @see SaRequest\n\t * \n\t * @return /\n\t */\n\tdefault SaRequest getRequest() {\n\t\treturn getModelBox().getRequest();\n\t}\n\n\t/**\n\t * 获取当前上下文的 Response 包装对象\n\t * @see SaResponse\n\t * \n\t * @return /\n\t */\n\tdefault SaResponse getResponse(){\n\t\treturn getModelBox().getResponse();\n\t}\n\n\t/**\n\t * 获取当前上下文的 Storage 包装对象\n\t * @see SaStorage\n\t * \n\t * @return /\n\t */\n\tdefault SaStorage getStorage(){\n\t\treturn getModelBox().getStorage();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenContextException;\n\n/**\n * Sa-Token 上下文处理器 [ 默认实现类 ]\n * \n * <p>  \n * \t一般情况下框架会为你自动注入合适的上下文处理器，如果代码断点走到了此默认实现类，\n * \t说明你引入的依赖有问题或者错误的调用了 Sa-Token 的API， 请在 [ 在线开发文档 → 附录 → 常见问题排查 ] 中按照提示进行排查\n * </p>\n * \n * @author click33\n * @since 1.16.0\n */\npublic class SaTokenContextDefaultImpl implements SaTokenContext {\n\t\n\t/**\n\t * 默认的上下文处理器对象  \n\t */\n\tpublic static SaTokenContextDefaultImpl defaultContext = new SaTokenContextDefaultImpl();\n\n\t/**\n\t * 错误提示语\n\t */\n\tpublic static final String ERROR_MESSAGE = \"未能获取有效的上下文处理器\";\n\n\t@Override\n\tpublic void setContext(SaRequest req, SaResponse res, SaStorage stg) {\n\t\tthrow new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001);\n\t}\n\n\t@Override\n\tpublic void clearContext() {\n\t\tthrow new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001);\n\t}\n\n\t@Override\n\tpublic boolean isValid() {\n\t\tthrow new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001);\n\t}\n\n\t@Override\n\tpublic SaTokenContextModelBox getModelBox() {\n\t\tthrow new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001);\n\t}\n\n\t@Override\n\tpublic SaRequest getRequest() {\n\t\tthrow new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001);\n\t}\n\n\t@Override\n\tpublic SaResponse getResponse() {\n\t\tthrow new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001);\n\t}\n\n\t@Override\n\tpublic SaStorage getStorage() {\n\t\tthrow new SaTokenContextException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10001);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForReadOnly.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\n\n/**\n * Sa-Token 上下文处理器次级实现：只读上下文\n * \n * @author click33\n * @since 1.42.0\n */\npublic interface SaTokenContextForReadOnly extends SaTokenContext {\n\n\t@Override\n\tdefault void setContext(SaRequest req, SaResponse res, SaStorage stg) {\n\n\t}\n\n\t@Override\n\tdefault void clearContext() {\n\n\t}\n\n\t@Override\n\tdefault SaTokenContextModelBox getModelBox() {\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocal.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\n\n/**\n * Sa-Token 上下文处理器 [ ThreadLocal 版本 ]\n * \n * <p>\n * \t使用 [ ThreadLocal 版本 ] 上下文处理器需要在全局过滤器或者拦截器内率先调用\n * \tSaTokenContextForThreadLocalStaff.setBox(req, res, sto) 初始化上下文\n * </p>\n *\n * <p> 一般情况下你不需要直接操作此类，因为框架的 starter 集成包里已经封装了完整的上下文操作 </p>\n *\n * @author click33\n * @since 1.16.0\n */\npublic class SaTokenContextForThreadLocal implements SaTokenContext {\n\n\t@Override\n\tpublic void setContext(SaRequest req, SaResponse res, SaStorage stg) {\n\t\tSaTokenContextForThreadLocalStaff.setModelBox(req, res, stg);\n\t}\n\n\t@Override\n\tpublic void clearContext() {\n\t\tSaTokenContextForThreadLocalStaff.clearModelBox();\n\t}\n\n\t@Override\n\tpublic boolean isValid() {\n\t\treturn SaTokenContextForThreadLocalStaff.getModelBoxOrNull() != null;\n\t}\n\n\t@Override\n\tpublic SaTokenContextModelBox getModelBox() {\n\t\treturn SaTokenContextForThreadLocalStaff.getModelBox();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStaff.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenContextException;\n\n/**\n * Sa-Token 上下文处理器 [ThreadLocal 版本] ---- 对象存储器\n *\n * <p> 一般情况下你不需要直接操作此类，因为框架的 starter 集成包里已经封装了完整的上下文操作 </p>\n *\n * @author click33\n * @since 1.16.0\n */\npublic class SaTokenContextForThreadLocalStaff {\n\t\n\t/**\n\t * 基于 ThreadLocal 的 [ Box 存储器 ]\n\t */\n\tpublic static ThreadLocal<SaTokenContextModelBox> modelBoxThreadLocal = new ThreadLocal<>();\n\t\n\t/**\n\t * 初始化当前线程的 [ Box 存储器 ]\n\t * @param request {@link SaRequest}\n\t * @param response {@link SaResponse}\n\t * @param storage {@link SaStorage}\n\t */\n\tpublic static void setModelBox(SaRequest request, SaResponse response, SaStorage storage) {\n\t\tSaTokenContextModelBox bok = new SaTokenContextModelBox(request, response, storage);\n\t\tmodelBoxThreadLocal.set(bok);\n\t}\n\n\t/**\n\t * 清除当前线程的 [ Box 存储器 ]\n\t */\n\tpublic static void clearModelBox() {\n\t\tmodelBoxThreadLocal.remove();\n\t}\n\n\t/**\n\t * 获取当前线程的 [ Box 存储器 ]\n\t * @return /\n\t */\n\tpublic static SaTokenContextModelBox getModelBoxOrNull() {\n\t\treturn modelBoxThreadLocal.get();\n\t}\n\t\n\t/**\n\t * 获取当前线程的 [ Box 存储器 ], 如果为空则抛出异常\n\t * @return /\n\t */\n\tpublic static SaTokenContextModelBox getModelBox() {\n\t\tSaTokenContextModelBox box = modelBoxThreadLocal.get();\n\t\tif(box ==  null) {\n\t\t\tthrow new SaTokenContextException(\"SaTokenContext 上下文尚未初始化\").setCode(SaErrorCode.CODE_10002);\n\t\t}\n\t\treturn box;\n\t}\n\n\t/**\n\t * 在当前线程的 SaRequest 包装对象\n\t * \n\t * @return /\n\t */\n\tpublic static SaRequest getRequest() {\n\t\treturn getModelBox().getRequest();\n\t}\n\n\t/**\n\t * 在当前线程的 SaResponse 包装对象\n\t * \n\t * @return /\n\t */\n\tpublic static SaResponse getResponse() {\n\t\treturn getModelBox().getResponse();\n\t}\n\n\t/**\n\t * 在当前线程的 SaStorage 存储器包装对象\n\t * \n\t * @return /\n\t */\n\tpublic static SaStorage getStorage() {\n\t\treturn getModelBox().getStorage();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaRequestForMock.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.mock;\n\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.context.model.SaRequest;\n\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 对 SaRequest 包装类的实现（Mock 版）\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaRequestForMock implements SaRequest {\n\n\tpublic Map<String, String> parameterMap = new LinkedHashMap<>();\n\tpublic Map<String, String> headerMap = new LinkedHashMap<>();\n\tpublic Map<String, String> cookieMap = new LinkedHashMap<>();\n\tpublic String requestPath;\n\tpublic String url;\n\tpublic String method;\n\tpublic String host;\n\tpublic String forwardTo;\n\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [请求体] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getParam(String name) {\n\t\treturn parameterMap.get(name);\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数名称\n\t * @return 参数名称列表\n\t */\n\t@Override\n\tpublic Collection<String> getParamNames(){\n\t\treturn parameterMap.keySet();\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数\n\t * @return 参数列表\n\t */\n\t@Override\n\tpublic Map<String, String> getParamMap(){\n\t\treturn parameterMap;\n\t}\n\n\t/**\n\t * 在 [请求头] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getHeader(String name) {\n\t\treturn headerMap.get(name);\n\t}\n\n\t/**\n\t * 在 [Cookie作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getCookieValue(String name) {\n\t\treturn getCookieLastValue(name);\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n\t */\n\t@Override\n\tpublic String getCookieFirstValue(String name){\n\t\treturn cookieMap.get(name);\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\t@Override\n\tpublic String getCookieLastValue(String name){\n\t\treturn cookieMap.get(name);\n\t}\n\n\t/**\n\t * 返回当前请求path (不包括上下文名称) \n\t */\n\t@Override\n\tpublic String getRequestPath() {\n\t\treturn ApplicationInfo.cutPathPrefix(requestPath);\n\t}\n\n\t/**\n\t * 返回当前请求的url，例：http://xxx.com/test\n\t * @return see note\n\t */\n\tpublic String getUrl() {\n\t\treturn url;\n\t}\n\t\n\t/**\n\t * 返回当前请求的类型 \n\t */\n\t@Override\n\tpublic String getMethod() {\n\t\treturn method;\n\t}\n\n\t/**\n\t * 查询请求 host\n\t */\n\t@Override\n\tpublic String getHost() {\n\t\treturn host;\n\t}\n\n\t/**\n\t * 转发请求 \n\t */\n\t@Override\n\tpublic Object forward(String path) {\n\t\tthis.forwardTo = path;\n\t\treturn null;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaResponseForMock.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.mock;\n\nimport cn.dev33.satoken.context.model.SaResponse;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 对 SaResponse 包装类的实现（Mock 版）\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaResponseForMock implements SaResponse {\n\n\tpublic int status;\n\tpublic Map<String, String> headerMap = new LinkedHashMap<>();\n\tpublic String redirectTo;\n\n\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn null;\n\t}\n\n\t/**\n\t * 设置响应状态码 \n\t */\n\t@Override\n\tpublic SaResponse setStatus(int sc) {\n\t\tthis.status = sc;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 在响应头里写入一个值 \n\t */\n\t@Override\n\tpublic SaResponse setHeader(String name, String value) {\n\t\theaderMap.put(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在响应头里添加一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tpublic SaResponse addHeader(String name, String value) {\n\t\theaderMap.put(name, value);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 重定向 \n\t */\n\t@Override\n\tpublic Object redirect(String url) {\n\t\tthis.redirectTo = url;\n\t\treturn null;\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaStorageForMock.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.mock;\n\nimport cn.dev33.satoken.context.model.SaStorage;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 对 SaStorage 包装类的实现（Mock 版）\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaStorageForMock implements SaStorage {\n\n\tpublic Map<String, Object> dataMap = new LinkedHashMap<>();\n\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里写入一个值 \n\t */\n\t@Override\n\tpublic SaStorageForMock set(String key, Object value) {\n\t\tdataMap.put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn dataMap.get(key);\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里删除一个值 \n\t */\n\t@Override\n\tpublic SaStorageForMock delete(String key) {\n\t\tdataMap.remove(key);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/mock/SaTokenContextMockUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.mock;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\n\n/**\n * Sa-Token Mock 上下文 操作工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextMockUtil {\n\n    /**\n     * 写入 Mock 上下文\n     */\n    public static void setMockContext() {\n        SaRequestForMock request = new SaRequestForMock();\n        SaResponseForMock response = new SaResponseForMock();\n        SaStorageForMock storage = new SaStorageForMock();\n        SaManager.getSaTokenContext().setContext(request, response, storage);\n    }\n\n    /**\n     * 写入 Mock 上下文，并执行一段代码，执行完毕后清除上下文\n     *\n     * @param fun /\n     */\n    public static void setMockContext(SaFunction fun) {\n        try {\n            setMockContext();\n            fun.run();\n        } finally {\n            clearContext();\n        }\n    }\n\n    /**\n     * 写入 Mock 上下文，并执行一段代码，执行完毕后清除上下文\n     *\n     * @param fun /\n     */\n    public static <T> T setMockContext(SaRetGenericFunction<T> fun) {\n        try {\n            setMockContext();\n            return fun.run();\n        } finally {\n            clearContext();\n        }\n    }\n\n    /**\n     * 清除上下文\n     */\n    public static void clearContext() {\n        SaManager.getSaTokenContext().clearContext();\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaCookie.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.model;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.time.Instant;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeFormatter;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Cookie Model，代表一个 Cookie 应该具有的所有参数\n *\n * @author click33\n * @since 1.16.0\n */\npublic class SaCookie {\n\n\t/**\n\t * 写入响应头时使用的key\n\t */\n\tpublic static final String HEADER_NAME = \"Set-Cookie\";\n\n\t/**\n\t * 名称\n\t */\n    private String name;\n\n    /**\n     * 值\n     */\n    private String value;\n\n    /**\n     * 有效时长 （单位：秒），-1 代表为临时Cookie 浏览器关闭后自动删除\n     */\n    private int maxAge = -1;\n\n    /**\n     * 域\n     */\n    private String domain;\n\n    /**\n     * 路径\n     */\n    private String path;\n\n    /**\n     * 是否只在 https 协议下有效\n     */\n    private Boolean secure = false;\n\n    /**\n     * 是否禁止 js 操作 Cookie\n     */\n    private Boolean httpOnly = false;\n\n    /**\n     * 第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\n     */\n\tprivate String sameSite;\n\n\t// Cookie 属性参考文章：https://blog.csdn.net/fengbin2005/article/details/136544226\n\n\t/**\n\t * 额外扩展属性\n\t */\n\tprivate Map<String, String> extraAttrs = new LinkedHashMap<>();\n\n\n\t/**\n\t * 构造一个\n\t */\n\tpublic SaCookie() {\n\t}\n\n\t/**\n\t * 构造一个\n\t * @param name 名字\n\t * @param value 值\n\t */\n\tpublic SaCookie(String name, String value) {\n\t\tthis.name = name;\n\t\tthis.value = value;\n\t}\n\n\n\n\t/**\n\t * @return 名称\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 名称\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setName(String name) {\n\t\tthis.name = name;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 值\n\t */\n\tpublic String getValue() {\n\t\treturn value;\n\t}\n\n\t/**\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setValue(String value) {\n\t\tthis.value = value;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 有效时长 （单位：秒），-1 代表为临时Cookie 浏览器关闭后自动删除\n\t */\n\tpublic int getMaxAge() {\n\t\treturn maxAge;\n\t}\n\n\t/**\n\t * @param maxAge 有效时长 （单位：秒），-1 代表为临时Cookie 浏览器关闭后自动删除\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setMaxAge(int maxAge) {\n\t\tthis.maxAge = maxAge;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 域\n\t */\n\tpublic String getDomain() {\n\t\treturn domain;\n\t}\n\n\t/**\n\t * @param domain 域\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setDomain(String domain) {\n\t\tthis.domain = domain;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 路径\n\t */\n\tpublic String getPath() {\n\t\treturn path;\n\t}\n\n\t/**\n\t * @param path 路径\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setPath(String path) {\n\t\tthis.path = path;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否只在 https 协议下有效\n\t */\n\tpublic Boolean getSecure() {\n\t\treturn secure;\n\t}\n\n\t/**\n\t * @param secure 是否只在 https 协议下有效\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setSecure(Boolean secure) {\n\t\tthis.secure = secure;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否禁止 js 操作 Cookie\n\t */\n\tpublic Boolean getHttpOnly() {\n\t\treturn httpOnly;\n\t}\n\n\t/**\n\t * @param httpOnly 是否禁止 js 操作 Cookie\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setHttpOnly(Boolean httpOnly) {\n\t\tthis.httpOnly = httpOnly;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\n\t */\n\tpublic String getSameSite() {\n\t\treturn sameSite;\n\t}\n\n\t/**\n\t * @param sameSite 第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setSameSite(String sameSite) {\n\t\tthis.sameSite = sameSite;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 获取额外扩展属性\n\t */\n\tpublic Map<String, String> getExtraAttrs() {\n\t\treturn extraAttrs;\n\t}\n\n\t/**\n\t * 写入额外扩展属性\n\t * @param extraAttrs /\n\t * @return 对象自身\n\t */\n\tpublic SaCookie setExtraAttrs(Map<String, String> extraAttrs) {\n\t\tthis.extraAttrs = extraAttrs;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加扩展属性\n\t * @param name /\n\t * @param value /\n\t * @return 对象自身\n\t */\n\tpublic SaCookie addExtraAttr(String name, String value) {\n\t\tif (extraAttrs == null) {\n\t\t\textraAttrs = new LinkedHashMap<>();\n\t\t}\n\t\tthis.extraAttrs.put(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 追加扩展属性\n\t * @param name /\n\t * @return 对象自身\n\t */\n\tpublic SaCookie addExtraAttr(String name) {\n\t\treturn this.addExtraAttr(name, null);\n\t}\n\n\t/**\n\t * 移除指定扩展属性\n\t * @param name /\n\t * @return 对象自身\n\t */\n\tpublic SaCookie removeExtraAttr(String name) {\n\t\tif(extraAttrs != null) {\n\t\t\tthis.extraAttrs.remove(name);\n\t\t}\n\t\treturn this;\n\t}\n\n\n\t// toString\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaCookie [\" +\n\t\t\t\t\"name=\" + name +\n\t\t\t\t\", value=\" + value +\n\t\t\t\t\", maxAge=\" + maxAge  +\n\t\t\t\t\", domain=\" + domain +\n\t\t\t\t\", path=\" + path\n\t\t\t\t+ \", secure=\" + secure +\n\t\t\t\t\", httpOnly=\" + httpOnly +\n\t\t\t\t\", sameSite=\" + sameSite +\n\t\t\t\t\", extraAttrs=\" + extraAttrs +\n\t\t\t\t\"]\";\n\t}\n\n\t/**\n\t * 构建一下\n\t */\n\tpublic void builder() {\n\t\tif(path == null) {\n\t\t\tpath = \"/\";\n\t\t}\n\t}\n\n\t/**\n\t * 转换为响应头 Set-Cookie 参数需要的值\n\t * @return /\n\t */\n\tpublic String toHeaderValue() {\n\t\tthis.builder();\n\n\t\tif(SaFoxUtil.isEmpty(name)) {\n\t\t\tthrow new SaTokenException(\"name不能为空\").setCode(SaErrorCode.CODE_12002);\n\t\t}\n\t\tif(value != null && value.contains(\";\")) {\n\t\t\tthrow new SaTokenException(\"无效Value：\" + value).setCode(SaErrorCode.CODE_12003);\n\t\t}\n\n\t\t// example：\n\t\t// Set-Cookie: name=value; Max-Age=100000; Expires=Tue, 05-Oct-2021 20:28:17 GMT; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=Lax\n\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(name).append(\"=\").append(value);\n\n\t\tif(maxAge >= 0) {\n\t\t\t sb.append(\"; Max-Age=\").append(maxAge);\n\t\t\t String expires;\n\t\t\t if(maxAge == 0) {\n\t\t\t\t expires = Instant.EPOCH.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME);\n\t\t\t } else {\n\t\t\t\t expires = OffsetDateTime.now().plusSeconds(maxAge).format(DateTimeFormatter.RFC_1123_DATE_TIME);\n\t\t\t }\n\t\t\t sb.append(\"; Expires=\").append(expires);\n\t\t}\n\t\tif(!SaFoxUtil.isEmpty(domain)) {\n\t\t\tsb.append(\"; Domain=\").append(domain);\n\t\t}\n\t\tif(!SaFoxUtil.isEmpty(path)) {\n\t\t\tsb.append(\"; Path=\").append(path);\n\t\t}\n\t\tif(secure) {\n\t\t\tsb.append(\"; Secure\");\n\t\t}\n\t\tif(httpOnly) {\n\t\t\tsb.append(\"; HttpOnly\");\n\t\t}\n\t\tif(!SaFoxUtil.isEmpty(sameSite)) {\n\t\t\tsb.append(\"; SameSite=\").append(sameSite);\n\t\t}\n\n\t\t// 扩展属性\n\t\tif(extraAttrs != null) {\n\t\t\textraAttrs.forEach((k, v) -> {\n\t\t\t\tif(SaFoxUtil.isEmpty(v)) {\n\t\t\t\t\tsb.append(\"; \").append(k);\n\t\t\t\t} else {\n\t\t\t\t\tsb.append(\"; \").append(k).append(\"=\").append(v);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.model;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * Request 请求对象 包装类\n *\n * @author click33\n * @since 1.16.0\n */\npublic interface SaRequest {\n\t\n\t/**\n\t * 获取底层被包装的源对象\n\t * @return /\n\t */\n\tObject getSource();\n\t\n\t/**\n\t * 在 [ 请求体 ] 里获取一个参数值\n\t * @param name 键 \n\t * @return 值 \n\t */\n\tString getParam(String name);\n\n\t/**\n\t * 在 [ 请求体 ] 里获取一个参数值，值为空时返回默认值\n\t * @param name 键 \n\t * @param defaultValue 值为空时的默认值  \n\t * @return 值 \n\t */\n\tdefault String getParam(String name, String defaultValue) {\n\t\tString value = getParam(name);\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 在 [ 请求体 ] 里检测提供的参数是否为指定值\n\t * @param name 键 \n\t * @param value 值 \n\t * @return 是否相等 \n\t */\n\tdefault boolean isParam(String name, String value) {\n\t\t String paramValue = getParam(name);\n\t\t return SaFoxUtil.isNotEmpty(paramValue) && paramValue.equals(value);\n\t}\n\n\t/**\n\t * 在 [ 请求体 ] 里检测请求是否提供了指定参数\n\t * @param name 参数名称 \n\t * @return 是否提供 \n\t */\n\tdefault boolean hasParam(String name) {\n\t\t return SaFoxUtil.isNotEmpty(getParam(name));\n\t}\n\t\n\t/**\n\t * 在 [ 请求体 ] 里获取一个值 （此值必须存在，否则抛出异常 ）\n\t * @param name 键\n\t * @return 参数值 \n\t */\n\tdefault String getParamNotNull(String name) {\n\t\tString paramValue = getParam(name);\n\t\tif(SaFoxUtil.isEmpty(paramValue)) {\n\t\t\tthrow new SaTokenException(\"缺少参数：\" + name).setCode(SaErrorCode.CODE_12001);\n\t\t}\n\t\treturn paramValue;\n\t}\n\n\t/**\n\t * 获取 [ 请求体 ] 里提交的所有参数名称\n\t * @return 参数名称列表\n\t */\n\tCollection<String> getParamNames();\n\n\t/**\n\t * 获取 [ 请求体 ] 里提交的所有参数\n\t * @return 参数列表\n\t */\n\tMap<String, String> getParamMap();\n\n\t/**\n\t * 在 [ 请求头 ] 里获取一个值 \n\t * @param name 键 \n\t * @return 值 \n\t */\n\tString getHeader(String name);\n\n\t/**\n\t * 在 [ 请求头 ] 里获取一个值 \n\t * @param name 键 \n\t * @param defaultValue 值为空时的默认值  \n\t * @return 值 \n\t */\n\tdefault String getHeader(String name, String defaultValue) {\n\t\tString value = getHeader(name);\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值\n\t * @param name 键 \n\t * @return 值 \n\t */\n\tString getCookieValue(String name);\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\tString getCookieFirstValue(String name);\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\tString getCookieLastValue(String name);\n\n\t/**\n\t * 返回当前请求path (不包括上下文名称) \n\t * @return /\n\t */\n\tString getRequestPath();\n\n\t/**\n\t * 返回当前请求 path 是否为指定值\n\t * @param path path \n\t * @return /\n\t */\n\tdefault boolean isPath(String path) {\n\t\treturn getRequestPath().equals(path);\n\t}\n\n\t/**\n\t * 返回当前请求的url，不带query参数，例：http://xxx.com/test\n\t * @return /\n\t */\n\tString getUrl();\n\t\n\t/**\n\t * 返回当前请求的类型 \n\t * @return /\n\t */\n\tString getMethod();\n\n\t/**\n\t * 返回当前请求 Method 是否为指定值\n\t * @param method method\n\t * @return /\n\t */\n\tdefault boolean isMethod(String method) {\n\t\treturn getMethod().equals(method);\n\t}\n\n\t/**\n\t * 返回当前请求 Method 是否为指定值\n\t * @param method method\n\t * @return /\n\t */\n\tdefault boolean isMethod(SaHttpMethod method) {\n\t\treturn getMethod().equals(method.name());\n\t}\n\n\t/**\n\t * 查询请求 host\n\t * @return /\n\t */\n\tString getHost();\n\n\t/**\n\t * 判断此请求是否为 Ajax 异步请求\n\t * @return /\n\t */\n\tdefault boolean isAjax() {\n\t\treturn \"XMLHttpRequest\".equalsIgnoreCase(getHeader(\"X-Requested-With\")) || isParam(\"_ajax\", \"true\");\n\t}\n\n\t/**\n\t * 转发请求 \n\t * @param path 转发地址 \n\t * @return 任意值 \n\t */\n\tObject forward(String path);\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.model;\n\n/**\n * Response 响应对象 包装类\n *\n * @author click33\n * @since 1.16.0\n */\npublic interface SaResponse {\n\n\t/**\n\t * 指定前端可以获取到哪些响应头时使用的参数名 \n\t */\n\tString ACCESS_CONTROL_EXPOSE_HEADERS = \"Access-Control-Expose-Headers\";\n\t\n\t/**\n\t * 获取底层被包装的源对象\n\t * @return /\n\t */\n\tObject getSource();\n\t\n\t/**\n\t * 删除指定Cookie \n\t * @param name Cookie名称\n\t */\n\tdefault void deleteCookie(String name) {\n\t\taddCookie(name, null, null, null, 0);\n\t}\n\n\t/**\n\t * 删除指定Cookie \n\t * @param name Cookie名称\n\t * @param path Cookie 路径\n\t * @param domain Cookie 作用域\n\t */\n\tdefault void deleteCookie(String name, String path, String domain) {\n\t\taddCookie(name, null, path, domain, 0);\n\t}\n\n\t/**\n\t * 写入指定Cookie\n\t * @param name     Cookie名称\n\t * @param value    Cookie值\n\t * @param path     Cookie路径\n\t * @param domain   Cookie的作用域\n\t * @param timeout  过期时间 （秒）\n\t */\n\tdefault void addCookie(String name, String value, String path, String domain, int timeout) {\n\t\tthis.addCookie(new SaCookie(name, value).setPath(path).setDomain(domain).setMaxAge(timeout)); \n\t}\n\t\n\t/**\n\t * 写入指定Cookie\n\t * @param cookie Cookie-Model\n\t */\n\tdefault void addCookie(SaCookie cookie) {\n\t\tthis.addHeader(SaCookie.HEADER_NAME, cookie.toHeaderValue());\n\t}\n\t\n\t/**\n\t * 设置响应状态码\n\t * @param sc 响应状态码\n\t * @return 对象自身\n\t */\n\tSaResponse setStatus(int sc);\n\t\n\t/**\n\t * 在响应头里写入一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tSaResponse setHeader(String name, String value);\n\n\t/**\n\t * 在响应头里添加一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tSaResponse addHeader(String name, String value);\n\t\n\t/**\n\t * 在响应头写入 [Server] 服务器名称 \n\t * @param value 服务器名称  \n\t * @return 对象自身 \n\t */\n\tdefault SaResponse setServer(String value) {\n\t\treturn this.setHeader(\"Server\", value);\n\t}\n\n\t/**\n\t * 重定向 \n\t * @param url 重定向地址 \n\t * @return 任意值 \n\t */\n\tObject redirect(String url);\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.model;\n\nimport cn.dev33.satoken.application.SaSetValueInterface;\n\n/**\n * Storage Model，请求作用域的读取值对象。\n *\n * <p> 在一次请求范围内: 存值、取值。数据在请求结束后失效。\n * \n * @author click33\n * @since 1.16.0\n */\npublic interface SaStorage extends SaSetValueInterface {\n\n\t/**\n\t * 获取底层被包装的源对象\n\t * @return /\n\t */\n\tObject getSource();\n\n\t// ---- 实现接口存取值方法 \n\n\t/** 取值 */\n\t@Override\n\tObject get(String key);\n\n\t/** 写值 */\n\t@Override\n\tSaStorage set(String key, Object value);\n\n\t/** 删值 */\n\t@Override\n\tSaStorage delete(String key);\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaTokenContextModelBox.java",
    "content": "package cn.dev33.satoken.context.model;\n\n/**\n * Box 盒子类，用于存储 [ SaRequest、SaResponse、SaStorage ] 三个包装对象\n *\n * @author click33\n * @since 1.16.0\n */\npublic class SaTokenContextModelBox {\n\n    public SaRequest request;\n\n    public SaResponse response;\n\n    public SaStorage storage;\n\n    public SaTokenContextModelBox(SaRequest request, SaResponse response, SaStorage storage) {\n        this.request = request;\n        this.response = response;\n        this.storage = storage;\n    }\n\n    public SaRequest getRequest() {\n        return request;\n    }\n\n    public void setRequest(SaRequest request) {\n        this.request = request;\n    }\n\n    public SaResponse getResponse() {\n        return response;\n    }\n\n    public void setResponse(SaResponse response) {\n        this.response = response;\n    }\n\n    public SaStorage getStorage() {\n        return storage;\n    }\n\n    public void setStorage(SaStorage storage) {\n        this.storage = storage;\n    }\n\n    @Override\n    public String toString() {\n        return \"Box [request=\" + request + \", response=\" + response + \", storage=\" + storage + \"]\";\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/context/model/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * 因为不能确定最终运行的 web 容器属于标准 Servlet 模型还是非 Servlet 模型，特封装此包下的包装类进行对接。\n * 调用路径为：Sa-Token 功能函数 -> SaRequest 封装接口 -> SaRequest 具体实现类。\n */\npackage cn.dev33.satoken.context.model;"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\nimport cn.dev33.satoken.session.SaSession;\n\nimport java.util.List;\n\n/**\n * Sa-Token 持久层接口\n *\n * <p>\n *     此接口的不同实现类可将数据存储至不同位置，如：内存Map、Redis 等等。\n *     如果你要自定义数据存储策略，也需通过实现此接口来完成。\n * </p>\n *\n * @author click33\n * @since 1.10.0\n */\npublic interface SaTokenDao {\n\n\t/** 常量，表示一个 key 永不过期 （在一个 key 被标注为永远不过期时返回此值） */\n\tlong NEVER_EXPIRE = -1;\n\t\n\t/** 常量，表示系统中不存在这个缓存（在对不存在的 key 获取剩余存活时间时返回此值） */\n\tlong NOT_VALUE_EXPIRE = -2;\n\n\t\n\t// --------------------- 字符串读写 ---------------------\n\t\n\t/**\n\t * 获取 value，如无返空\n\t *\n\t * @param key 键名称 \n\t * @return value\n\t */\n\tString get(String key);\n\n\t/**\n\t * 写入 value，并设定存活时间（单位: 秒）\n\t *\n\t * @param key 键名称 \n\t * @param value 值 \n\t * @param timeout 数据有效期（值大于0时限时存储，值=-1时永久存储，值=0或小于等于-2时不存储）\n\t */\n\tvoid set(String key, String value, long timeout);\n\n\t/**\n\t * 更新 value （过期时间不变）\n\t * @param key 键名称 \n\t * @param value 值 \n\t */\n\tvoid update(String key, String value);\n\n\t/**\n\t * 删除 value\n\t * @param key 键名称 \n\t */\n\tvoid delete(String key);\n\t\n\t/**\n\t * 获取 value 的剩余存活时间（单位: 秒）\n\t * @param key 指定 key\n\t * @return 这个 key 的剩余存活时间\n\t */\n\tlong getTimeout(String key);\n\t\n\t/**\n\t * 修改 value 的剩余存活时间（单位: 秒）\n\t * @param key 指定 key\n\t * @param timeout 过期时间（单位: 秒）\n\t */\n\tvoid updateTimeout(String key, long timeout);\n\n\t\n\t// --------------------- 对象读写 ---------------------\n\n\t/**\n\t * 获取 Object，如无返空\n\t *\n\t * @param key 键名称\n\t * @return object\n\t */\n\tObject getObject(String key);\n\n\t/**\n\t * 获取 Object (指定反序列化类型)，如无返空\n\t *\n\t * @param key 键名称\n\t * @return object\n\t */\n\t<T> T getObject(String key, Class<T> classType);\n\n\t/**\n\t * 写入 Object，并设定存活时间 （单位: 秒）\n\t *\n\t * @param key     键名称\n\t * @param object  值\n\t * @param timeout 存活时间（值大于0时限时存储，值=-1时永久存储，值=0或小于等于-2时不存储）\n\t */\n\tvoid setObject(String key, Object object, long timeout);\n\n\t/**\n\t * 更新 Object （过期时间不变）\n\t * @param key 键名称 \n\t * @param object 值 \n\t */\n\tvoid updateObject(String key, Object object);\n\n\t/**\n\t * 删除 Object\n\t * @param key 键名称 \n\t */\n\tvoid deleteObject(String key);\n\t\n\t/**\n\t * 获取 Object 的剩余存活时间 （单位: 秒）\n\t * @param key 指定 key\n\t * @return 这个 key 的剩余存活时间\n\t */\n\tlong getObjectTimeout(String key);\n\t\n\t/**\n\t * 修改 Object 的剩余存活时间（单位: 秒）\n\t * @param key 指定 key\n\t * @param timeout 剩余存活时间\n\t */\n\tvoid updateObjectTimeout(String key, long timeout);\n\n\t\n\t// --------------------- SaSession 读写 （默认复用 Object 读写方法） ---------------------\n\n\t/**\n\t * 获取 SaSession，如无返空\n\t * @param sessionId sessionId\n\t * @return SaSession\n\t */\n\tSaSession getSession(String sessionId);\n\n\t/**\n\t * 写入 SaSession，并设定存活时间（单位: 秒）\n\t * @param session 要保存的 SaSession 对象\n\t * @param timeout 过期时间（单位: 秒）\n\t */\n\tvoid setSession(SaSession session, long timeout);\n\n\t/**\n\t * 更新 SaSession\n\t * @param session 要更新的 SaSession 对象\n\t */\n\tvoid updateSession(SaSession session);\n\t\n\t/**\n\t * 删除 SaSession\n\t * @param sessionId sessionId\n\t */\n\tvoid deleteSession(String sessionId);\n\n\t/**\n\t * 获取 SaSession 剩余存活时间（单位: 秒）\n\t * @param sessionId 指定 SaSession\n\t * @return 这个 SaSession 的剩余存活时间\n\t */\n\tlong getSessionTimeout(String sessionId);\n\t\n\t/**\n\t * 修改 SaSession 剩余存活时间（单位: 秒）\n\t * @param sessionId 指定 SaSession\n\t * @param timeout 剩余存活时间\n\t */\n\tvoid updateSessionTimeout(String sessionId, long timeout);\n\t\n\t\n\t// --------------------- 会话管理 ---------------------\n\n\t/**\n\t * 搜索数据 \n\t * @param prefix 前缀 \n\t * @param keyword 关键字 \n\t * @param start 开始处索引\n\t * @param size 获取数量  (-1代表从 start 处一直取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t * \n\t * @return 查询到的数据集合 \n\t */\n\tList<String> searchData(String prefix, String keyword, int start, int size, boolean sortType);\n\n\n\t// --------------------- 生命周期 ---------------------\n\n\t/**\n\t * 当此 SaTokenDao 实例被装载时触发\n\t */\n\tdefault void init() {\n\t}\n\n\t/**\n\t * 当此 SaTokenDao 实例被卸载时触发\n\t */\n\tdefault void destroy() {\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDaoDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\n\nimport cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject;\nimport cn.dev33.satoken.dao.timedcache.SaMapPackageForConcurrentHashMap;\nimport cn.dev33.satoken.dao.timedcache.SaTimedCache;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.List;\n\n/**\n * Sa-Token 持久层接口，默认实现类，基于 SaTimedCache - ConcurrentHashMap （内存缓存，系统重启后数据丢失）\n *\n * @author click33\n * @since 1.10.0\n */\npublic class SaTokenDaoDefaultImpl implements SaTokenDaoByStringFollowObject {\n\n\tpublic SaTimedCache timedCache = new SaTimedCache(\n\t\t\tnew SaMapPackageForConcurrentHashMap<>(),\n\t\t\tnew SaMapPackageForConcurrentHashMap<>()\n\t);\n\t\n\t// ------------------------ Object 读写操作 \n\t\n\t@Override\n\tpublic Object getObject(String key) {\n\t\treturn timedCache.getObject(key);\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getObject(String key, Class<T> classType){\n\t\treturn (T) getObject(key);\n\t}\n\n\t@Override\n\tpublic void setObject(String key, Object object, long timeout) {\n\t\ttimedCache.setObject(key, object, timeout);\n\t}\n\n\t@Override\n\tpublic void updateObject(String key, Object object) {\n\t\ttimedCache.updateObject(key, object);\n\t}\n\n\t@Override\n\tpublic void deleteObject(String key) {\n\t\ttimedCache.deleteObject(key);\n\t}\n\n\t@Override\n\tpublic long getObjectTimeout(String key) {\n\t\treturn timedCache.getObjectTimeout(key);\n\t}\n\n\t@Override\n\tpublic void updateObjectTimeout(String key, long timeout) {\n\t\ttimedCache.updateObjectTimeout(key, timeout);\n\t}\n\n\n\t// --------- 会话管理\n\n\t@Override\n\tpublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\t\treturn SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType);\n\t}\n\n\n\t// --------- 组件生命周期\n\n\t/**\n\t * 组件被安装时，开始刷新数据线程\n\t */\n\t@Override\n\tpublic void init() {\n\t\ttimedCache.initRefreshThread();\n\t}\n\n\t/**\n\t * 组件被卸载时，结束定时任务，不再定时清理过期数据\n\t */\n\t@Override\n\tpublic void destroy() {\n\t\ttimedCache.endRefreshThread();\n\t}\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByObjectFollowString.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.auto;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * SaTokenDao 次级实现，Object 读写跟随 String 读写 (推荐中间件型缓存实现 implements 此接口)\n *\n * @author click33\n * @since 1.41.0\n */\npublic interface SaTokenDaoByObjectFollowString extends SaTokenDaoBySessionFollowObject {\n\n\t// --------------------- Object 读写 ---------------------\n\n\t/**\n\t * 获取 Object，如无返空\n\t *\n\t * @param key 键名称\n\t * @return object\n\t */\n\t@Override\n\tdefault Object getObject(String key) {\n\t\tString jsonString = get(key);\n\t\treturn SaManager.getSaSerializerTemplate().stringToObject(jsonString);\n\t}\n\n\t/**\n\t * 获取 Object (指定反序列化类型)，如无返空\n\t *\n\t * @param key 键名称\n\t * @return object\n\t */\n\tdefault  <T> T getObject(String key, Class<T> classType) {\n\t\tString jsonString = get(key);\n\t\treturn SaManager.getSaSerializerTemplate().stringToObject(jsonString, classType);\n\t}\n\n\t/**\n\t * 写入 Object，并设定存活时间 （单位: 秒）\n\t *\n\t * @param key     键名称\n\t * @param object  值\n\t * @param timeout 存活时间（值大于0时限时存储，值=-1时永久存储，值=0或小于等于-2时不存储）\n\t */\n\t@Override\n\tdefault void setObject(String key, Object object, long timeout) {\n\t\tString jsonString = SaManager.getSaSerializerTemplate().objectToString(object);\n\t\tset(key, jsonString, timeout);\n\t}\n\n\t/**\n\t * 更新 Object （过期时间不变）\n\t * @param key 键名称 \n\t * @param object 值 \n\t */\n\t@Override\n\tdefault void updateObject(String key, Object object) {\n\t\tString jsonString = SaManager.getSaSerializerTemplate().objectToString(object);\n\t\tupdate(key, jsonString);\n\t}\n\n\t/**\n\t * 删除 Object\n\t * @param key 键名称 \n\t */\n\t@Override\n\tdefault void deleteObject(String key) {\n\t\tdelete(key);\n\t}\n\t\n\t/**\n\t * 获取 Object 的剩余存活时间 （单位: 秒）\n\t * @param key 指定 key\n\t * @return 这个 key 的剩余存活时间\n\t */\n\t@Override\n\tdefault long getObjectTimeout(String key) {\n\t\treturn getTimeout(key);\n\t}\n\t\n\t/**\n\t * 修改 Object 的剩余存活时间（单位: 秒）\n\t * @param key 指定 key\n\t * @param timeout 剩余存活时间\n\t */\n\t@Override\n\tdefault void updateObjectTimeout(String key, long timeout) {\n\t\tupdateTimeout(key, timeout);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoBySessionFollowObject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.auto;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\n/**\n * SaTokenDao 次级实现：SaSession 读写跟随 Object 读写\n *\n * @author click33\n * @since 1.41.0\n */\npublic interface SaTokenDaoBySessionFollowObject extends SaTokenDao {\n\n\t// --------------------- SaSession 读写 （默认复用 Object 读写方法） ---------------------\n\n\t/**\n\t * 获取 SaSession，如无返空\n\t * @param sessionId sessionId\n\t * @return SaSession\n\t */\n\tdefault SaSession getSession(String sessionId) {\n\t\treturn getObject(sessionId, SaStrategy.instance.sessionClassType);\n\t}\n\n\t/**\n\t * 写入 SaSession，并设定存活时间（单位: 秒）\n\t * @param session 要保存的 SaSession 对象\n\t * @param timeout 过期时间（单位: 秒）\n\t */\n\tdefault void setSession(SaSession session, long timeout) {\n\t\tsetObject(session.getId(), session, timeout);\n\t}\n\n\t/**\n\t * 更新 SaSession\n\t * @param session 要更新的 SaSession 对象\n\t */\n\tdefault void updateSession(SaSession session) {\n\t\tupdateObject(session.getId(), session);\n\t}\n\n\t/**\n\t * 删除 SaSession\n\t * @param sessionId sessionId\n\t */\n\tdefault void deleteSession(String sessionId) {\n\t\tdeleteObject(sessionId);\n\t}\n\n\t/**\n\t * 获取 SaSession 剩余存活时间（单位: 秒）\n\t * @param sessionId 指定 SaSession\n\t * @return 这个 SaSession 的剩余存活时间\n\t */\n\tdefault long getSessionTimeout(String sessionId) {\n\t\treturn getObjectTimeout(sessionId);\n\t}\n\n\t/**\n\t * 修改 SaSession 剩余存活时间（单位: 秒）\n\t * @param sessionId 指定 SaSession\n\t * @param timeout 剩余存活时间\n\t */\n\tdefault void updateSessionTimeout(String sessionId, long timeout) {\n\t\tupdateObjectTimeout(sessionId, timeout);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/auto/SaTokenDaoByStringFollowObject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.auto;\n\n/**\n * SaTokenDao 次级实现：String 读写跟随 Object 读写 (推荐内存型缓存实现 implements 此接口)\n *\n * @author click33\n * @since 1.41.0\n */\npublic interface SaTokenDaoByStringFollowObject extends SaTokenDaoBySessionFollowObject {\n\n\t// --------------------- String 读写 ---------------------\n\n\t@Override\n\tdefault String get(String key) {\n\t\treturn (String) getObject(key);\n\t}\n\n\t@Override\n\tdefault void set(String key, String value, long timeout) {\n\t\tsetObject(key, value, timeout);\n\t}\n\n\t@Override\n\tdefault void update(String key, String value) {\n\t\tupdateObject(key, value);\n\t}\n\n\t@Override\n\tdefault void delete(String key) {\n\t\tdeleteObject(key);\n\t}\n\n\t@Override\n\tdefault long getTimeout(String key) {\n\t\treturn getObjectTimeout(key);\n\t}\n\n\t@Override\n\tdefault void updateTimeout(String key, long timeout) {\n\t\tupdateObjectTimeout(key, timeout);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackage.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.timedcache;\n\nimport java.util.Set;\n\n/**\n * Map 包装类\n *\n * @author click33\n * @since 1.41.0\n */\npublic interface SaMapPackage<V> {\n\n\t/**\n\t * 获取底层被包装的源对象\n\t *\n\t * @return /\n\t */\n\tObject getSource();\n\n\n\t/**\n\t * 读\n\t *\n\t * @param key /\n\t * @return /\n\t */\n\tV get(String key);\n\n\t/**\n\t * 写\n\t *\n\t * @param key /\n\t * @param value /\n\t */\n\tvoid put(String key, V value);\n\n\t/**\n\t * 删\n\t * @param key /\n\t */\n\tvoid remove(String key);\n\n\t/**\n\t * 所有 key\n\t */\n\tSet<String> keySet();\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaMapPackageForConcurrentHashMap.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.timedcache;\n\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * Map 包装类 (ConcurrentHashMap 版)\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaMapPackageForConcurrentHashMap<V> implements SaMapPackage<V> {\n\n\tprivate final ConcurrentHashMap<String, V> map = new ConcurrentHashMap<String, V>();\n\n\t@Override\n\tpublic Object getSource() {\n\t\treturn map;\n\t}\n\n\t@Override\n\tpublic V get(String key) {\n\t\treturn map.get(key);\n\t}\n\n\t@Override\n\tpublic void put(String key, V value) {\n\t\tmap.put(key, value);\n\t}\n\n\t@Override\n\tpublic void remove(String key) {\n\t\tmap.remove(key);\n\t}\n\n\t@Override\n\tpublic Set<String> keySet() {\n\t\treturn map.keySet();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/dao/timedcache/SaTimedCache.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.timedcache;\n\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\n\nimport java.util.Set;\n\n/**\n * 一个定时缓存的简单实现，采用：惰性检查 + 异步循环扫描\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTimedCache {\n\n\t/**\n\t * 存储数据的集合\n\t */\n\tpublic SaMapPackage<Object> dataMap;\n\n\t/**\n\t * 存储数据过期时间的集合（单位: 毫秒）, 记录所有 key 的到期时间 （注意存储的是到期时间，不是剩余存活时间）\n\t */\n\tpublic SaMapPackage<Long> expireMap;\n\n\tpublic SaTimedCache(SaMapPackage<Object> dataMap, SaMapPackage<Long> expireMap) {\n\t\tthis.dataMap = dataMap;\n\t\tthis.expireMap = expireMap;\n\t}\n\n\t\n\t// ------------------------ 基础 API 读写操作\n\n\tpublic Object getObject(String key) {\n\t\tclearKeyByTimeout(key);\n\t\treturn dataMap.get(key);\n\t}\n\n\tpublic void setObject(String key, Object object, long timeout) {\n\t\tif(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {\n\t\t\treturn;\n\t\t}\n\t\tdataMap.put(key, object);\n\t\texpireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));\n\t}\n\n\tpublic void updateObject(String key, Object object) {\n\t\tif(getKeyTimeout(key) == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn;\n\t\t}\n\t\tdataMap.put(key, object);\n\t}\n\n\tpublic void deleteObject(String key) {\n\t\tdataMap.remove(key);\n\t\texpireMap.remove(key);\n\t}\n\n\tpublic long getObjectTimeout(String key) {\n\t\treturn getKeyTimeout(key);\n\t}\n\n\tpublic void updateObjectTimeout(String key, long timeout) {\n\t\texpireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));\n\t}\n\n\tpublic Set<String> keySet() {\n\t\treturn dataMap.keySet();\n\t}\n\n\n\t// --------- 过期时间相关操作\n\n\t/**\n\t * 如果指定的 key 已经过期，则立即清除它\n\t * @param key 指定 key\n\t */\n\tvoid clearKeyByTimeout(String key) {\n\t\tLong expirationTime = expireMap.get(key);\n\t\t// 清除条件：\n\t\t// \t\t1、数据存在。\n\t\t// \t\t2、不是 [ 永不过期 ]。\n\t\t// \t\t3、已经超过过期时间。\n\t\tif(expirationTime != null && expirationTime != SaTokenDao.NEVER_EXPIRE && expirationTime < System.currentTimeMillis()) {\n\t\t\tdataMap.remove(key);\n\t\t\texpireMap.remove(key);\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定 key 的剩余存活时间 （单位：秒）\n\t * @param key 指定 key\n\t * @return 这个 key 的剩余存活时间\n\t */\n\tlong getKeyTimeout(String key) {\n\t\t// 由于数据过期检测属于惰性扫描，很可能此时这个 key 已经是过期状态了，所以这里需要先检查一下\n\t\tclearKeyByTimeout(key);\n\n\t\t// 获取这个 key 的过期时间\n\t\tLong expire = expireMap.get(key);\n\n\t\t// 如果 expire 数据不存在，说明框架没有存储这个 key，此时返回 NOT_VALUE_EXPIRE\n\t\tif(expire == null) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\n\t\t// 如果 expire 被标注为永不过期，则返回 NEVER_EXPIRE\n\t\tif(expire == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn SaTokenDao.NEVER_EXPIRE;\n\t\t}\n\n\t\t// ---- 代码至此，说明这个 key 是有过期时间的，且未过期，那么：\n\n\t\t// 计算剩余时间并返回 （过期时间戳 - 当前时间戳） / 1000 转秒\n\t\tlong timeout = (expire - System.currentTimeMillis()) / 1000;\n\n\t\t// 小于零时，视为不存在 \n\t\tif(timeout < 0) {\n\t\t\tdataMap.remove(key);\n\t\t\texpireMap.remove(key);\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\t\treturn timeout;\n\t}\n\n\t// --------- 定时清理过期数据\n\t\n\t/**\n\t * 执行数据清理的线程引用\n\t */\n\tpublic Thread refreshThread;\n\t\n\t/**\n\t * 是否继续执行数据清理的线程标记\n\t */\n\tpublic volatile boolean refreshFlag;\n\n\t/**\n\t * 清理所有已经过期的 key\n\t */\n\tpublic void refreshDataMap() {\n\t\tfor (String s : expireMap.keySet()) {\n\t\t\tclearKeyByTimeout(s);\n\t\t}\n\t}\n\t\n\t/**\n\t * 初始化定时任务，定时清理过期数据\n\t */\n\tpublic void initRefreshThread() {\n\n\t\t// 如果开发者配置了 <=0 的值，则不启动定时清理\n\t\tif(SaManager.getConfig().getDataRefreshPeriod() <= 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 启动定时刷新\n\t\tthis.refreshFlag = true;\n\t\tthis.refreshThread = new Thread(() -> {\n\t\t\tfor (;;) {\n\t\t\t\ttry {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// 如果已经被标记为结束\n\t\t\t\t\t\tif( ! refreshFlag) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// 执行清理\n\t\t\t\t\t\trefreshDataMap(); \n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\te.printStackTrace();\n\t\t\t\t\t}\n\t\t\t\t\t// 休眠N秒 \n\t\t\t\t\tint dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod();\n\t\t\t\t\tif(dataRefreshPeriod <= 0) {\n\t\t\t\t\t\tdataRefreshPeriod = 1;\n\t\t\t\t\t}\n\t\t\t\t\tThread.sleep(dataRefreshPeriod * 1000L);\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis.refreshThread.start();\n\t}\n\n\t/**\n\t * 结束定时任务\n\t */\n\tpublic void endRefreshThread() {\n\t\tthis.refreshFlag = false;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.error;\n\n/**\n * 定义所有异常细分状态码 \n * \n * @author click33\n * @since 1.33.0\n */\npublic interface SaErrorCode {\n\t\n\t/** 代表这个异常在抛出时未指定异常细分状态码 */\n\tint CODE_UNDEFINED = -1;\n\n\t// ------------ \n\t\n\t/** 未能获取有效的上下文处理器 */\n\tint CODE_10001 = 10001;\n\n\t/** 未能获取有效的上下文 */\n\tint CODE_10002 = 10002;\n\n\t/** JSON 转换器未实现 */\n\tint CODE_10003 = 10003;\n\n\t/** HTTP 请求处理器未实现 */\n\tint CODE_10004 = 10004;\n\n\t/** 未能从全局 StpLogic 集合中找到对应 type 的 StpLogic */\n\tint CODE_10011 = 10011;\n\n\t/** 指定的配置文件加载失败 */\n\tint CODE_10021 = 10021;\n\n\t/** 配置文件属性无法正常读取 */\n\tint CODE_10022 = 10022;\n\n\t/** 重置的侦听器集合不可以为空 */\n\tint CODE_10031 = 10031;\n\n\t/** 注册的侦听器不可以为空 */\n\tint CODE_10032 = 10032;\n\n\t// 1030x core模块\n\n\t/** 提供的 Same-Token 是无效的 */\n\tint CODE_10301 = 10301;\n\n\t/** 表示未能通过 Http Basic 认证校验 */\n\tint CODE_10311 = 10311;\n\n\t/** 表示未能通过 Http Digest 认证校验 */\n\tint CODE_10312 = 10312;\n\n\t/** 提供的 HttpMethod 是无效的 */\n\tint CODE_10321 = 10321;\n\n\t// 1100x StpLogic\n\n\t/** 未能读取到有效Token */\n\tint CODE_11001 = 11001;\n\n\t/** 登录时的账号id值为空 */\n\tint CODE_11002 = 11002;\n\n\t/** 更改 Token 指向的 账号Id 时，账号Id值为空 */\n\tint CODE_11003 = 11003;\n\n\t/** 登录失败：当前账号已在其它客户端登录 */\n\tint CODE_11004 = 11004;\n\n\t/** 未能读取到有效Token */\n\tint CODE_11011 = 11011;\n\t\n\t/** Token无效 */\n\tint CODE_11012 = 11012;\n\n\t/** Token已过期 */\n\tint CODE_11013 = 11013;\n\t\n\t/** Token已被顶下线 */\n\tint CODE_11014 = 11014;\n\n\t/** Token已被踢下线 */\n\tint CODE_11015 = 11015;\n\n\t/** Token已被冻结 */\n\tint CODE_11016 = 11016;\n\n\t/** 前端未按照指定的前缀提交 token */\n\tint CODE_11017 = 11017;\n\n\t/** 在未集成 sa-token-jwt 插件时调用 getExtra() 抛出异常 */\n\tint CODE_11031 = 11031;\n\n\t/** 缺少指定的角色 */\n\tint CODE_11041 = 11041;\n\n\t/** 缺少指定的权限 */\n\tint CODE_11051 = 11051;\n\n\t/** 当前账号未通过服务封禁校验 */\n\tint CODE_11061 = 11061;\n\n\t/** 提供要解禁的账号无效 */\n\tint CODE_11062 = 11062;\n\n\t/** 提供要解禁的服务无效 */\n\tint CODE_11063 = 11063;\n\n\t/** 提供要解禁的等级无效 */\n\tint CODE_11064 = 11064;\n\n\t/** 二级认证校验未通过 */\n\tint CODE_11071 = 11071;\n\n\t/** 获取 SaSession 时提供的 SessionId 为空 */\n\tint CODE_11072 = 11072;\n\n\t/** 获取 Token-Session 时提供的 token 为空 */\n\tint CODE_11073 = 11073;\n\n\t/** 获取 Token-Session 时提供的 token 为无效 token */\n\tint CODE_11074 = 11074;\n\n\n\t// ------------ \n\t\n\t/** 请求中缺少指定的参数 */\n\tint CODE_12001 = 12001;\n\n\t/** 构建 Cookie 时缺少 name 参数 */\n\tint CODE_12002 = 12002;\n\n\t/** 构建 Cookie 时缺少 value 参数 */\n\tint CODE_12003 = 12003;\n\n\t// ------------ \n\n\t/** Base64 编码异常 */\n\tint CODE_12101 = 12101;\n\n\t/** Base64 解码异常 */\n\tint CODE_12102 = 12102;\n\n\t/** URL 编码异常 */\n\tint CODE_12103 = 12103;\n\n\t/** URL 解码异常 */\n\tint CODE_12104 = 12104;\n\n\t/** md5 加密异常 */\n\tint CODE_12111 = 12111;\n\n\t/** sha1 加密异常 */\n\tint CODE_12112 = 12112;\n\n\t/** sha256 加密异常 */\n\tint CODE_12113 = 12113;\n\n\t/** sha384 加密异常 */\n\tint CODE_121131 = 121131;\n\n\t/** sha512 加密异常 */\n\tint CODE_121132 = 121132;\n\n\t/** AES 加密异常 */\n\tint CODE_12114 = 12114;\n\n\t/** AES 解密异常 */\n\tint CODE_12115 = 12115;\n\n\t/** RSA 公钥加密异常 */\n\tint CODE_12116 = 12116;\n\n\t/** RSA 私钥加密异常 */\n\tint CODE_12117 = 12117;\n\n\t/** RSA 公钥解密异常 */\n\tint CODE_12118 = 12118;\n\n\t/** RSA 私钥解密异常 */\n\tint CODE_12119 = 12119;\n\n\n\t// ------------\n\n\t/** 未实现具体的路由匹配策略 */\n\tint CODE_12401 = 12401;\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/ApiDisabledException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表 API 已被禁用\n *\n * <p> 一般在 API 不合适调用的时候抛出，例如在集成 jwt 模块后调用数据持久化相关方法 </p>\n *\n * @author click33\n * @since 1.28.0\n */\npublic class ApiDisabledException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130133L;\n\t\n\t/** 异常提示语 */\n\tpublic static final String BE_MESSAGE = \"this api is disabled\";\n\n\t/**\n\t * 一个异常：代表 API 已被禁用  \n\t */\n\tpublic ApiDisabledException() {\n\t\tsuper(BE_MESSAGE);\n\t}\n\n\t/**\n\t * 一个异常：代表 API 已被禁用  \n\t * @param message 异常描述 \n\t */\n\tpublic ApiDisabledException(String message) {\n\t\tsuper(message);\n\t}\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/BackResultException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表停止匹配，直接退出，向前端输出结果 （框架内部专属异常，一般情况下开发者无需关注）\n * \n * @author click33\n * @since 1.21.0\n */\npublic class BackResultException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130143L;\n\n\t/**\n\t * 要输出的结果 \n\t */\n\tpublic Object result;\n\t\n\t/**\n\t * 构造 \n\t * @param result 要输出的结果 \n\t */\n\tpublic BackResultException(Object result) {\n\t\tsuper(String.valueOf(result));\n\t\tthis.result = result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/DisableServiceException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表指定账号的指定服务已被封禁\n * \n * @author click33\n * @since 1.31.0\n */\npublic class DisableServiceException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130143L;\n\n\t/** 异常标记值（已更改为 SaTokenConsts.DEFAULT_DISABLE_LEVEL） */\n\t@Deprecated\n\tpublic static final String BE_VALUE = \"disable\";\n\t\n\t/** 异常提示语 */\n\tpublic static final String BE_MESSAGE = \"此账号已被禁止访问服务：\";\n\n\t/**\n\t * 账号类型 \n\t */\n\tprivate final String loginType;\n\n\t/**\n\t * 被封禁的账号id \n\t */\n\tprivate final Object loginId;\n\n\t/**\n\t * 具体被封禁的服务 \n\t */\n\tprivate final String service;\n\n\t/**\n\t * 具体被封禁的等级 \n\t */\n\tprivate final int level;\n\n\t/**\n\t * 校验时要求低于的等级 \n\t */\n\tprivate final int limitLevel;\n\t\n\t/**\n\t * 封禁剩余时间，单位：秒 \n\t */\n\tprivate final long disableTime;\n\n\t/**\n\t * 获取：账号类型 \n\t * \n\t * @return / \n\t */\n\tpublic String getLoginType() {\n\t\treturn loginType;\n\t}\n\n\t/**\n\t * 获取: 被封禁的账号id \n\t * \n\t * @return / \n\t */\n\tpublic Object getLoginId() {\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * 获取: 被封禁的服务 \n\t * \n\t * @return / \n\t */\n\tpublic Object getService() {\n\t\treturn service;\n\t}\n\n\t/**\n\t * 获取: 被封禁的等级 \n\t * \n\t * @return / \n\t */\n\tpublic int getLevel() {\n\t\treturn level;\n\t}\n\n\t/**\n\t * 获取: 校验时要求低于的等级 \n\t * \n\t * @return / \n\t */\n\tpublic int getLimitLevel() {\n\t\treturn limitLevel;\n\t}\n\t\n\t/**\n\t * 获取: 封禁剩余时间，单位：秒\n\t * @return / \n\t */\n\tpublic long getDisableTime() {\n\t\treturn disableTime;\n\t}\n\t\n\t/**\n\t * 一个异常：代表指定账号指定服务已被封禁 \n\t * \n\t * @param loginType 账号类型\n\t * @param loginId  被封禁的账号id \n\t * @param service  具体封禁的服务 \n\t * @param level 被封禁的等级 \n\t * @param limitLevel 校验时要求低于的等级 \n\t * @param disableTime 封禁剩余时间，单位：秒 \n\t */\n\tpublic DisableServiceException(String loginType, Object loginId, String service, int level, int limitLevel, long disableTime) {\n\t\tsuper(BE_MESSAGE + service);\n\t\tthis.loginId = loginId;\n\t\tthis.loginType = loginType;\n\t\tthis.service = service;\n\t\tthis.level = level;\n\t\tthis.limitLevel = limitLevel;\n\t\tthis.disableTime = disableTime;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/FirewallCheckException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表防火墙检验未通过\n * \n * @author click33\n * @since 1.41.0\n */\npublic class FirewallCheckException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 8243974276159004739L;\n\n\tpublic FirewallCheckException(String message) {\n\t\tsuper(message);\n\t}\n\n\tpublic FirewallCheckException(Throwable e) {\n\t\tsuper(e);\n\t}\n\n\tpublic FirewallCheckException(String message, Throwable e) {\n\t\tsuper(message, e);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/InvalidContextException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表框架未能获取有效的上下文\n * <h1>已过期：请更名为 SaTokenContextException 用法不变，未来版本将彻底删除此类</h1>\n * \n * @author click33\n * @since 1.33.0\n */\n@Deprecated\npublic class InvalidContextException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\n\t/**\n\t * 一个异常：代表框架未能获取有效的上下文\n\t * @param message 异常描述\n\t */\n\tpublic InvalidContextException(String message) {\n\t\tsuper(message);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpBasicAuthException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表会话未能通过 Http Basic 认证校验\n *\n * @author click33\n * @since 1.26.0\n */\npublic class NotHttpBasicAuthException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\t\n\t/** 异常提示语 */\n\tpublic static final String BE_MESSAGE = \"no basic auth\";\n\n\t/**\n\t * 一个异常：代表会话未通过 Http Basic 认证 \n\t */\n\tpublic NotHttpBasicAuthException() {\n\t\tsuper(BE_MESSAGE);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表会话未能通过 Http Digest 认证校验\n *\n * @author click33\n * @since 1.38.0\n */\npublic class NotHttpDigestAuthException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\n\t/** 异常提示语 */\n\tpublic static final String BE_MESSAGE = \"no http digest auth\";\n\n\t/**\n\t * 一个异常：代表会话未通过 Http Digest 认证\n\t */\n\tpublic NotHttpDigestAuthException() {\n\t\tsuper(BE_MESSAGE);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotImplException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表组件或方法未被提供有效的实现\n * \n * @author click33\n * @since 1.33.0\n */\npublic class NotImplException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\n\t/**\n\t * 一个异常：代表组件或方法未被提供有效的实现\n\t * @param message 异常描述 \n\t */\n\tpublic NotImplException(String message) {\n\t\tsuper(message);\n\t}\n\n}\n\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotLoginException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 一个异常：代表会话未能通过登录认证校验\n *\n * @author click33\n * @since 1.10.0\n */\npublic class NotLoginException extends SaTokenException {\n\t\n\t/**\n\t * 序列化版本号 \n\t */\n\tprivate static final long serialVersionUID = 6806129545290130142L;\n\t\n\t\n\t// ------------------- 异常类型常量  -------------------- \n\t\n\t/*\n\t * 这里简述一下为什么要把常量设计为String类型 \n\t * 因为loginId刚取出的时候类型为String，为了避免两者相比较时不必要的类型转换带来的性能消耗，故在此直接将常量类型设计为String \n\t */\n\t\n\t/** 表示未能读取到有效 token */\n\tpublic static final String NOT_TOKEN = \"-1\";\n\tpublic static final String NOT_TOKEN_MESSAGE = \"未能读取到有效 token\";\n\t\n\t/** 表示 token 无效 */\n\tpublic static final String INVALID_TOKEN = \"-2\";\n\tpublic static final String INVALID_TOKEN_MESSAGE = \"token 无效\";\n\t\n\t/** 表示 token 已过期 */\n\tpublic static final String TOKEN_TIMEOUT = \"-3\";\n\tpublic static final String TOKEN_TIMEOUT_MESSAGE = \"token 已过期\";\n\t\n\t/** 表示 token 已被顶下线 */\n\tpublic static final String BE_REPLACED = \"-4\";\n\tpublic static final String BE_REPLACED_MESSAGE = \"token 已被顶下线\";\n\t\n\t/** 表示 token 已被踢下线 */\n\tpublic static final String KICK_OUT = \"-5\";\n\tpublic static final String KICK_OUT_MESSAGE = \"token 已被踢下线\";\n\n\t/** 表示 token 已被冻结 */\n\tpublic static final String TOKEN_FREEZE = \"-6\";\n\tpublic static final String TOKEN_FREEZE_MESSAGE = \"token 已被冻结\";\n\n\t/** 表示 未按照指定前缀提交 token */\n\tpublic static final String NO_PREFIX = \"-7\";\n\tpublic static final String NO_PREFIX_MESSAGE = \"未按照指定前缀提交 token\";\n\n\t/** 默认的提示语 */\n\tpublic static final String DEFAULT_MESSAGE = \"当前会话未登录\";\n\t\n\t\n\t/** \n\t * 代表异常 token 的标志集合\n\t */\n\tpublic static final List<String> ABNORMAL_LIST =\n\t\t\tArrays.asList(NOT_TOKEN, INVALID_TOKEN, TOKEN_TIMEOUT, BE_REPLACED, KICK_OUT, TOKEN_FREEZE, NO_PREFIX);\n\t\n\n\t/**\n\t * 异常类型 \n\t */\n\tprivate final String type;\n\t\n\t/**\n\t * 获取异常类型 \n\t * @return 异常类型 \n\t */\n\tpublic String getType() {\n\t\treturn type;\n\t}\n\t\n\t\n\t/**\n\t * 账号类型 \n\t */\n\tprivate final String loginType;\n\t\n\t/** \n\t * 获得账号类型 \n\t * @return 账号类型\n\t */\n\tpublic String getLoginType() {\n\t\treturn loginType;\n\t}\n\n\t/**\n\t * 构造方法创建一个\n\t * @param message 异常消息\n\t * @param loginType 账号类型\n\t * @param type 类型\n\t */\n\tpublic NotLoginException(String message, String loginType, String type) {\n\t\tsuper(message);\n        this.loginType = loginType;\n        this.type = type;\n    }\n\n\t/**\n\t * 静态方法构建一个 NotLoginException\n\t * @param loginType 账号类型\n\t * @param type 未登录场景值\n\t * @param message 异常描述信息\n\t * @param token 引起异常的 token 值，可不填，如果填了会拼接到异常描述信息后面\n\t * @return 构建完毕的异常对象\n\t */\n\tpublic static NotLoginException newInstance(String loginType, String type, String message, String token) {\n\t\tif(SaFoxUtil.isNotEmpty(token)) {\n\t\t\tmessage = message + \"：\" + token;\n\t\t}\n\t\treturn new NotLoginException(message, loginType, type);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotPermissionException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 一个异常：代表会话未能通过权限认证校验\n * \n * @author click33\n * @since 1.10.0\n */\npublic class NotPermissionException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号 \n\t */\n\tprivate static final long serialVersionUID = 6806129545290130141L;\n\n\t/** 权限码 */\n\tprivate final String permission;\n\n\t/**\n\t * @return 获得具体缺少的权限码\n\t */\n\tpublic String getPermission() {\n\t\treturn permission;\n\t}\n\n\t/**\n\t * 账号类型\n\t */\n\tprivate final String loginType;\n\n\t/**\n\t * 获得账号类型\n\t * \n\t * @return 账号类型\n\t */\n\tpublic String getLoginType() {\n\t\treturn loginType;\n\t}\n\n\tpublic NotPermissionException(String permission) {\n\t\tthis(permission, StpUtil.stpLogic.loginType);\n\t}\n\n\tpublic NotPermissionException(String permission, String loginType) {\n\t\tsuper(\"无此权限：\" + permission);\n\t\tthis.permission = permission;\n\t\tthis.loginType = loginType;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotRoleException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 一个异常：代表会话未能通过角色认证校验\n * \n * @author click33\n * @since 1.10.0\n */\npublic class NotRoleException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号 \n\t */\n\tprivate static final long serialVersionUID = 8243974276159004739L;\n\n\t/** 角色标识 */\n\tprivate final String role;\n\n\t/**\n\t * @return 获得角色标识\n\t */\n\tpublic String getRole() {\n\t\treturn role;\n\t}\n\n\t/**\n\t * 账号类型\n\t */\n\tprivate final String loginType;\n\n\t/**\n\t * 获得账号类型\n\t * \n\t * @return 账号类型\n\t */\n\tpublic String getLoginType() {\n\t\treturn loginType;\n\t}\n\n\tpublic NotRoleException(String role) {\n\t\tthis(role, StpUtil.stpLogic.loginType);\n\t}\n\n\tpublic NotRoleException(String role, String loginType) {\n\t\tsuper(\"无此角色：\" + role);\n\t\tthis.role = role;\n\t\tthis.loginType = loginType;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotSafeException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表会话未能通过二级认证校验 \n * \n * @author click33\n * @since 1.21.0\n */\npublic class NotSafeException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\t\n\t/** 异常提示语 */\n\tpublic static final String BE_MESSAGE = \"二级认证校验失败\";\n\n\t/**\n\t * 账号类型 \n\t */\n\tprivate final String loginType;\n\n\t/**\n\t * 未通过校验的 Token 值 \n\t */\n\tprivate final Object tokenValue;\n\n\t/**\n\t * 未通过校验的服务 \n\t */\n\tprivate final String service;\n\n\t/**\n\t * 获取：账号类型 \n\t * \n\t * @return / \n\t */\n\tpublic String getLoginType() {\n\t\treturn loginType;\n\t}\n\n\t/**\n\t * 获取: 未通过校验的 Token 值  \n\t * \n\t * @return / \n\t */\n\tpublic Object getTokenValue() {\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * 获取: 未通过校验的服务 \n\t * \n\t * @return / \n\t */\n\tpublic Object getService() {\n\t\treturn service;\n\t}\n\n\t/**\n\t * 一个异常：代表会话未能通过二级认证校验\n\t * \n\t * @param loginType 账号类型\n\t * @param tokenValue  未通过校验的 Token 值  \n\t * @param service  未通过校验的服务 \n\t */\n\tpublic NotSafeException(String loginType, String tokenValue, String service) {\n\t\tsuper(BE_MESSAGE + \"：\" + service);\n\t\tthis.tokenValue = tokenValue;\n\t\tthis.loginType = loginType;\n\t\tthis.service = service;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/NotWebContextException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表当前不是 Web 上下文，无法调用某个 API\n * \n * @author click33\n * @since 1.33.0\n */\npublic class NotWebContextException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\n\t/**\n\t * 一个异常：代表当前不是 Web 上下文，无法调用某个 API\n\t * @param message 异常描述 \n\t */\n\tpublic NotWebContextException(String message) {\n\t\tsuper(message);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/RequestPathInvalidException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表请求 path 无效或非法\n * \n * @author click33\n * @since 1.37.0\n */\npublic class RequestPathInvalidException extends FirewallCheckException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 8243974276159004739L;\n\n\t/** 具体无效的 path */\n\tprivate final String path;\n\n\t/**\n\t * @return 具体无效的 path\n\t */\n\tpublic String getPath() {\n\t\treturn path;\n\t}\n\n\tpublic RequestPathInvalidException(String message, String path) {\n\t\tsuper(message);\n\t\tthis.path = path;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/SaJsonConvertException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表 JSON 转换失败 \n * \n * @author click33\n * @since 1.30.0\n */\npublic class SaJsonConvertException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290134144L;\n\t\n\t/**\n\t * 一个异常：代表 JSON 转换失败 \n\t * @param cause 异常对象\n\t */\n\tpublic SaJsonConvertException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenContextException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\nimport java.io.Serializable;\n\n/**\n * 一个异常：代表框架未能获取有效的上下文\n * \n * @author click33\n * @since 1.33.0\n */\npublic class SaTokenContextException extends InvalidContextException implements Serializable {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\n\t/**\n\t * 一个异常：代表框架未能获取有效的上下文\n\t * @param message 异常描述 \n\t */\n\tpublic SaTokenContextException(String message) {\n\t\tsuper(message);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * Sa-Token 框架内部逻辑发生错误抛出的异常\n *\n * <p> 框架其它异常均继承自此类，开发者可通过捕获此异常来捕获框架内部抛出的所有异常 </p>\n * \n * @author click33\n * @since 1.10.0\n */\npublic class SaTokenException extends RuntimeException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130132L;\n\n\t/**\n\t * 异常细分状态码 \n\t */\n\tprivate int code = SaErrorCode.CODE_UNDEFINED;\n\n\t/**\n\t * 构建一个异常\n\t * \n\t * @param code 异常细分状态码 \n\t */\n\tpublic SaTokenException(int code) {\n\t\tsuper();\n\t\tthis.code = code;\n\t}\n\n\n\t/**\n\t * 构建一个异常\n\t * \n\t * @param message 异常描述信息\n\t */\n\tpublic SaTokenException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 构建一个异常\n\t * \n\t * @param code 异常细分状态码 \n\t * @param message 异常信息\n\t */\n\tpublic SaTokenException(int code, String message) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n\n\t/**\n\t * 构建一个异常\n\t * \n\t * @param cause 异常对象\n\t */\n\tpublic SaTokenException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 构建一个异常\n\t * \n\t * @param message 异常信息\n\t * @param cause 异常对象\n\t */\n\tpublic SaTokenException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\t/**\n\t * 获取异常细分状态码\n\t * @return 异常细分状态码\n\t */\n\tpublic int getCode() {\n\t\treturn code;\n\t}\n\n\t/**\n\t * 写入异常细分状态码 \n\t * @param code 异常细分状态码\n\t * @return 对象自身 \n\t */\n\tpublic SaTokenException setCode(int code) {\n\t\tthis.code = code;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 断言 flag 不为 true，否则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息\n\t */\n\tpublic static void notTrue(boolean flag, String message) {\n\t\tnotTrue(flag, message, SaErrorCode.CODE_UNDEFINED);\n\t}\n\n\t/**\n\t * 断言 flag 不为 true，否则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分状态码 \n\t */\n\tpublic static void notTrue(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaTokenException(message).setCode(code);\n\t\t}\n\t}\n\n\t/**\n\t * 断言 value 不为空，否则抛出 message 异常\n\t * @param value 值\n\t * @param message 异常信息\n\t */\n\tpublic static void notEmpty(Object value, String message) {\n\t\tnotEmpty(value, message, SaErrorCode.CODE_UNDEFINED);\n\t}\n\n\t/**\n\t * 断言 value 不为空，否则抛出 message 异常\n\t * @param value 值 \n\t * @param message 异常信息 \n\t * @param code 异常细分状态码 \n\t */\n\tpublic static void notEmpty(Object value, String message, int code) {\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\tthrow new SaTokenException(message).setCode(code);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenPluginException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表插件安装过程中出现异常\n *\n * @author click33\n * @since 1.28.0\n */\npublic class SaTokenPluginException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130131L;\n\n\t/**\n\t * 一个异常：代表插件安装过程中出现异常\n\t * @param message 异常描述\n\t */\n\tpublic SaTokenPluginException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 一个异常：代表插件安装过程中出现异常\n\t *\n\t * @param cause 异常对象\n\t */\n\tpublic SaTokenPluginException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表插件安装过程中出现异常\n\t *\n\t * @param message 异常描述\n\t * @param cause 异常对象\n\t */\n\tpublic SaTokenPluginException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/SameTokenInvalidException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表 Same-Token 校验未通过\n * \n * @author click33\n * @since 1.32.0\n */\npublic class SameTokenInvalidException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\t\n\t/**\n\t * 一个异常：代表 Same-Token 校验未通过\n\t * @param message 异常描述 \n\t */\n\tpublic SameTokenInvalidException(String message) {\n\t\tsuper(message);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/StopMatchException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表停止路由匹配，进入 Controller （框架内部专属异常，一般情况下开发者无需关注）\n * \n * @author click33\n * @since 1.20.0\n */\npublic class StopMatchException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130143L;\n\n\t/**\n\t * 构造 \n\t */\n\tpublic StopMatchException() {\n\t\tsuper(\"stop match\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/exception/TotpAuthException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.exception;\n\n/**\n * 一个异常：代表 TOTP 校验未通过\n *\n * @author click33\n * @since 1.41.0\n */\npublic class TotpAuthException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\n\t/** 异常提示语 */\n\tpublic static final String BE_MESSAGE = \"totp check fail\";\n\n\t/**\n\t * 一个异常：代表会话未通过 Totp 校验\n\t */\n\tpublic TotpAuthException() {\n\t\tsuper(BE_MESSAGE);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport java.util.List;\n\n/**\n * Sa-Token 过滤器接口，为不同版本的过滤器：\n *  1、封装共同代码。\n *  2、定义统一的行为接口。\n *\n * @author click33\n * @since 1.34.0\n */\npublic interface SaFilter {\n\n    // ------------------------ 设置此过滤器 拦截 & 放行 的路由\n\n    /**\n     * 添加 [ 拦截路由 ]\n     * @param paths 路由\n     * @return 对象自身\n     */\n    SaFilter addInclude(String... paths);\n\n    /**\n     * 添加 [ 放行路由 ]\n     * @param paths 路由\n     * @return 对象自身\n     */\n    SaFilter addExclude(String... paths);\n\n    /**\n     * 写入 [ 拦截路由 ] 集合\n     * @param pathList 路由集合\n     * @return 对象自身\n     */\n    SaFilter setIncludeList(List<String> pathList);\n\n    /**\n     * 写入 [ 放行路由 ] 集合\n     * @param pathList 路由集合\n     * @return 对象自身\n     */\n    SaFilter setExcludeList(List<String> pathList);\n\n\n    // ------------------------ 钩子函数\n\n    /**\n     * 写入[ 认证函数 ]: 每次请求执行\n     * @param auth see note\n     * @return 对象自身\n     */\n    SaFilter setAuth(SaFilterAuthStrategy auth);\n\n    /**\n     * 写入[ 异常处理函数 ]：每次[ 认证函数 ]发生异常时执行此函数\n     * @param error see note\n     * @return 对象自身\n     */\n    SaFilter setError(SaFilterErrorStrategy error);\n\n    /**\n     * 写入[ 前置函数 ]：在每次[ 认证函数 ]之前执行。\n     *      <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n     * @param beforeAuth /\n     * @return 对象自身\n     */\n    SaFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth);\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterAuthStrategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\n/**\n * Sa-Token 全局过滤器 - 认证策略封装，方便 lambda 表达式风格调用\n *\n * @author click33\n * @since 1.17.0\n */\n@FunctionalInterface\npublic interface SaFilterAuthStrategy {\n\t\n\t/**\n\t * 执行方法 \n\t * @param obj 无含义参数，留作扩展\n\t */\n\tvoid run(Object obj);\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/filter/SaFilterErrorStrategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\n/**\n * Sa-Token 全局过滤器 - 异常处理策略封装，方便 lambda 表达式风格调用\n *\n * <p> 此方法的返回值将在 toString() 后返回给前端，如果你要返回 JSON 数据，需要在返回前自行序列化为 JSON 字符串 </p>\n *\n * @author click33\n * @since 1.16.0\n */\n@FunctionalInterface\npublic interface SaFilterErrorStrategy {\n\t\n\t/**\n\t * 执行方法 \n\t * @param e 异常对象\n\t * @return 输出对象，此返回值将在 toString() 后返回给前端，如果你要返回 JSON 数据，需要在返回前自行序列化为 JSON 字符串\n\t */\n\tObject run(Throwable e);\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/IsRunFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\n/**\n * lambda 表达式辅助封装：根据 Boolean 变量，决定是否执行一个函数\n * \n * @author click33\n * @since 1.13.0\n */\npublic class IsRunFunction {\n\n\t/**\n\t * 变量 \n\t */\n\tpublic final Boolean isRun;\n\n\t/**\n\t * 设定一个变量，如果为true，则执行exe函数\n\t * \n\t * @param isRun 变量\n\t */\n\tpublic IsRunFunction(boolean isRun) {\n\t\tthis.isRun = isRun;\n\t}\n\n\t/**\n\t * 当 isRun == true 时执行此函数\n\t * @param function 函数\n\t * @return 对象自身\n\t */\n\tpublic IsRunFunction exe(SaFunction function) {\n\t\tif (isRun) {\n\t\t\tfunction.run();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 当 isRun == false 时执行此函数\n\t * @param function 函数\n\t * @return 对象自身\n\t */\n\tpublic IsRunFunction noExe(SaFunction function) {\n\t\tif (!isRun) {\n\t\t\tfunction.run();\n\t\t}\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/SaFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\n/**\n * 无形参、无返回值的函数式接口，方便开发者进行 lambda 表达式风格调用\n * \n * @author click33\n * @since 1.13.0\n */\n@FunctionalInterface\npublic interface SaFunction {\n\n\t/**\n\t * 执行的方法\n\t */\n\tvoid run();\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\n/**\n * 单形参、无返回值的函数式接口，方便开发者进行 lambda 表达式风格调用\n *\n * @author click33\n * @since 1.27.0\n */\n@FunctionalInterface\npublic interface SaParamFunction<T> {\n\t\n\t/**\n\t * 执行的方法 \n\t * @param r 传入的参数 \n\t */\n\tvoid run(T r);\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/SaParamRetFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\n/**\n * 单形参、有返回值的函数式接口，方便开发者进行 lambda 表达式风格调用\n *\n * @author click33\n * @since 1.27.0\n */\n@FunctionalInterface\npublic interface SaParamRetFunction<T, R> {\n\t\n\t/**\n\t * 执行的方法 \n\t * @param param 传入的参数 \n\t * @return 返回值 \n\t */\n\tR run(T param);\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\n/**\n * 无形参、有返回值的函数式接口，方便开发者进行 lambda 表达式风格调用\n *\n * @author click33\n * @since 1.20.0\n */\n@FunctionalInterface\npublic interface SaRetFunction {\n\t\n\t/**\n\t * 执行的方法 \n\t * @return 返回值 \n\t */\n\tObject run();\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRetGenericFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\n/**\n * 无形参、有返回值(泛型)的函数式接口，方便开发者进行 lambda 表达式风格调用\n *\n * @author click33\n * @since 1.42.0\n */\n@FunctionalInterface\npublic interface SaRetGenericFunction<T> {\n\t\n\t/**\n\t * 执行的方法 \n\t * @return 返回值 \n\t */\n\tT run();\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/SaRouteFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\n\n/**\n * 路由拦截器验证方法的函数式接口，方便开发者进行 lambda 表达式风格调用\n * \n * @author click33\n * @since 1.34.0\n */\n@FunctionalInterface\npublic interface SaRouteFunction {\n\n\t/**\n\t * 执行验证的方法\n\t * \n\t * @param request  Request 包装对象\n\t * @param response Response 包装对象\n\t * @param handler  处理对象\n\t */\n\tvoid run(SaRequest request, SaResponse response, Object handler);\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/SaTwoParamFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun;\n\n/**\n * 双形参、无返回值的函数式接口，方便开发者进行 lambda 表达式风格调用\n *\n * @author click33\n * @since 1.41.0\n */\n@FunctionalInterface\npublic interface SaTwoParamFunction<T, T2> {\n\t\n\t/**\n\t * 执行的方法 \n\t * @param r 传入的参数\n\t * @param r2 传入的参数 2\n\t */\n\tvoid run(T r, T2 r2);\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/hooks/SaTokenPluginHookFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.hooks;\n\nimport cn.dev33.satoken.plugin.SaTokenPlugin;\n\n/**\n * SaTokenPlugin 钩子函数\n *\n * @author click33\n * @since 1.41.0\n */\n@FunctionalInterface\npublic interface SaTokenPluginHookFunction<T extends SaTokenPlugin> {\n\n    /**\n     * 执行的方法\n     * @param plugin 插件实例\n     */\n    void execute(SaTokenPlugin plugin);\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaAutoRenewFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport cn.dev33.satoken.stp.StpLogic;\n\nimport java.util.function.Function;\n\n/**\n * 函数式接口：自定义自动续期条件\n *\n * <p>  参数：StpLogic 实例  </p>\n * <p>  返回：Boolean 是否续期  </p>\n *\n * @author fangzhengjin\n * @since 1.41.0\n */\n@FunctionalInterface\npublic interface SaAutoRenewFunction extends Function<StpLogic, Boolean> {\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckELRootMapExtendFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * 函数式接口：SaCheckELRootMap 扩展函数\n *\n * <p>  参数：SaCheckELRootMap 对象 </p>\n *\n * @author click33\n * @since 1.40.0\n */\n@FunctionalInterface\npublic interface SaCheckELRootMapExtendFunction extends Consumer<Map<String, Object>> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckElementAnnotationFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.function.Consumer;\n\n/**\n * 函数式接口：对一个 [元素] 对象进行注解校验 （注解鉴权内部实现）\n *\n * <p>  参数：element元素  </p>\n * <p>  返回：无  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaCheckElementAnnotationFunction extends Consumer<AnnotatedElement> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckMethodAnnotationFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.lang.reflect.Method;\nimport java.util.function.Consumer;\n\n/**\n * 函数式接口：对一个 [Method] 对象进行注解校验 （注解鉴权内部实现）\n *\n * <p>  参数：Method句柄  </p>\n * <p>  返回：无  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaCheckMethodAnnotationFunction extends Consumer<Method> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckOrAnnotationFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport cn.dev33.satoken.annotation.SaCheckOr;\n\nimport java.util.function.Consumer;\n\n/**\n * 函数式接口：对一个 @SaCheckOr 进行注解校验\n *\n * <p>  参数：SaCheckOr 注解的实例  </p>\n * <p>  返回：无  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaCheckOrAnnotationFunction extends Consumer<SaCheckOr> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCorsHandleFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\n\n/**\n * CORS 跨域策略处理函数\n *\n * @author click33\n * @since 1.42.0\n */\n@FunctionalInterface\npublic interface SaCorsHandleFunction {\n\n    /**\n     * CORS 策略处理函数\n     *\n     * @param req 请求包装对象\n     * @param res 响应包装对象\n     * @param sto 数据读写对象\n     */\n    void execute(\n            SaRequest req,\n            SaResponse res,\n            SaStorage sto\n    );\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateSessionFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport cn.dev33.satoken.session.SaSession;\n\nimport java.util.function.Function;\n\n/**\n * 函数式接口：创建 SaSession 的策略\n *\n * <p>  参数：SessionId  </p>\n * <p>  返回：SaSession对象  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaCreateSessionFunction extends Function<String, SaSession> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateStpLogicFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport cn.dev33.satoken.stp.StpLogic;\n\nimport java.util.function.Function;\n\n/**\n * 函数式接口：创建 StpLogic 的算法\n *\n * <p>  参数：账号体系标识  </p>\n * <p>  返回：创建好的 StpLogic 对象  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaCreateStpLogicFunction extends Function<String, StpLogic> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCreateTokenFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：创建 token 的策略\n *\n * <p>  参数：账号 id、账号类型  </p>\n * <p>  返回：token 值  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaCreateTokenFunction extends BiFunction<Object, String, String> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFailHandleFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.FirewallCheckException;\n\n/**\n * 函数式接口：当防火墙校验不通过时执行的函数\n *\n * @author click33\n * @since 1.37.0\n */\n@FunctionalInterface\npublic interface SaFirewallCheckFailHandleFunction {\n\n    /**\n     * 执行函数\n     * @param e 防火墙校验异常\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    void run(FirewallCheckException e, SaRequest req, SaResponse res, Object extArg);\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaFirewallCheckFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\n\n/**\n * 函数式接口：防火墙校验函数\n *\n * @author click33\n * @since 1.37.0\n */\n@FunctionalInterface\npublic interface SaFirewallCheckFunction {\n\n    /**\n     * 执行函数\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    void execute(SaRequest req, SaResponse res, Object extArg);\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGenerateUniqueTokenFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * 生成唯一式 token 的函数式接口，方便开发者进行 lambda 表达式风格调用\n *\n * <p>  参数：元素名称, 最大尝试次数, 创建 token 函数, 检查 token 函数 </p>\n * <p>  返回：生成的token  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaGenerateUniqueTokenFunction {\n\n    /**\n     * 封装 token 生成、校验的代码，生成唯一式 token\n     *\n     * @param elementName 要生成的元素名称，方便抛出异常时组织提示信息\n     * @param maxTryTimes 最大尝试次数\n     * @param createTokenFunction 创建 token 的函数\n     * @param checkTokenFunction 校验 token 是否唯一的函数（返回 true 表示唯一，可用）\n     * @return 最终生成的唯一式 token\n     */\n    String execute(\n            String elementName,\n            int maxTryTimes,\n            Supplier<String> createTokenFunction,\n            Function<String, Boolean> checkTokenFunction\n    );\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaGetAnnotationFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.AnnotatedElement;\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：从元素上获取注解\n *\n * <p>  参数：element元素，要获取的注解类型  </p>\n * <p>  返回：注解对象  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaGetAnnotationFunction extends BiFunction<AnnotatedElement, Class<? extends Annotation>, Annotation> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaHasElementFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.util.List;\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：判断集合中是否包含指定元素（模糊匹配）\n *\n * <p>  参数：集合、元素  </p>\n * <p>  返回：是否包含  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaHasElementFunction extends BiFunction<List<String>, String, Boolean> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaIsAnnotationPresentFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：判断一个 Method 或其所属 Class 是否包含指定注解\n *\n * <p>  参数：Method、注解  </p>\n * <p>  返回：是否包含  </p>\n *\n * @author click33\n * @since 1.35.0\n */\n@FunctionalInterface\npublic interface SaIsAnnotationPresentFunction extends BiFunction<Method, Class<? extends Annotation>, Boolean> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaRouteMatchFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.fun.strategy;\n\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：路由匹配策略\n *\n * <p>  参数：pattern, path  </p>\n * <p>  返回：是否匹配  </p>\n *\n * @author click33\n * @since 1.42.0\n */\n@FunctionalInterface\npublic interface SaRouteMatchFunction extends BiFunction<String, String, Boolean> {\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.http;\n\nimport java.util.Map;\n\n/**\n * Http 请求处理器\n * \n * @author click33\n * @since 1.43.0\n */\npublic interface SaHttpTemplate {\n\n\t/**\n\t * get 请求\n\t *\n\t * @param url /\n\t * @return /\n\t */\n\tString get(String url);\n\n\t/**\n\t * post 请求，form-data 格式参数\n\t *\n\t * @param url /\n\t * @param params /\n\t * @return /\n\t */\n\tString postByFormData(String url, Map<String, Object> params);\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplateDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.http;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.NotImplException;\n\nimport java.util.Map;\n\n/**\n * Http 请求处理器，默认实现类\n * \n * @author click33\n * @since 1.43.0\n */\npublic class SaHttpTemplateDefaultImpl implements SaHttpTemplate {\n\n\tpublic static final String ERROR_MESSAGE = \"HTTP 请求处理器未实现\";\n\n\t/**\n\t * get 请求\n\t *\n\t * @param url /\n\t * @return /\n\t */\n\t@Override\n\tpublic String get(String url) {\n\t\tthrow new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10004);\n\t}\n\n\t/**\n\t * post 请求，form-data 格式参数\n\t */\n\t@Override\n\tpublic String postByFormData(String url, Map<String, Object> params) {\n\t\tthrow new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10004);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.http;\n\nimport cn.dev33.satoken.SaManager;\n\nimport java.util.Map;\n\n/**\n * Http 请求处理器 工具类\n * \n * @author click33\n * @since 1.43.0\n */\npublic class SaHttpUtil {\n\n\t/**\n\t * get 请求\n\t *\n\t * @param url /\n\t * @return /\n\t */\n\tpublic static String get(String url) {\n\t\treturn SaManager.getSaHttpTemplate().get(url);\n\t}\n\n\t/**\n\t * post 请求，form-data 格式参数\n\t *\n\t * @param url /\n\t * @param params /\n\t * @return /\n\t */\n\tpublic static String postByFormData(String url, Map<String, Object> params) {\n\t\treturn SaManager.getSaHttpTemplate().postByFormData(url, params);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicAccount.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.httpauth.basic;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * Sa-Token Http Basic 账号\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaHttpBasicAccount {\n\n\t/**\n\t * 账号\n\t */\n\tprivate String username;\n\n\t/**\n\t * 密码\n\t */\n\tprivate String password;\n\n\t/**\n\t * 构造函数\n\t * @param username 账号\n\t * @param password 密码\n\t */\n\tpublic SaHttpBasicAccount(String username, String password) {\n\t\tthis.username = username;\n\t\tthis.password = password;\n\t}\n\n\t/**\n\t * 构造函数\n\t * @param usernameAndPassword 账号和密码，冒号隔开\n\t */\n\tpublic SaHttpBasicAccount(String usernameAndPassword) {\n\t\tif(SaFoxUtil.isEmpty(usernameAndPassword)) {\n\t\t\tthrow new SaTokenException(\"UsernameAndPassword 不能为空\");\n\t\t}\n\t\tString[] arr = usernameAndPassword.split(\":\");\n\t\tif(arr.length != 2) {\n\t\t\tthrow new SaTokenException(\"UsernameAndPassword 格式错误，正确格式为：username:password\");\n\t\t}\n\t\tthis.username = arr[0];\n\t\tthis.password = arr[1];\n\t}\n\n\t/**\n\t * 获取 账号\n\t *\n\t * @return username 账号\n\t */\n\tpublic String getUsername() {\n\t\treturn this.username;\n\t}\n\n\t/**\n\t * 设置 账号\n\t *\n\t * @param username 账号\n\t */\n\tpublic void setUsername(String username) {\n\t\tthis.username = username;\n\t}\n\n\t/**\n\t * 获取 密码\n\t *\n\t * @return password 密码\n\t */\n\tpublic String getPassword() {\n\t\treturn this.password;\n\t}\n\n\t/**\n\t * 设置 密码\n\t *\n\t * @param password 密码\n\t */\n\tpublic void setPassword(String password) {\n\t\tthis.password = password;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaHttpBasicAccount{\" +\n\t\t\t\t\"username='\" + username + '\\'' +\n\t\t\t\t\", password='\" + password + '\\'' +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.httpauth.basic;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.NotHttpBasicAuthException;\nimport cn.dev33.satoken.secure.SaBase64Util;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * Sa-Token Http Basic 认证模块\n *\n * @author click33\n * @since 1.26.0\n */\npublic class SaHttpBasicTemplate {\n\t\n\t/**\n\t * 默认的 Realm 领域名称\n\t */\n\tpublic static final String DEFAULT_REALM = \"Sa-Token\";\n\n\t/**\n\t * 在校验失败时，设置响应头，并抛出异常\n\t * @param realm 领域 \n\t */\n\tpublic void throwNotBasicAuthException(String realm) {\n\t\tSaHolder.getResponse().setStatus(401).setHeader(\"WWW-Authenticate\", \"Basic Realm=\" + realm);\n\t\tthrow new NotHttpBasicAuthException().setCode(SaErrorCode.CODE_10311);\n\t}\n\n\t/**\n\t * 获取浏览器提交的 Http Basic 参数 （裁剪掉前缀并解码）\n\t * @return 值\n\t */\n\tpublic String getAuthorizationValue() {\n\t\t\n\t\t// 获取前端提交的请求头 Authorization 参数\n\t\tString authorization = SaHolder.getRequest().getHeader(\"Authorization\");\n\t\t\n\t\t// 如果不是以 Basic 作为前缀，则视为无效 \n\t\tif(authorization == null || ! authorization.startsWith(\"Basic \")) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\t// 裁剪前缀并解码 \n\t\treturn SaBase64Util.decode(authorization.substring(6));\n\t}\n\n\t/**\n\t * 获取 Http Basic 账号密码对象\n\t * @return /\n\t */\n\tpublic SaHttpBasicAccount getHttpBasicAccount() {\n\t\tString authorizationValue = getAuthorizationValue();\n\t\tif(authorizationValue == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new SaHttpBasicAccount(authorizationValue);\n\t}\n\n\t/**\n\t * 对当前会话进行 Basic 校验（使用全局配置的账号密码），校验不通过则抛出异常  \n\t */\n\tpublic void check() {\n\t\tcheck(DEFAULT_REALM, SaManager.getConfig().getHttpBasic());\n\t}\n\n\t/**\n\t * 对当前会话进行 Basic 校验（手动设置账号密码），校验不通过则抛出异常  \n\t * @param account 账号（格式为 user:password）\n\t */\n\tpublic void check(String account) {\n\t\tcheck(DEFAULT_REALM, account);\n\t}\n\n\t/**\n\t * 对当前会话进行 Basic 校验（手动设置 Realm 和 账号密码），校验不通过则抛出异常 \n\t * @param realm 领域 \n\t * @param account 账号（格式为 user:password）\n\t */\n\tpublic void check(String realm, String account) {\n\t\tif(SaFoxUtil.isEmpty(account)) {\n\t\t\taccount = SaManager.getConfig().getHttpBasic();\n\t\t}\n\t\tString authorization = getAuthorizationValue();\n\t\tif(SaFoxUtil.isEmpty(authorization) || ! authorization.equals(account)) {\n\t\t\tthrowNotBasicAuthException(realm);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/httpauth/basic/SaHttpBasicUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.httpauth.basic;\n\n/**\n * Sa-Token Http Basic 认证模块，Util 工具类\n *\n * @author click33\n * @since 1.26.0\n */\npublic class SaHttpBasicUtil {\n\n\tprivate SaHttpBasicUtil() {\n\t}\n\t\n\t/**\n\t * 底层使用的 SaBasicTemplate 对象\n\t */\n\tpublic static SaHttpBasicTemplate saHttpBasicTemplate = new SaHttpBasicTemplate();\n\n\t/**\n\t * 获取浏览器提交的 Http Basic 参数 （裁剪掉前缀并解码）\n\t * @return 值\n\t */\n\tpublic static String getAuthorizationValue() {\n\t\treturn saHttpBasicTemplate.getAuthorizationValue();\n\t}\n\n\t/**\n\t * 获取 Http Basic 账号密码对象\n\t * @return /\n\t */\n\tpublic static SaHttpBasicAccount getHttpBasicAccount() {\n\t\treturn saHttpBasicTemplate.getHttpBasicAccount();\n\t}\n\n\t/**\n\t * 对当前会话进行 Basic 校验（使用全局配置的账号密码），校验不通过则抛出异常  \n\t */\n\tpublic static void check() {\n\t\tsaHttpBasicTemplate.check();\n\t}\n\n\t/**\n\t * 对当前会话进行 Basic 校验（手动设置账号密码），校验不通过则抛出异常  \n\t * @param account 账号（格式为 user:password）\n\t */\n\tpublic static void check(String account) {\n\t\tsaHttpBasicTemplate.check(account);\n\t}\n\n\t/**\n\t * 对当前会话进行 Basic 校验（手动设置 Realm 和 账号密码），校验不通过则抛出异常 \n\t * @param realm 领域 \n\t * @param account 账号（格式为 user:password）\n\t */\n\tpublic static void check(String realm, String account) {\n\t\tsaHttpBasicTemplate.check(realm, account);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.httpauth.digest;\n\n/**\n * Sa-Token Http Digest 认证 - 参数实体类\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaHttpDigestModel {\n\n    /**\n     * 默认的 Realm 领域名称\n     */\n    public static final String DEFAULT_REALM = \"Sa-Token\";\n\n    /**\n     * 默认的 qop 值\n     */\n    public static final String DEFAULT_QOP = \"auth\";\n\n\n    /**\n     * 用户名\n     */\n    public String username;\n\n    /**\n     * 密码\n     */\n    public String password;\n\n    /**\n     * 领域\n     */\n    public String realm = DEFAULT_REALM;\n\n    /**\n     * 随机数\n     */\n    public String nonce;\n\n    /**\n     * 请求 uri\n     */\n    public String uri;\n\n    /**\n     * 请求方法\n     */\n    public String method;\n\n    /**\n     * 保护质量（auth=默认的，auth-int=增加报文完整性检测），可以为空，但不推荐\n     */\n    public String qop;\n\n    /**\n     * nonce计数器，是一个16进制的数值，表示同一nonce下客户端发送出请求的数量\n     */\n    public String nc;\n\n    /**\n     * 客户端随机数，由客户端提供\n     */\n    public String cnonce;\n\n    /**\n     * opaque\n     */\n    public String opaque;\n\n    /**\n     * 请求摘要，最终计算的摘要结果\n     */\n    public String response;\n\n    // ------------------- 构造函数 -------------------\n\n    public SaHttpDigestModel() {\n    }\n    public SaHttpDigestModel(String username, String password) {\n        this.username = username;\n        this.password = password;\n    }\n    public SaHttpDigestModel(String username, String password, String realm) {\n        this.username = username;\n        this.password = password;\n        this.realm = realm;\n    }\n\n\n    // ------------------- get/set -------------------\n\n    /**\n     * 获取 用户名\n     *\n     * @return username 用户名\n     */\n    public String getUsername() {\n        return this.username;\n    }\n\n    /**\n     * 设置 用户名\n     *\n     * @param username 用户名\n     * @return /\n     */\n    public SaHttpDigestModel setUsername(String username) {\n        this.username = username;\n        return this;\n    }\n\n    /**\n     * 获取 领域\n     *\n     * @return realm 领域\n     */\n    public String getRealm() {\n        return this.realm;\n    }\n\n    /**\n     * 设置 领域\n     *\n     * @param realm 领域\n     * @return /\n     */\n    public SaHttpDigestModel setRealm(String realm) {\n        this.realm = realm;\n        return this;\n    }\n\n    /**\n     * 获取 密码\n     *\n     * @return password 密码\n     */\n    public String getPassword() {\n        return this.password;\n    }\n\n    /**\n     * 设置 密码\n     *\n     * @param password 密码\n     * @return /\n     */\n    public SaHttpDigestModel setPassword(String password) {\n        this.password = password;\n        return this;\n    }\n\n    /**\n     * 获取 随机数\n     *\n     * @return nonce 随机数\n     */\n    public String getNonce() {\n        return this.nonce;\n    }\n\n    /**\n     * 设置 随机数\n     *\n     * @param nonce 随机数\n     * @return /\n     */\n    public SaHttpDigestModel setNonce(String nonce) {\n        this.nonce = nonce;\n        return this;\n    }\n\n    /**\n     * 获取 请求 uri\n     *\n     * @return uri 请求 uri\n     */\n    public String getUri() {\n        return this.uri;\n    }\n\n    /**\n     * 设置 请求 uri\n     *\n     * @param uri 请求 uri\n     * @return /\n     */\n    public SaHttpDigestModel setUri(String uri) {\n        this.uri = uri;\n        return this;\n    }\n\n    /**\n     * 获取 请求方法\n     *\n     * @return method 请求方法\n     */\n    public String getMethod() {\n        return this.method;\n    }\n\n    /**\n     * 设置 请求方法\n     *\n     * @param method 请求方法\n     * @return /\n     */\n    public SaHttpDigestModel setMethod(String method) {\n        this.method = method;\n        return this;\n    }\n\n    /**\n     * 获取 保护质量（auth=默认的，auth-int=增加报文完整性检测），可以为空，但不推荐\n     *\n     * @return qop 保护质量（auth=默认的，auth-int=增加报文完整性检测），可以为空，但不推荐\n     */\n    public String getQop() {\n        return this.qop;\n    }\n\n    /**\n     * 设置 保护质量（auth=默认的，auth-int=增加报文完整性检测），可以为空，但不推荐\n     *\n     * @param qop 保护质量（auth=默认的，auth-int=增加报文完整性检测），可以为空，但不推荐\n     * @return /\n     */\n    public SaHttpDigestModel setQop(String qop) {\n        this.qop = qop;\n        return this;\n    }\n\n    /**\n     * 获取 nonce计数器，是一个16进制的数值，表示同一nonce下客户端发送出请求的数量\n     *\n     * @return nc nonce计数器，是一个16进制的数值，表示同一nonce下客户端发送出请求的数量\n     */\n    public String getNc() {\n        return this.nc;\n    }\n\n    /**\n     * 设置 nonce计数器，是一个16进制的数值，表示同一nonce下客户端发送出请求的数量\n     *\n     * @param nc nonce计数器，是一个16进制的数值，表示同一nonce下客户端发送出请求的数量\n     * @return /\n     */\n    public SaHttpDigestModel setNc(String nc) {\n        this.nc = nc;\n        return this;\n    }\n\n    /**\n     * 获取 客户端随机数，由客户端提供\n     *\n     * @return cnonce 客户端随机数，由客户端提供\n     */\n    public String getCnonce() {\n        return this.cnonce;\n    }\n\n    /**\n     * 设置 客户端随机数，由客户端提供\n     *\n     * @param cnonce 客户端随机数，由客户端提供\n     * @return /\n     */\n    public SaHttpDigestModel setCnonce(String cnonce) {\n        this.cnonce = cnonce;\n        return this;\n    }\n\n    /**\n     * 获取 opaque\n     *\n     * @return opaque opaque\n     */\n    public String getOpaque() {\n        return this.opaque;\n    }\n\n    /**\n     * 设置 opaque\n     *\n     * @param opaque opaque\n     * @return /\n     */\n    public SaHttpDigestModel setOpaque(String opaque) {\n        this.opaque = opaque;\n        return this;\n    }\n\n    /**\n     * 获取 请求摘要，最终计算的摘要结果\n     *\n     * @return response 请求摘要，最终计算的摘要结果\n     */\n    public String getResponse() {\n        return this.response;\n    }\n\n    /**\n     * 设置 请求摘要，最终计算的摘要结果\n     *\n     * @param response 请求摘要，最终计算的摘要结果\n     * @return /\n     */\n    public SaHttpDigestModel setResponse(String response) {\n        this.response = response;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"SaHttpDigestModel[\" +\n                \"username=\" + username +\n                \", password=\" + password +\n                \", realm=\" + realm +\n                \", nonce=\" + nonce +\n                \", uri=\" + uri +\n                \", method=\" + method +\n                \", qop=\" + qop +\n                \", nc=\" + nc +\n                \", cnonce=\" + cnonce +\n                \", opaque=\" + opaque +\n                \", response=\" + response +\n                \"]\";\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.httpauth.digest;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.SaCheckHttpDigest;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.NotHttpDigestAuthException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.secure.SaSecureUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token Http Digest 认证模块 - 模板方法类\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaHttpDigestTemplate {\n\n    /*\n        这里只是 Http Digest 认证的一个简单实现，待实现功能还有：\n            1、nonce 防重放攻击\n            2、nc 计数器\n            3、qop 保护质量=auth-int\n            4、opaque 透明值\n            5、algorithm 更多摘要算法\n            等等\n     */\n\n    /**\n     * 构建认证失败的响应头参数\n     * @param model 参数对象\n     * @return 响应头值\n     */\n    public String buildResponseHeaderValue(SaHttpDigestModel model) {\n        // 抛异常\n        String headerValue = \"Digest \" +\n                \"realm=\\\"\" + model.realm + \"\\\", \" +\n                \"qop=\\\"\" + model.qop + \"\\\", \" +\n                \"nonce=\\\"\" + model.nonce + \"\\\", \" +\n                \"nc=\" + model.nc + \", \" +\n                \"opaque=\\\"\" + model.opaque + \"\\\"\";\n        return headerValue;\n    }\n\n    /**\n     * 在校验失败时，设置响应头，并抛出异常\n     * @param model Digest 参数对象\n     */\n    public void throwNotHttpDigestAuthException(SaHttpDigestModel model) {\n        // 补全一些必须的参数\n        model.realm = (model.realm != null) ? model.realm : SaHttpDigestModel.DEFAULT_REALM;\n        model.qop = (model.qop != null) ? model.qop : SaHttpDigestModel.DEFAULT_QOP;\n        model.nonce = (model.nonce != null) ? model.nonce : SaFoxUtil.getRandomString(32);\n        model.opaque = (model.opaque != null) ? model.opaque : SaFoxUtil.getRandomString(32);\n        model.nc = (model.nc != null) ? model.nc : \"00000001\";\n\n        // 设置响应头\n        SaHolder.getResponse()\n                .setStatus(401)\n                .setHeader(\"WWW-Authenticate\", buildResponseHeaderValue(model));\n\n        // 抛异常\n        throw new NotHttpDigestAuthException().setCode(SaErrorCode.CODE_10312);\n    }\n\n    /**\n     * 获取浏览器提交的 Digest 参数 （裁剪掉前缀）\n     * @return 值\n     */\n    public String getAuthorizationValue() {\n\n        // 获取前端提交的请求头 Authorization 参数\n        String authorization = SaHolder.getRequest().getHeader(\"Authorization\");\n\n        // 如果不是以 Digest 作为前缀，则视为无效\n        if(authorization == null || ! authorization.startsWith(\"Digest \")) {\n            return null;\n        }\n\n        // 裁剪前缀并解码\n        return authorization.substring(7);\n    }\n\n    /**\n     * 获取浏览器提交的 Digest 参数，并转化为 Map\n     * @return /\n     */\n    public SaHttpDigestModel getAuthorizationValueToModel() {\n\n        // 先获取字符串值\n        String authorization = getAuthorizationValue();\n        if(authorization == null) {\n//            throw new SaTokenException(\"请求头中未携带 Digest 认证参数\");\n            return null;\n        }\n\n        // 根据逗号分割，解析为 Map\n        Map<String, String> map = new LinkedHashMap<>();\n        String[] arr = authorization.split(\",\");\n        for (String s : arr) {\n            String[] kv = s.split(\"=\");\n            if (kv.length == 2) {\n                map.put(kv[0].trim(), kv[1].trim().replace(\"\\\"\", \"\"));\n            }\n            // 兼容字符串包含多个=的情况，如：uri 带参数的问题\n            // username=\"sa\", realm=\"Sa-Token\", nonce=\"IWlEwO23oCAbIAbHX1BYnX5ddKHUdsjW\", uri=\"/test/testDigest?name=zhangsan&age=18\", response=\"c4359210ccb23c985234ee6e02def88d\", opaque=\"H6jPyjwfioc0oUbDE0OSmpX7wznfxxMo\", qop=auth, nc=00000002, cnonce=\"46dd0073c981a9c7\"\n            else if (s.contains(\"=\")) {\n\t\t\t\tmap.put(kv[0].trim(), s.substring(kv[0].length() + 1).trim().replace(\"\\\"\", \"\"));\n\t\t\t}\n        }\n\n        /*\n            参考样例：\n                username=sa,\n                realm=Sa-Token,\n                nonce=dcd98b7102dd2f0e8b11d0f600bfb0c093,\n                uri=/test/testDigest,\n                response=a32023c128e142163dd4856a2f511c70,\n                opaque=5ccc069c403ebaf9f0171e9517f40e41,\n                qop=auth,\n                nc=00000002,\n                cnonce=f3ca6bfc0b2f59c4\n         */\n\n        // 转化为 Model\n        SaHttpDigestModel model = new SaHttpDigestModel();\n        model.username = map.get(\"username\");\n        model.realm = map.get(\"realm\");\n        model.nonce = map.get(\"nonce\");\n        model.uri = map.get(\"uri\");\n        model.method = SaHolder.getRequest().getMethod();\n        model.qop = map.get(\"qop\");\n        model.nc = map.get(\"nc\");\n        model.cnonce = map.get(\"cnonce\");\n        model.opaque = map.get(\"opaque\");\n        model.response = map.get(\"response\");\n\n        //\n        return model;\n    }\n\n    /**\n     * 计算：根据 Digest 参数计算 response\n     *\n     * @param model Digest 参数对象\n     * @return 计算出的 response\n     */\n    public String calcResponse(SaHttpDigestModel model) {\n\n        // frag1 = md5(username:realm:password)\n        String frag1 = SaSecureUtil.md5(model.username + \":\" + model.realm + \":\" + model.password);\n\n        // frag2 = nonce:nc:cnonce:qop\n        String frag2 = model.nonce + \":\" + model.nc + \":\" + model.cnonce + \":\" + model.qop;\n\n        // frag3 = md5(method:uri)\n        String frag3 = SaSecureUtil.md5(model.method + \":\" + model.uri);\n\n        // 最终结果 = md5(frag1:frag2:frag3)\n        String response = SaSecureUtil.md5(frag1 + \":\" + frag2 + \":\" + frag3);\n\n        //\n        return response;\n    }\n\n    /**\n     * 把 hopeModel 有的值都 copy 到 reqModel 中\n     */\n    public void copyHopeToReq(SaHttpDigestModel hopeModel, SaHttpDigestModel reqModel){\n        reqModel.username = hopeModel.username;\n        reqModel.password = hopeModel.password;\n        reqModel.realm = hopeModel.realm != null ? hopeModel.realm : reqModel.realm;\n        reqModel.nonce = hopeModel.nonce != null ? hopeModel.nonce : reqModel.nonce;\n        reqModel.uri = hopeModel.uri != null ? hopeModel.uri : reqModel.uri;\n        reqModel.method = hopeModel.method != null ? hopeModel.method : reqModel.method;\n        reqModel.qop = hopeModel.qop != null ? hopeModel.qop : reqModel.qop;\n        reqModel.nc = hopeModel.nc != null ? hopeModel.nc : reqModel.nc;\n        reqModel.opaque = hopeModel.opaque != null ? hopeModel.opaque : reqModel.opaque;\n        // reqModel.cnonce = hopeModel.cnonce != null ? hopeModel.cnonce : reqModel.cnonce;\n        // reqModel.response = hopeModel.response != null ? hopeModel.response : reqModel.response;\n    }\n\n    // ---------- 校验 ----------\n\n    /**\n     * 校验：根据提供 Digest 参数计算 res，与 request 请求中的 Digest 参数进行校验，校验不通过则抛出异常\n     * @param hopeModel 提供的 Digest 参数对象\n     */\n    public void check(SaHttpDigestModel hopeModel) {\n\n        // 先进行一些必须的希望参数校验\n        SaTokenException.notEmpty(hopeModel, \"Digest参数对象不能为空\");\n        SaTokenException.notEmpty(hopeModel.username, \"必须提供希望的 username 参数\");\n        SaTokenException.notEmpty(hopeModel.password, \"必须提供希望的 password 参数\");\n\n        // 获取 web 请求中的 Digest 参数\n        SaHttpDigestModel reqModel = getAuthorizationValueToModel();\n\n        // 为空代表前端根本没有提交 Digest 参数，直接抛异常\n        if(reqModel == null) {\n            throwNotHttpDigestAuthException(hopeModel);\n        }\n\n        // 把 hopeModel 有的值都 copy 到 reqModel 中\n        copyHopeToReq(hopeModel, reqModel);\n\n        // 计算\n        String cResponse = calcResponse(reqModel);\n\n        // 比对，不一致就抛异常\n        if(! cResponse.equals(reqModel.response)) {\n            throwNotHttpDigestAuthException(hopeModel);\n        }\n\n        // 认证通过\n    }\n\n    /**\n     * 校验：根据提供的参数，校验不通过抛出异常\n     * @param username 用户名\n     * @param password 密码\n     */\n    public void check(String username, String password) {\n        check(new SaHttpDigestModel(username, password));\n    }\n\n    /**\n     * 校验：根据提供的参数，校验不通过抛出异常\n     * @param username 用户名\n     * @param password 密码\n     * @param realm 领域\n     */\n    public void check(String username, String password, String realm) {\n        check(new SaHttpDigestModel(username, password, realm));\n    }\n\n    /**\n     * 校验：根据全局配置参数，校验不通过抛出异常\n     */\n    public void check() {\n        String httpDigest = SaManager.getConfig().getHttpDigest();\n        if(SaFoxUtil.isEmpty(httpDigest)){\n            throw new SaTokenException(\"未配置全局 Http Digest 认证参数\");\n        }\n        String[] arr = httpDigest.split(\":\");\n        if(arr.length != 2){\n            throw new SaTokenException(\"全局 Http Digest 认证参数配置错误，格式应如：username:password\");\n        }\n        check(arr[0], arr[1]);\n    }\n\n\n\n    // ----------------- 过期方法 -----------------\n\n    /**\n     * 根据注解 ( @SaCheckHttpDigest ) 鉴权\n     *\n     * @param at 注解对象\n     */\n    @Deprecated\n    public void checkByAnnotation(SaCheckHttpDigest at) {\n\n        // 如果配置了 value，则以 value 优先\n        String value = at.value();\n        if(SaFoxUtil.isNotEmpty(value)){\n            String[] arr = value.split(\":\");\n            if(arr.length != 2){\n                throw new SaTokenException(\"注解参数配置错误，格式应如：username:password\");\n            }\n            check(arr[0], arr[1]);\n            return;\n        }\n\n        // 如果配置了 username，则分别获取参数\n        String username = at.username();\n        if(SaFoxUtil.isNotEmpty(username)){\n            check(username, at.password(), at.realm());\n            return;\n        }\n\n        // 都没有配置，则根据全局配置参数进行校验\n        check();\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.httpauth.digest;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpDigest;\n\nimport java.util.Map;\n\n/**\n * Sa-Token Http Digest 认证模块，Util 工具类\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaHttpDigestUtil {\n\n\tprivate SaHttpDigestUtil() {\n\t}\n\t\n\t/**\n\t * 底层使用的 SaHttpDigestTemplate 对象\n\t */\n\tpublic static SaHttpDigestTemplate saHttpDigestTemplate = new SaHttpDigestTemplate();\n\n\n\t/**\n\t * 获取浏览器提交的 Digest 参数 （裁剪掉前缀）\n\t * @return 值\n\t */\n\tpublic static String getAuthorizationValue() {\n\t\treturn saHttpDigestTemplate.getAuthorizationValue();\n\t}\n\n\t/**\n\t * 获取浏览器提交的 Digest 参数，并转化为 Map\n\t * @return /\n\t */\n\tpublic static SaHttpDigestModel getAuthorizationValueToModel() {\n\t\treturn saHttpDigestTemplate.getAuthorizationValueToModel();\n\t}\n\n\t// ---------- 校验 ----------\n\n\t/**\n\t * 校验：根据提供 Digest 参数计算 res，与 request 请求中的 Digest 参数进行校验，校验不通过则抛出异常\n\t * @param hopeModel 提供的 Digest 参数对象\n\t */\n\tpublic static void check(SaHttpDigestModel hopeModel) {\n\t\tsaHttpDigestTemplate.check(hopeModel);\n\t}\n\n\t/**\n\t * 校验：根据提供的参数，校验不通过抛出异常\n\t * @param username 用户名\n\t * @param password 密码\n\t */\n\tpublic static void check(String username, String password) {\n\t\tsaHttpDigestTemplate.check(username, password);\n\t}\n\n\t/**\n\t * 校验：根据提供的参数，校验不通过抛出异常\n\t * @param username 用户名\n\t * @param password 密码\n\t * @param realm 领域\n\t */\n\tpublic static void check(String username, String password, String realm) {\n\t\tsaHttpDigestTemplate.check(username, password, realm);\n\t}\n\n\t/**\n\t * 校验：根据全局配置参数，校验不通过抛出异常\n\t */\n\tpublic static void check() {\n\t\tsaHttpDigestTemplate.check();\n\t}\n\n\n\n\t// ----------------- 过期方法 -----------------\n\n\t/**\n\t * 根据注解 ( @SaCheckHttpDigest ) 鉴权\n\t *\n\t * @param at 注解对象\n\t */\n\t@Deprecated\n\tpublic static void checkByAnnotation(SaCheckHttpDigest at) {\n\t\tsaHttpDigestTemplate.checkByAnnotation(at);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\nimport java.util.Map;\n\n/**\n * JSON 转换器 \n * \n * @author click33\n * @since 1.30.0\n */\npublic interface SaJsonTemplate {\n\n\t/**\n\t * 序列化：对象 -> json 字符串\n\t *\n\t * @param obj /\n\t * @return /\n\t */\n\tString objectToJson(Object obj);\n\n\t/**\n\t * 反序列化：json 字符串 → 对象\n\t *\n\t * @param jsonStr /\n\t * @param type /\n\t * @return /\n\t * @param <T> /\n\t */\n\t<T>T jsonToObject(String jsonStr, Class<T> type);\n\n\t/**\n\t * 反序列化：json 字符串 → 对象 (自动判断类型)\n\t *\n\t * @param jsonStr /\n\t * @return /\n\t */\n\tdefault Object jsonToObject(String jsonStr) {\n\t\treturn jsonToObject(jsonStr, Object.class);\n\t};\n\n\t/**\n\t * 反序列化：json 字符串 → Map\n\t *\n\t * @param jsonStr /\n\t * @return /\n\t */\n\tdefault Map<String, Object> jsonToMap(String jsonStr) {\n\t\treturn jsonToObject(jsonStr, Map.class);\n\t};\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplateDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.NotImplException;\n\nimport java.util.Map;\n\n/**\n * JSON 转换器，默认实现类\n *\n * <p> 如果代码断点走到了此默认实现类，说明框架没有注入有效的 JSON 转换器，需要开发者自行实现并注入 </p>\n *\n * @author click33\n * @since 1.30.0\n */\npublic class SaJsonTemplateDefaultImpl implements SaJsonTemplate {\n\n\tpublic static final String ERROR_MESSAGE = \"未实现具体的 json 转换器\";\n\n\t@Override\n\tpublic String objectToJson(Object obj) {\n\t\tthrow new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003);\n\t}\n\n\t@Override\n\tpublic Object jsonToObject(String jsonStr) {\n\t\tthrow new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003);\n\t}\n\n\t@Override\n\tpublic <T> T jsonToObject(String jsonStr, Class<T> type) {\n\t\tthrow new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003);\n\t}\n\n\t@Override\n\tpublic Map<String, Object> jsonToMap(String jsonStr) {\n\t\tthrow new NotImplException(ERROR_MESSAGE).setCode(SaErrorCode.CODE_10003);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenEventCenter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.listener;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.StpLogic;\n\n/**\n * Sa-Token 事件中心 事件发布器\n *\n * <p> 提供侦听器注册、事件发布能力 </p>\n * \n * @author click33\n * @since 1.31.0\n */\npublic class SaTokenEventCenter {\n\n\t// --------- 注册侦听器 \n\t\n\tprivate static List<SaTokenListener> listenerList = new ArrayList<>();\n\t\n\tstatic {\n\t\t// 默认添加控制台日志侦听器 \n\t\tlistenerList.add(new SaTokenListenerForLog());\n\t}\n\n\t/**\n\t * 获取已注册的所有侦听器\n\t * @return / \n\t */\n\tpublic static List<SaTokenListener> getListenerList() {\n\t\treturn listenerList;\n\t}\n\n\t/**\n\t * 重置侦听器集合\n\t * @param listenerList / \n\t */\n\tpublic static void setListenerList(List<SaTokenListener> listenerList) {\n\t\tif(listenerList == null) {\n\t\t\tthrow new SaTokenException(\"重置的侦听器集合不可以为空\").setCode(SaErrorCode.CODE_10031);\n\t\t}\n\t\tSaTokenEventCenter.listenerList = listenerList;\n\t}\n\n\t/**\n\t * 注册一个侦听器 \n\t * @param listener / \n\t */\n\tpublic static void registerListener(SaTokenListener listener) {\n\t\tif(listener == null) {\n\t\t\tthrow new SaTokenException(\"注册的侦听器不可以为空\").setCode(SaErrorCode.CODE_10032);\n\t\t}\n\t\tlistenerList.add(listener);\n\t}\n\n\t/**\n\t * 注册一组侦听器 \n\t * @param listenerList / \n\t */\n\tpublic static void registerListenerList(List<SaTokenListener> listenerList) {\n\t\tif(listenerList == null) {\n\t\t\tthrow new SaTokenException(\"注册的侦听器集合不可以为空\").setCode(SaErrorCode.CODE_10031);\n\t\t}\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tif(listener == null) {\n\t\t\t\tthrow new SaTokenException(\"注册的侦听器不可以为空\").setCode(SaErrorCode.CODE_10032);\n\t\t\t}\n\t\t}\n\t\tSaTokenEventCenter.listenerList.addAll(listenerList);\n\t}\n\n\t/**\n\t * 移除一个侦听器 \n\t * @param listener / \n\t */\n\tpublic static void removeListener(SaTokenListener listener) {\n\t\tlistenerList.remove(listener);\n\t}\n\n\t/**\n\t * 移除指定类型的所有侦听器 \n\t * @param cls / \n\t */\n\tpublic static void removeListener(Class<? extends SaTokenListener> cls) {\n\t\tArrayList<SaTokenListener> listenerListCopy = new ArrayList<>(listenerList);\n\t\tfor (SaTokenListener listener : listenerListCopy) {\n\t\t\tif(cls.isAssignableFrom(listener.getClass())) {\n\t\t\t\tlistenerList.remove(listener);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 清空所有已注册的侦听器 \n\t */\n\tpublic static void clearListener() {\n\t\tlistenerList.clear();\n\t}\n\n\t/**\n\t * 判断是否已经注册了指定侦听器 \n\t * @param listener / \n\t * @return / \n\t */\n\tpublic static boolean hasListener(SaTokenListener listener) {\n\t\treturn listenerList.contains(listener);\n\t}\n\n\t/**\n\t * 判断是否已经注册了指定类型的侦听器 \n\t * @param cls / \n\t * @return / \n\t */\n\tpublic static boolean hasListener(Class<? extends SaTokenListener> cls) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tif(cls.isAssignableFrom(listener.getClass())) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t\n\t// --------- 事件发布 \n\t\n\t/**\n\t * 事件发布：xx 账号登录\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue 本次登录产生的 token 值 \n\t * @param loginParameter 登录参数\n\t */\n\tpublic static void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doLogin(loginType, loginId, tokenValue, loginParameter);\n\t\t}\n\t}\n\t\t\t\n\t/**\n\t * 事件发布：xx 账号注销\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue token值\n\t */\n\tpublic static void doLogout(String loginType, Object loginId, String tokenValue) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doLogout(loginType, loginId, tokenValue);\n\t\t}\n\t}\n\t\n\t/**\n\t * 事件发布：xx 账号被踢下线\n\t * @param loginType 账号类别 \n\t * @param loginId 账号id \n\t * @param tokenValue token值 \n\t */\n\tpublic static void doKickout(String loginType, Object loginId, String tokenValue) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doKickout(loginType, loginId, tokenValue);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：xx 账号被顶下线\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue token值\n\t */\n\tpublic static void doReplaced(String loginType, Object loginId, String tokenValue) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doReplaced(loginType, loginId, tokenValue);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：xx 账号被封禁\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param service 指定服务 \n\t * @param level 封禁等级 \n\t * @param disableTime 封禁时长，单位: 秒\n\t */\n\tpublic static void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doDisable(loginType, loginId, service, level, disableTime);\n\t\t}\n\t}\n\t\n\t/**\n\t * 事件发布：xx 账号被解封\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param service 指定服务 \n\t */\n\tpublic static void doUntieDisable(String loginType, Object loginId, String service) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doUntieDisable(loginType, loginId, service);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：xx 账号完成二级认证\n\t * @param loginType 账号类别\n\t * @param tokenValue token值\n\t * @param service 指定服务 \n\t * @param safeTime 认证时间，单位：秒 \n\t */\n\tpublic static void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doOpenSafe(loginType, tokenValue, service, safeTime);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：xx 账号关闭二级认证\n\t * @param loginType 账号类别\n\t * @param service 指定服务 \n\t * @param tokenValue token值\n\t */\n\tpublic static void doCloseSafe(String loginType, String tokenValue, String service) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doCloseSafe(loginType, tokenValue, service);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：创建了一个新的 SaSession\n\t * @param id SessionId\n\t */\n\tpublic static void doCreateSession(String id) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doCreateSession(id);\n\t\t}\n\t}\n\t\n\t/**\n\t * 事件发布：一个 SaSession 注销了\n\t * @param id SessionId\n\t */\n\tpublic static void doLogoutSession(String id) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doLogoutSession(id);\n\t\t}\n\t}\n\n\t/**\n\t * 每次 Token 续期时触发（注意：是 timeout 续期，而不是 active-timeout 续期）\n\t *\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue token 值\n\t * @param timeout 续期时间\n\t */\n\tpublic static void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doRenewTimeout(loginType, loginId, tokenValue, timeout);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：有新的全局组件载入到框架中\n\t * @param compName 组件名称\n\t * @param compObj 组件对象\n\t */\n\tpublic static void doRegisterComponent(String compName, Object compObj) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doRegisterComponent(compName, compObj);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：有新的注解处理器载入到框架中\n\t * @param handler 注解处理器\n\t */\n\tpublic static void doRegisterAnnotationHandler(SaAnnotationHandlerInterface<?> handler) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doRegisterAnnotationHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：有新的 StpLogic 载入到框架中\n\t * @param stpLogic / \n\t */\n\tpublic static void doSetStpLogic(StpLogic stpLogic) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doSetStpLogic(stpLogic);\n\t\t}\n\t}\n\n\t/**\n\t * 事件发布：有新的全局配置载入到框架中\n\t * @param config / \n\t */\n\tpublic static void doSetConfig(SaTokenConfig config) {\n\t\tfor (SaTokenListener listener : listenerList) {\n\t\t\tlistener.doSetConfig(config);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListener.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.listener;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.StpLogic;\n\n/**\n * Sa-Token 侦听器\n *\n * <p> 你可以通过实现此接口在用户登录、退出等关键性操作时进行一些AOP切面操作 </p>\n *\n * @author click33\n * @since 1.17.0\n */\npublic interface SaTokenListener {\n\n\t/**\n\t * 每次登录时触发 \n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue 本次登录产生的 token 值 \n\t * @param loginParameter 登录参数\n\t */\n\tvoid doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter);\n\t\t\t\n\t/**\n\t * 每次注销时触发 \n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue token值\n\t */\n\tvoid doLogout(String loginType, Object loginId, String tokenValue);\n\t\n\t/**\n\t * 每次被踢下线时触发 \n\t * @param loginType 账号类别 \n\t * @param loginId 账号id \n\t * @param tokenValue token值 \n\t */\n\tvoid doKickout(String loginType, Object loginId, String tokenValue);\n\n\t/**\n\t * 每次被顶下线时触发\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue token值\n\t */\n\tvoid doReplaced(String loginType, Object loginId, String tokenValue);\n\n\t/**\n\t * 每次被封禁时触发\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param service 指定服务 \n\t * @param level 封禁等级 \n\t * @param disableTime 封禁时长，单位: 秒\n\t */\n\tvoid doDisable(String loginType, Object loginId, String service, int level, long disableTime);\n\t\n\t/**\n\t * 每次被解封时触发\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param service 指定服务 \n\t */\n\tvoid doUntieDisable(String loginType, Object loginId, String service);\n\n\t/**\n\t * 每次打开二级认证时触发\n\t * @param loginType 账号类别\n\t * @param tokenValue token值\n\t * @param service 指定服务 \n\t * @param safeTime 认证时间，单位：秒 \n\t */\n\tvoid doOpenSafe(String loginType, String tokenValue, String service, long safeTime);\n\n\t/**\n\t * 每次关闭二级认证时触发\n\t * @param loginType 账号类别\n\t * @param tokenValue token值\n\t * @param service 指定服务 \n\t */\n\tvoid doCloseSafe(String loginType, String tokenValue, String service);\n\n\t/**\n\t * 每次创建 SaSession 时触发\n\t * @param id SessionId\n\t */\n\tvoid doCreateSession(String id);\n\t\n\t/**\n\t * 每次注销 SaSession 时触发\n\t * @param id SessionId\n\t */\n\tvoid doLogoutSession(String id);\n\n\t/**\n\t * 每次 Token 续期时触发（注意：是 timeout 续期，而不是 active-timeout 续期）\n\t *\n\t * @param loginType 账号类别\n\t * @param loginId 账号id\n\t * @param tokenValue token 值\n\t * @param timeout 续期时间 \n\t */\n\tvoid doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout);\n\n\t/**\n\t * 全局组件载入 \n\t * @param compName 组件名称\n\t * @param compObj 组件对象\n\t */\n\tdefault void doRegisterComponent(String compName, Object compObj) {}\n\n\t/**\n\t * 注册了自定义注解处理器\n\t * @param handler 注解处理器\n\t */\n\tdefault void doRegisterAnnotationHandler(SaAnnotationHandlerInterface<?> handler) {}\n\n\t/**\n\t * StpLogic 对象替换 \n\t * @param stpLogic / \n\t */\n\tdefault void doSetStpLogic(StpLogic stpLogic) {}\n\n\t/**\n\t * 载入全局配置 \n\t * @param config / \n\t */\n\tdefault void doSetConfig(SaTokenConfig config) {}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.listener;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport static cn.dev33.satoken.SaManager.log;\n\n/**\n * Sa-Token 侦听器的一个实现：Log 打印\n * \n * @author click33\n * @since 1.33.0\n */\npublic class SaTokenListenerForLog implements SaTokenListener {\n\n\t/**\n\t * 每次登录时触发 \n\t */\n\t@Override\n\tpublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n\t\tlog.info(\"账号 {} 登录成功 (loginType={}), 会话凭证 token={}\", loginId, loginType, tokenValue);\n\t}\n\n\t/**\n\t * 每次注销时触发 \n\t */\n\t@Override\n\tpublic void doLogout(String loginType, Object loginId, String tokenValue) {\n\t\tlog.info(\"账号 {} 注销登录 (loginType={}), 会话凭证 token={}\", loginId, loginType, tokenValue);\n\t}\n\n\t/**\n\t * 每次被踢下线时触发\n\t */\n\t@Override\n\tpublic void doKickout(String loginType, Object loginId, String tokenValue) {\n\t\tlog.info(\"账号 {} 被踢下线 (loginType={}), 会话凭证 token={}\", loginId, loginType, tokenValue);\n\t}\n\n\t/**\n\t * 每次被顶下线时触发\n\t */\n\t@Override\n\tpublic void doReplaced(String loginType, Object loginId, String tokenValue) {\n\t\tlog.info(\"账号 {} 被顶下线 (loginType={}), 会话凭证 token={}\", loginId, loginType, tokenValue);\n\t}\n\n\t/**\n\t * 每次被封禁时触发\n\t */\n\t@Override\n\tpublic void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {\n\t\tlog.info(\"账号 {} [{}服务] 被封禁 (loginType={}), 封禁等级={}, 解封时间为 {}\", loginId, loginType, service, level, SaFoxUtil.formatAfterDate(disableTime * 1000));\n\t}\n\n\t/**\n\t * 每次被解封时触发\n\t */\n\t@Override\n\tpublic void doUntieDisable(String loginType, Object loginId, String service) {\n\t\tlog.info(\"账号 {} [{}服务] 解封成功 (loginType={})\", loginId, service, loginType);\n\t}\n\t\n\t/**\n\t * 每次打开二级认证时触发\n\t */\n\t@Override\n\tpublic void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {\n\t\tlog.info(\"token 二级认证成功, 业务标识={}, 有效期={}秒, Token值={}\", service, safeTime, tokenValue);\n\t}\n\n\t/**\n\t * 每次关闭二级认证时触发\n\t */\n\t@Override\n\tpublic void doCloseSafe(String loginType, String tokenValue, String service) {\n\t\tlog.info(\"token 二级认证关闭, 业务标识={}, Token值={}\", service, tokenValue);\n\t}\n\n\t/**\n\t * 每次创建Session时触发\n\t */\n\t@Override\n\tpublic void doCreateSession(String id) {\n\t\tlog.info(\"SaSession [{}] 创建成功\", id);\n\t}\n\n\t/**\n\t * 每次注销Session时触发\n\t */\n\t@Override\n\tpublic void doLogoutSession(String id) {\n\t\tlog.info(\"SaSession [{}] 注销成功\", id);\n\t}\n\n\t/**\n\t * 每次 Token 续期时触发\n\t */\n\t@Override\n\tpublic void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {\n\t\tlog.info(\"token 续期成功, {} 秒后到期, 帐号={}, token值={} \", timeout, loginId, tokenValue);\n\t}\n\n\t/**\n\t * 全局组件载入 \n\t * @param compName 组件名称\n\t * @param compObj 组件对象\n\t */\n\t@Override\n\tpublic void doRegisterComponent(String compName, Object compObj) {\n\t\tString canonicalName = compObj == null ? null : compObj.getClass().getCanonicalName();\n\t\tlog.info(\"全局组件 {} 载入成功: {}\", compName, canonicalName);\n\t}\n\n\t/**\n\t * 注册了自定义注解处理器\n\t * @param handler 注解处理器\n\t */\n\t@Override\n\tpublic void doRegisterAnnotationHandler(SaAnnotationHandlerInterface<?> handler) {\n\t\tif(handler != null) {\n\t\t\tlog.info(\"注解扩展 @{} (处理器: {})\", handler.getHandlerAnnotationClass().getSimpleName(), handler.getClass().getCanonicalName());\n\t\t}\n\t}\n\n\t/**\n\t * StpLogic 对象替换 \n\t * @param stpLogic / \n\t */\n\t@Override\n\tpublic void doSetStpLogic(StpLogic stpLogic) {\n\t\tif(stpLogic != null) {\n\t\t\tlog.info(\"会话组件 StpLogic(type={}) 重置成功: {}\", stpLogic.getLoginType(), stpLogic.getClass());\n\t\t}\n\t}\n\n\t/**\n\t * 载入全局配置 \n\t * @param config / \n\t */\n\t@Override\n\tpublic void doSetConfig(SaTokenConfig config) {\n\t\tif(config != null) {\n\t\t\tlog.info(\"全局配置 {} \", config);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForSimple.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.listener;\n\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\n\n/**\n * Sa-Token 侦听器，默认空实现 \n * \n * <p> 对所有事件方法提供空实现，方便开发者通过继承此类快速实现一个可用的侦听器 </p>\n * \n * @author click33\n * @since 1.31.0\n */\npublic class SaTokenListenerForSimple implements SaTokenListener {\n\n\t@Override\n\tpublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doLogout(String loginType, Object loginId, String tokenValue) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doKickout(String loginType, Object loginId, String tokenValue) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doReplaced(String loginType, Object loginId, String tokenValue) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doUntieDisable(String loginType, Object loginId, String service) {\n\t\t\n\t}\n\t\n\t@Override\n\tpublic void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doCloseSafe(String loginType, String tokenValue, String service) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doCreateSession(String id) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doLogoutSession(String id) {\n\t\t\n\t}\n\n\t@Override\n\tpublic void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {\n\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/log/SaLog.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.log;\n\n/**\n * Sa-Token 日志输出接口\n * \n * @author click33\n * @since 1.33.0\n */\npublic interface SaLog {\n\n    /**\n     * 输出 trace 日志 \n     * @param str 日志内容\n     * @param args 参数列表\n     */\n    void trace(String str, Object ...args);\n\n    /**\n     * 输出 debug 日志 \n     * @param str 日志内容\n     * @param args 参数列表\n     */\n    void debug(String str, Object ...args);\n\n    /**\n     * 输出 info 日志 \n     * @param str 日志内容\n     * @param args 参数列表\n     */\n    void info(String str, Object ...args);\n\n    /**\n     * 输出 warn 日志 \n     * @param str 日志内容\n     * @param args 参数列表\n     */\n    void warn(String str, Object ...args);\n\n    /**\n     * 输出 error 日志 \n     * @param str 日志内容\n     * @param args 参数列表\n     */\n    void error(String str, Object ...args);\n\n    /**\n     * 输出 fatal 日志 \n     * @param str 日志内容\n     * @param args 参数列表\n     */\n    void fatal(String str, Object ...args);\n    \n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/log/SaLogForConsole.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.log;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.util.StrFormatter;\n\n/**\n * Sa-Token 日志实现类 [ 控制台打印 ]\n * \n * @author click33\n * @since 1.33.0\n */\npublic class SaLogForConsole implements SaLog {\n\n\t/**\n\t * 日志等级 \n\t */\n\tpublic static final int trace = 1;\n\tpublic static final int debug = 2;\n\tpublic static final int info = 3;\n\tpublic static final int warn = 4;\n\tpublic static final int error = 5;\n\tpublic static final int fatal = 6;\n\n\t/**\n\t * 日志输出的前缀\n\t */\n\tpublic static String LOG_PREFIX = \"SaLog -->: \";\n\tpublic static String TRACE_PREFIX = \"SA [TRACE]-->: \";\n\tpublic static String DEBUG_PREFIX = \"SA [DEBUG]-->: \";\n\tpublic static String INFO_PREFIX  = \"SA [INFO] -->: \";\n\tpublic static String WARN_PREFIX  = \"SA [WARN] -->: \";\n\tpublic static String ERROR_PREFIX = \"SA [ERROR]-->: \";\n\tpublic static String FATAL_PREFIX = \"SA [FATAL]-->: \";\n\n\t/**\n\t * 日志输出的颜色\n\t */\n\tpublic static String TRACE_COLOR = \"\\033[39m\";\n\tpublic static String DEBUG_COLOR = \"\\033[34m\";\n\tpublic static String INFO_COLOR  = \"\\033[32m\";\n\tpublic static String WARN_COLOR  = \"\\033[33m\";\n\tpublic static String ERROR_COLOR = \"\\033[31m\";\n\tpublic static String FATAL_COLOR = \"\\033[35m\";\n\n\tpublic static String DEFAULT_COLOR = \"\\033[39m\";\n\n\t@Override\n\tpublic void trace(String str, Object... args) {\n\t\tprintln(trace, TRACE_COLOR, TRACE_PREFIX, str, args);\n\t}\n\n\t@Override\n\tpublic void debug(String str, Object... args) {\n\t\tprintln(debug, DEBUG_COLOR, DEBUG_PREFIX, str, args);\n\t}\n\n\t@Override\n\tpublic void info(String str, Object... args) {\n\t\tprintln(info, INFO_COLOR, INFO_PREFIX, str, args);\n\t}\n\n\t@Override\n\tpublic void warn(String str, Object... args) {\n\t\tprintln(warn, WARN_COLOR, WARN_PREFIX, str, args);\n\t}\n\n\t@Override\n\tpublic void error(String str, Object... args) {\n\t\tprintln(error, ERROR_COLOR, ERROR_PREFIX, str, args);\n\t}\n\n\t@Override\n\tpublic void fatal(String str, Object... args) {\n\t\tprintln(fatal, FATAL_COLOR, FATAL_PREFIX, str, args);\n\t}\n\n\t/**\n\t * 打印日志到控制台 \n\t * @param level 日志等级\n\t * @param color 颜色编码\n\t * @param prefix 前缀\n\t * @param str 字符串\n\t * @param args 参数列表 \n\t */\n\tpublic void println(int level, String color, String prefix, String str, Object... args) {\n\t\tSaTokenConfig config = SaManager.getConfig();\n\t\tif(config.getIsLog() && level >= config.getLogLevelInt()) {\n\t\t\tif(config.getIsColorLog() == Boolean.TRUE) {\n\t\t\t\t// 彩色日志\n\t\t\t\tSystem.out.println(color + prefix + StrFormatter.format(str, args) + DEFAULT_COLOR);\n\t\t\t} else {\n\t\t\t\t// 黑白日志\n\t\t\t\tSystem.out.println(prefix + StrFormatter.format(str, args));\n\t\t\t}\n\t\t}\n\t}\n\n\t/*\n\t\t// 三种写法速度对比\n\t\t// if( config.getIsColorLog() != null && config.getIsColorLog() )  10亿次，2058ms\n\t\t// if( config.getIsColorLog() == Boolean.TRUE ) \t10亿次，1050ms   最快\n\t\t// if( Objects.equals(config.getIsColorLog(), Boolean.TRUE) )  \t10亿次，1543ms\n\t */\n\n\t/*\n\t\t颜色参考：\n\t\t\tDEFAULT  \t39\n\t\t\tBLACK  \t\t30\n\t\t\tRED  \t\t31\n\t\t\tGREEN  \t\t32\n\t\t\tYELLOW  \t33\n\t\t\tBLUE  \t\t34\n\t\t\tMAGENTA  \t35\n\t\t\tCYAN  \t\t36\n\t\t\tWHITE  \t\t37\n\t\t\tBRIGHT_BLACK  \t90\n\t\t\tBRIGHT_RED  \t91\n\t\t\tBRIGHT_GREEN  \t92\n\t\t\tBRIGHT_YELLOW  \t93\n\t\t\tBRIGHT_BLUE  \t94\n\t\t\tBRIGHT_MAGENTA\t95\n\t\t\tBRIGHT_CYAN  \t96\n\t\t\tBRIGHT_WHITE  \t97\n\t */\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/model/wrapperInfo/SaDisableWrapperInfo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.model.wrapperInfo;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\n\n/**\n * 返回值包装类：描述一个账号是否已被封禁等信息\n *\n * @author click33\n * @since 1.40.0\n */\npublic class SaDisableWrapperInfo {\n\n    /**\n     * 是否被封禁\n     */\n    public boolean isDisable;\n\n    /**\n     * 封禁剩余时间，单位：秒（-1=永久封禁，0 or -2=未封禁）\n     */\n    public long disableTime;\n\n    /**\n     * 封禁等级（最小1级，0=未封禁）\n     */\n    public int disableLevel;\n\n    /**\n     * 构建对象\n     *\n     * @param isDisable 是否被封禁\n     * @param disableTime 封禁剩余时间，单位：秒（-1=永久封禁，0 or -2=未封禁）\n     * @param disableLevel 封禁等级（最小1级，0=未封禁）\n     */\n    public SaDisableWrapperInfo(boolean isDisable, long disableTime, int disableLevel) {\n        this.isDisable = isDisable;\n        this.disableTime = disableTime;\n        this.disableLevel = disableLevel;\n    }\n\n    /**\n     * 创建一个已封禁描述对象\n     * @param disableTime 封禁时间\n     * @param disableLevel 封禁等级\n     * @return /\n     */\n    public static SaDisableWrapperInfo createDisabled(long disableTime, int disableLevel) {\n        return new SaDisableWrapperInfo(true, disableTime, disableLevel);\n    }\n\n    /**\n     * 创建一个未封禁描述对象\n     * @return /\n     */\n    public static SaDisableWrapperInfo createNotDisabled() {\n        return new SaDisableWrapperInfo(false, 0, SaTokenConsts.NOT_DISABLE_LEVEL);\n    }\n\n    /**\n     * 创建一个未封禁描述对象，并指定缓存时间，指定时间内不再重复查询\n     * @param cacheTime 缓存时间（单位：秒）\n     * @return /\n     */\n    public static SaDisableWrapperInfo createNotDisabled(long cacheTime) {\n        return new SaDisableWrapperInfo(false, cacheTime, SaTokenConsts.NOT_DISABLE_LEVEL);\n    }\n\n    @Override\n    public String toString() {\n        return \"SaDisableWrapperInfo{\" +\n                \"isDisable=\" + isDisable +\n                \", disableTime=\" + disableTime +\n                \", disableLevel=\" + disableLevel +\n                '}';\n    }\n\n    // setter / getter 仅为兼容部分框架序列化操作，不建议调用\n\n    public boolean getIsDisable() {\n        return isDisable;\n    }\n\n    public SaDisableWrapperInfo setIsDisable(boolean isDisable) {\n        this.isDisable = isDisable;\n        return this;\n    }\n\n    public long getDisableTime() {\n        return disableTime;\n    }\n\n    public SaDisableWrapperInfo setDisableTime(long disableTime) {\n        this.disableTime = disableTime;\n        return this;\n    }\n\n    public int getDisableLevel() {\n        return disableLevel;\n    }\n\n    public SaDisableWrapperInfo setDisableLevel(int disableLevel) {\n        this.disableLevel = disableLevel;\n        return this;\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPlugin.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\n/**\n * Sa-Token 插件总接口\n *\n * @author click33\n * @since 1.41.0\n */\npublic interface SaTokenPlugin {\n\n\t/**\n\t * 安装插件\n\t */\n\tvoid install();\n\n\t/**\n\t * 卸载插件\n\t */\n\tdefault void destroy(){\n\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.exception.SaTokenPluginException;\nimport cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction;\n\nimport java.io.BufferedReader;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.lang.reflect.InvocationTargetException;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.List;\n\n/**\n * Sa-Token 插件管理器，管理所有插件的加载与卸载\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginHolder {\n\n\t/**\n\t * 默认实例，非单例模式，可替换\n\t */\n\tpublic static SaTokenPluginHolder instance = new SaTokenPluginHolder();\n\n\n\t// ------------------- 插件管理器初始化相关 -------------------\n\n\t/**\n\t * 是否已经加载过插件\n\t */\n\tpublic boolean isLoader = false;\n\n\t/**\n\t * SPI 文件所在目录名称\n\t */\n\tpublic String spiDir = \"satoken\";\n\n\t/**\n\t * 初始化加载所有插件（多次调用只会执行一次）\n\t */\n\tpublic synchronized void init() {\n\t\tif(isLoader) {\n\t\t\treturn;\n\t\t}\n\t\tloaderPlugins();\n\t\tisLoader = true;\n\t}\n\n\t/**\n\t * 根据 SPI 机制加载所有插件\n\t * <p>\n\t *    加载所有 jar 下 /META-INF/satoken/ 目录下 cn.dev33.satoken.plugin.SaTokenPlugin 文件指定的实现类\n\t * </p>\n\t */\n\tpublic synchronized void loaderPlugins() {\n\t\tSaManager.getLog().info(\"SPI plugin loading start ...\");\n\t\tList<SaTokenPlugin> plugins = _loaderPluginsBySpi(SaTokenPlugin.class, spiDir);\n\t\tfor (SaTokenPlugin plugin : plugins) {\n\t\t\tinstallPlugin(plugin);\n\t\t}\n\t\tSaManager.getLog().info(\"SPI plugin loading end ...\");\n\t}\n\n\t/**\n\t * 自定义 SPI 读取策略 （无状态函数）\n\t * @param serviceInterface SPI 接口\n\t * @param dirName 目录名称\n\t * @return /\n\t * @param <T> /\n\t */\n\tprotected <T> List<T> _loaderPluginsBySpi(Class<T> serviceInterface, String dirName) {\n\t\tString path = \"META-INF/\" + dirName + \"/\" + serviceInterface.getName();\n\t\tList<T> providers = new ArrayList<>();\n\t\ttry {\n\t\t\tClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n\t\t\tEnumeration<URL> resources = classLoader.getResources(path);\n\t\t\twhile (resources.hasMoreElements()) {\n\t\t\t\tURL url = resources.nextElement();\n\t\t\t\ttry (InputStream is = url.openStream()) {\n\t\t\t\t\tBufferedReader reader = new BufferedReader(new InputStreamReader(is));\n\t\t\t\t\tString line;\n\t\t\t\t\twhile ((line = reader.readLine()) != null) {\n\t\t\t\t\t\tline = line.trim();\n\t\t\t\t\t\t// 忽略空行和注释行\n\t\t\t\t\t\tif (!line.isEmpty() && !line.startsWith(\"#\")) {\n\t\t\t\t\t\t\tClass<?> clazz = Class.forName(line, true, classLoader);\n\t\t\t\t\t\t\tT instance = serviceInterface.cast(clazz.getDeclaredConstructor().newInstance());\n\t\t\t\t\t\t\tproviders.add(instance);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\tthrow new SaTokenPluginException(\"SPI 插件加载失败: \" + e.getMessage(), e);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenPluginException(\"SPI 插件加载失败: \" + e.getMessage(), e);\n\t\t}\n\t\treturn providers;\n\t}\n\n\n\n\t// ------------------- 插件管理 -------------------\n\n\t/**\n\t * 所有插件的集合\n\t */\n\tprivate final List<SaTokenPlugin> pluginList = new ArrayList<>();\n\n\t/**\n\t * 获取插件集合副本 (拷贝插件集合，而非每个插件实例)\n\t * @return /\n\t */\n\tpublic synchronized List<SaTokenPlugin> getPluginListCopy() {\n\t\treturn new ArrayList<>(pluginList);\n\t}\n\n\t/**\n\t * 判断是否已经安装了指定插件\n\t *\n\t * @param pluginClass 插件类型\n\t * @return /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> boolean isInstalledPlugin(Class<T> pluginClass) {\n\t\tfor (SaTokenPlugin plugin : pluginList) {\n\t\t\tif (plugin.getClass().equals(pluginClass)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 获取指定类型的插件\n\t * @param pluginClass /\n\t * @return /\n\t * @param <T> /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> T getPlugin(Class<T> pluginClass) {\n\t\tfor (SaTokenPlugin plugin : pluginList) {\n\t\t\tif (plugin.getClass().equals(pluginClass)) {\n\t\t\t\treturn (T) plugin;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 消费指定集合的钩子函数，返回消费的数量\n\t * @param pluginClass /\n\t * @param hooks /\n\t * @param <T> /\n\t */\n\tprotected synchronized <T extends SaTokenPlugin> int _consumeHooks(List<SaTokenPluginHookModel<? extends SaTokenPlugin>> hooks, Class<T> pluginClass) {\n\t\tint consumeCount = 0;\n\t\tfor (int i = 0; i < hooks.size(); i++) {\n\t\t\tSaTokenPluginHookModel<? extends SaTokenPlugin> model = hooks.get(i);\n\t\t\tif(model.listenerClass.equals(pluginClass)) {\n\t\t\t\tmodel.executeFunction.execute(getPlugin(pluginClass));\n\t\t\t\thooks.remove(i);\n\t\t\t\ti--;\n\t\t\t\tconsumeCount++;\n\t\t\t}\n\t\t}\n\t\treturn consumeCount;\n\t}\n\n\n\t// ------------------- 插件 Install 与 Destroy -------------------\n\n\t/**\n\t * 安装指定插件\n\t * @param plugin /\n\t */\n\tpublic synchronized SaTokenPluginHolder installPlugin(SaTokenPlugin plugin) {\n\n\t\t// 插件为空，拒绝安装\n\t\tif (plugin == null) {\n\t\t\tthrow new SaTokenPluginException(\"插件不可为空\");\n\t\t}\n\n\t\t// 插件已经被安装过了，拒绝再次安装\n\t\tif (isInstalledPlugin(plugin.getClass())) {\n\t\t\tthrow new SaTokenPluginException(\"插件 [ \" + plugin.getClass().getCanonicalName() + \" ] 已安装，不可重复安装\");\n\t\t}\n\n\t\t// 执行该插件的 install 前置钩子\n\t\t_consumeHooks(beforeInstallHooks, plugin.getClass());\n\n\t\t// 插件安装\n\t\tint consumeCount = _consumeHooks(installHooks, plugin.getClass());\n\t\tif (consumeCount == 0) {\n\t\t\tplugin.install();\n\t\t}\n\n\t\t// 执行该插件的 install 后置钩子\n\t\t_consumeHooks(afterInstallHooks, plugin.getClass());\n\n\t\t// 添加到插件集合\n\t\tpluginList.add(plugin);\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n\t/**\n\t * 安装指定插件，根据插件类型\n\t * @param pluginClass /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder installPlugin(Class<T> pluginClass) {\n\t\ttry {\n\t\t\tT plugin = pluginClass.getDeclaredConstructor().newInstance();\n\t\t\treturn installPlugin(plugin);\n\t\t} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {\n\t\t\tthrow new SaTokenPluginException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 卸载指定插件\n\t * @param plugin /\n\t */\n\tpublic synchronized SaTokenPluginHolder destroyPlugin(SaTokenPlugin plugin) {\n\n\t\t// 插件为空，拒绝卸载\n\t\tif (plugin == null) {\n\t\t\tthrow new SaTokenPluginException(\"插件不可为空\");\n\t\t}\n\n\t\t// 插件未被安装，拒绝卸载\n\t\tif (!isInstalledPlugin(plugin.getClass())) {\n\t\t\tthrow new SaTokenPluginException(\"插件 [ \" + plugin.getClass().getCanonicalName() + \" ] 未安装，无法卸载\");\n\t\t}\n\n\t\t// 执行该插件的 destroy 前置钩子\n\t\t_consumeHooks(beforeDestroyHooks, plugin.getClass());\n\n\t\t// 插件卸载\n\t\tint consumeCount = _consumeHooks(destroyHooks, plugin.getClass());\n\t\tif (consumeCount == 0) {\n\t\t\tplugin.destroy();\n\t\t}\n\n\t\t// 执行该插件的 destroy 后置钩子\n\t\t_consumeHooks(afterDestroyHooks, plugin.getClass());\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n\t/**\n\t * 卸载指定插件，根据插件类型\n\t * @param pluginClass /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder destroyPlugin(Class<T> pluginClass) {\n\t\treturn destroyPlugin(getPlugin(pluginClass));\n\t}\n\n\n\t// ------------------- 插件 Install 钩子 -------------------\n\n\t/**\n\t * 插件 [ Install 钩子 ] 集合\n\t */\n\tprivate final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> installHooks = new ArrayList<>();\n\n\t/**\n\t * 插件 [ Install 前置钩子 ] 集合\n\t */\n\tprivate final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> beforeInstallHooks = new ArrayList<>();\n\n\t/**\n\t * 插件 [ Install 后置钩子 ] 集合\n\t */\n\tprivate final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> afterInstallHooks = new ArrayList<>();\n\n\t/**\n\t * 注册指定插件的 [ Install 钩子 ]，1、同插件支持多次注册。2、如果插件已经安装完毕，则抛出异常。3、注册 Install 钩子的插件默认安装行为将不再执行\n\t * @param listenerClass /\n\t * @param executeFunction /\n\t * @param <T> /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder onInstall(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {\n\t\t// 如果指定的插件已经安装完毕，则不再允许注册前置钩子函数\n\t\tif(isInstalledPlugin(listenerClass)) {\n\t\t\tthrow new SaTokenPluginException(\"插件 [ \" + listenerClass.getCanonicalName() + \" ] 已安装完毕，不允许再注册 Install 钩子函数\");\n\t\t}\n\n\t\t// 堆积到钩子函数集合\n\t\tinstallHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n\t/**\n\t * 注册指定插件的 [ Install 前置钩子 ]，1、同插件支持多次注册。2、如果插件已经安装完毕，则抛出异常\n\t * @param listenerClass /\n\t * @param executeFunction /\n\t * @param <T> /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder onBeforeInstall(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {\n\t\t// 如果指定的插件已经安装完毕，则不再允许注册前置钩子函数\n\t\tif(isInstalledPlugin(listenerClass)) {\n\t\t\tthrow new SaTokenPluginException(\"插件 [ \" + listenerClass.getCanonicalName() + \" ] 已安装完毕，不允许再注册 Install 前置钩子函数\");\n\t\t}\n\n\t\t// 堆积到钩子函数集合\n\t\tbeforeInstallHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n\t/**\n\t * 注册指定插件的 [ Install 后置钩子 ]，1、同插件支持多次注册。2、如果插件已经安装完毕，则立即执行该钩子函数\n\t * @param listenerClass /\n\t * @param executeFunction /\n\t * @param <T> /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder onAfterInstall(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {\n\t\t// 如果指定的插件已经安装完毕，则立即执行该钩子函数\n\t\tif(isInstalledPlugin(listenerClass)) {\n\t\t\texecuteFunction.execute(getPlugin(listenerClass));\n\t\t\treturn this;\n\t\t}\n\n\t\t// 堆积到钩子函数集合\n\t\tafterInstallHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n\n\t// ------------------- 插件 Destroy 钩子 -------------------\n\n\t/**\n\t * 插件 [ Destroy 钩子 ] 集合\n\t */\n\tprivate final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> destroyHooks = new ArrayList<>();\n\n\t/**\n\t * 插件 [ Destroy 前置钩子 ] 集合\n\t */\n\tprivate final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> beforeDestroyHooks = new ArrayList<>();\n\n\t/**\n\t * 插件 [ Destroy 后置钩子 ] 集合\n\t */\n\tprivate final List<SaTokenPluginHookModel<? extends SaTokenPlugin>> afterDestroyHooks = new ArrayList<>();\n\n\t/**\n\t * 注册指定插件的 [ Destroy 钩子 ]，1、同插件支持多次注册。2、注册 Destroy 钩子的插件默认卸载行为将不再执行\n\t * @param listenerClass /\n\t * @param executeFunction /\n\t * @param <T> /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder onDestroy(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {\n\t\tdestroyHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n\t/**\n\t * 注册指定插件的 [ Destroy 前置钩子 ]，同插件支持多次注册\n\t * @param listenerClass /\n\t * @param executeFunction /\n\t * @param <T> /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder onBeforeDestroy(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {\n\t\tbeforeDestroyHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n\t/**\n\t * 注册指定插件的 [ Destroy 后置钩子 ]，同插件支持多次注册\n\t * @param listenerClass /\n\t * @param executeFunction /\n\t * @param <T> /\n\t */\n\tpublic synchronized<T extends SaTokenPlugin> SaTokenPluginHolder onAfterDestroy(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {\n\t\tafterDestroyHooks.add(new SaTokenPluginHookModel<T>(listenerClass, executeFunction));\n\n\t\t// 返回对象自身，支持连缀风格调用\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginHookModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.fun.hooks.SaTokenPluginHookFunction;\n\n/**\n * Sa-Token 插件 Hook Model\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginHookModel<T extends SaTokenPlugin> {\n\n\t/**\n\t * 监听插件类型\n\t */\n\tpublic Class<T> listenerClass;\n\n\t/**\n\t * 执行的方法\n\t */\n\tpublic SaTokenPluginHookFunction<T> executeFunction;\n\n\t/**\n\t * 构造函数\n\t * @param listenerClass /\n\t * @param executeFunction /\n\t */\n\tpublic SaTokenPluginHookModel(Class<T> listenerClass, SaTokenPluginHookFunction<T> executeFunction) {\n\t\tthis.listenerClass = listenerClass;\n\t\tthis.executeFunction = executeFunction;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/router/SaHttpMethod.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.router;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\n\n/**\n * Http 请求各种请求类型的枚举表示 \n * \n * <p> 参考：Spring - HttpMethod \n * \n * @author click33\n * @since 1.27.0\n */\npublic enum SaHttpMethod {\n\t\n\tGET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT, \n\t\n\t/**\n\t * 代表全部请求方式 \n\t */\n\tALL;\n\t\n\tprivate static final Map<String, SaHttpMethod> map = new HashMap<>();\n\n\tstatic {\n\t\tfor (SaHttpMethod reqMethod : values()) {\n\t\t\tmap.put(reqMethod.name(), reqMethod);\n\t\t}\n\t}\n\n\t/**\n\t * String 转 enum \n\t * @param method 请求类型 \n\t * @return SaHttpMethod 对象\n\t */\n\tpublic static SaHttpMethod toEnum(String method) {\n\t\tif(method == null) {\n\t\t\tthrow new SaTokenException(\"Method 不可以是 null\").setCode(SaErrorCode.CODE_10321);\n\t\t}\n\t\tSaHttpMethod reqMethod = map.get(method.toUpperCase());\n\t\tif(reqMethod == null) {\n\t\t\tthrow new SaTokenException(\"无效Method：\" + method).setCode(SaErrorCode.CODE_10321);\n\t\t}\n\t\treturn reqMethod;\n\t}\n\n\t/**\n\t * String[] 转 enum[]\n\t * @param methods 请求类型数组 \n\t * @return SaHttpMethod 数组\n\t */\n\tpublic static SaHttpMethod[] toEnumArray(String... methods) {\n\t\tSaHttpMethod [] arr = new SaHttpMethod[methods.length];\n\t\tfor (int i = 0; i < methods.length; i++) {\n\t\t\tarr[i] = SaHttpMethod.toEnum(methods[i]);\n\t\t}\n\t\treturn arr;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.router;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.fun.SaParamRetFunction;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\nimport java.util.List;\n\n/**\n * 路由匹配操作工具类\n *\n * <p> 提供了一系列的路由匹配操作方法，一般用在全局拦截器、过滤器做路由拦截鉴权。 </p>\n * <p> 简单示例： </p>\n * <pre>\n *    \t// 指定一条 match 规则\n *    \tSaRouter\n *    \t   \t.match(\"/**\")    // 拦截的 path 列表，可以写多个\n *   \t   \t.notMatch(\"/user/doLogin\")        // 排除掉的 path 列表，可以写多个\n *   \t   \t.check(r->StpUtil.checkLogin());        // 要执行的校验动作，可以写完整的 lambda 表达式\n * </pre>\n *\n * @author click33\n * @since 1.27.0\n */\npublic class SaRouter {\n\n\tprivate SaRouter() {\n\t}\n\t\n\t// -------------------- 路由匹配相关 -------------------- \n\t\n\t/**\n\t * 路由匹配\n\t * @param pattern 路由匹配符 \n\t * @param path 被匹配的路由  \n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatch(String pattern, String path) {\n\t\treturn SaStrategy.instance.routeMatcher.apply(pattern, path);\n\t}\n\n\t/**\n\t * 路由匹配   \n\t * @param patterns 路由匹配符集合 \n\t * @param path 被匹配的路由  \n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatch(List<String> patterns, String path) {\n\t\tif(patterns == null) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (String pattern : patterns) {\n\t\t\tif(isMatch(pattern, path)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t/**\n\t * 路由匹配   \n\t * @param patterns 路由匹配符数组  \n\t * @param path 被匹配的路由  \n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatch(String[] patterns, String path) {\n\t\tif(patterns == null) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (String pattern : patterns) {\n\t\t\tif(isMatch(pattern, path)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Http请求方法匹配 \n\t * @param methods Http请求方法断言数组  \n\t * @param methodString Http请求方法\n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatch(SaHttpMethod[] methods, String methodString) {\n\t\tif(methods == null) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (SaHttpMethod method : methods) {\n\t\t\tif(method == SaHttpMethod.ALL || (method != null && method.toString().equalsIgnoreCase(methodString))) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t// ------ 使用当前URI匹配 \n\t\n\t/**\n\t * 路由匹配 (使用当前URI) \n\t * @param pattern 路由匹配符 \n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatchCurrURI(String pattern) {\n\t\treturn isMatch(pattern, SaHolder.getRequest().getRequestPath());\n\t}\n\n\t/**\n\t * 路由匹配 (使用当前URI) \n\t * @param patterns 路由匹配符集合 \n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatchCurrURI(List<String> patterns) {\n\t\treturn isMatch(patterns, SaHolder.getRequest().getRequestPath());\n\t}\n\n\t/**\n\t * 路由匹配 (使用当前URI) \n\t * @param patterns 路由匹配符数组 \n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatchCurrURI(String[] patterns) {\n\t\treturn isMatch(patterns, SaHolder.getRequest().getRequestPath());\n\t}\n\n\t/**\n\t * Http请求方法匹配 (使用当前请求方式) \n\t * @param methods Http请求方法断言数组  \n\t * @return 是否匹配成功 \n\t */\n\tpublic static boolean isMatchCurrMethod(SaHttpMethod[] methods) {\n\t\treturn isMatch(methods, SaHolder.getRequest().getMethod());\n\t}\n\t\n\n\t// -------------------- 开始匹配 -------------------- \n\t\n\t/**\n\t * 初始化一个SaRouterStaff，开始匹配\n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff newMatch() {\n\t\treturn new SaRouterStaff();\n\t}\n\n\t// ----------------- path匹配 \n\t\n\t/**\n\t * 路由匹配 \n\t * @param patterns 路由匹配符集合\n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff match(String... patterns) {\n\t\treturn new SaRouterStaff().match(patterns);\n\t}\n\n\t/**\n\t * 路由匹配排除 \n\t * @param patterns 路由匹配符排除数组  \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff notMatch(String... patterns) {\n\t\treturn new SaRouterStaff().notMatch(patterns);\n\t}\n\n\t/**\n\t * 路由匹配 \n\t * @param patterns 路由匹配符集合 \n\t * @return 对象自身 \n\t */\n\tpublic static SaRouterStaff match(List<String> patterns) {\n\t\treturn new SaRouterStaff().match(patterns);\n\t}\n\n\t/**\n\t * 路由匹配排除 \n\t * @param patterns 路由匹配符排除集合 \n\t * @return 对象自身 \n\t */\n\tpublic static SaRouterStaff notMatch(List<String> patterns) {\n\t\treturn new SaRouterStaff().notMatch(patterns);\n\t}\n\n\t// ----------------- Method匹配 \n\t\n\t/**\n\t * Http请求方式匹配 (Enum) \n\t * @param methods Http请求方法断言数组  \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff match(SaHttpMethod... methods) {\n\t\treturn new SaRouterStaff().match(methods);\n\t}\n\n\t/**\n\t * Http请求方法匹配排除 (Enum) \n\t * @param methods Http请求方法断言排除数组  \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff notMatch(SaHttpMethod... methods) {\n\t\treturn new SaRouterStaff().notMatch(methods);\n\t}\n\n\t/**\n\t * Http请求方法匹配 (String)  \n\t * @param methods Http请求方法断言数组  \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff matchMethod(String... methods) {\n\t\treturn new SaRouterStaff().matchMethod(methods);\n\t}\n\n\t/**\n\t * Http请求方法匹配排除 (String) \n\t * @param methods Http请求方法断言排除数组  \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff notMatchMethod(String... methods) {\n\t\treturn new SaRouterStaff().notMatchMethod(methods);\n\t}\n\t\n\t// ----------------- 条件匹配 \n\n\t/**\n\t * 根据 boolean 值进行匹配 \n\t * @param flag boolean值 \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff match(boolean flag) {\n\t\treturn new SaRouterStaff().match(flag);\n\t}\n\n\t/**\n\t * 根据 boolean 值进行匹配排除 \n\t * @param flag boolean值 \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff notMatch(boolean flag) {\n\t\treturn new SaRouterStaff().notMatch(flag);\n\t}\n\t\n\t/**\n\t * 根据自定义方法进行匹配 (lazy) \n\t * @param fun 自定义方法\n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff match(SaParamRetFunction<Object, Boolean> fun) {\n\t\treturn new SaRouterStaff().match(fun);\n\t}\n\n\t/**\n\t * 根据自定义方法进行匹配排除 (lazy) \n\t * @param fun 自定义排除方法\n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff notMatch(SaParamRetFunction<Object, Boolean> fun) {\n\t\treturn new SaRouterStaff().notMatch(fun);\n\t}\n\n\t\n\t// -------------------- 直接指定check函数 -------------------- \n\t\n\t/**\n\t * 路由匹配，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符\n\t * @param fun 要执行的校验方法 \n\t * @return /\n\t */\n\tpublic static SaRouterStaff match(String pattern, SaFunction fun) {\n\t\treturn new SaRouterStaff().match(pattern, fun);\n\t}\n\n\t/**\n\t * 路由匹配，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符\n\t * @param fun 要执行的校验方法 \n\t * @return /\n\t */\n\tpublic static SaRouterStaff match(String pattern, SaParamFunction<SaRouterStaff> fun) {\n\t\treturn new SaRouterStaff().match(pattern, fun);\n\t}\n\n\t/**\n\t * 路由匹配 (并指定排除匹配符)，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符 \n\t * @param excludePattern 要排除的路由匹配符 \n\t * @param fun 要执行的方法 \n\t * @return /\n\t */\n\tpublic static SaRouterStaff match(String pattern, String excludePattern, SaFunction fun) {\n\t\treturn new SaRouterStaff().match(pattern, excludePattern, fun);\n\t}\n\n\t/**\n\t * 路由匹配 (并指定排除匹配符)，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符 \n\t * @param excludePattern 要排除的路由匹配符 \n\t * @param fun 要执行的方法 \n\t * @return /\n\t */\n\tpublic static SaRouterStaff match(String pattern, String excludePattern, SaParamFunction<SaRouterStaff> fun) {\n\t\treturn new SaRouterStaff().match(pattern, excludePattern, fun);\n\t}\n\n\t\n\t// -------------------- 提前退出 -------------------- \n\t\n\t/**\n\t * 停止匹配，跳出函数 (在多个匹配链中一次性跳出Auth函数) \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff stop() {\n\t\tthrow new StopMatchException();\n\t}\n\n\t/**\n\t * 停止匹配，结束执行，向前端返回结果 \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff back() {\n\t\tthrow new BackResultException(\"\");\n\t}\n\t\n\t/**\n\t * 停止匹配，结束执行，向前端返回结果 \n\t * @param result 要输出的结果 \n\t * @return SaRouterStaff\n\t */\n\tpublic static SaRouterStaff back(Object result) {\n\t\tthrow new BackResultException(result);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/router/SaRouterStaff.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.router;\n\nimport java.util.List;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.fun.SaParamRetFunction;\n\n/**\n * 路由匹配操作对象 \n * \n * @author click33\n * @since 1.27.0\n */\npublic class SaRouterStaff {\n\n\t/**\n\t * 是否命中的标记变量 \n\t */\n\tpublic boolean isHit = true;\n\t\n\t/**\n\t * @return 是否命中 \n\t */\n\tpublic boolean isHit() {\n\t\treturn isHit;\n\t}\n\n\t/**\n\t * @param isHit 命中标记 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff setHit(boolean isHit) {\n\t\tthis.isHit = isHit;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重置命中标记为 true \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff reset() {\n\t\tthis.isHit = true;\n\t\treturn this;\n\t}\n\t\n\t\n\t// ----------------- path匹配 \n\t\n\t/**\n\t * 路由匹配 \n\t * @param patterns 路由匹配符数组  \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff match(String... patterns) {\n\t\tif(isHit)  {\n\t\t\tisHit = SaRouter.isMatchCurrURI(patterns);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 路由匹配排除 \n\t * @param patterns 路由匹配符排除数组  \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff notMatch(String... patterns) {\n\t\tif(isHit)  {\n\t\t\tisHit = !SaRouter.isMatchCurrURI(patterns);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 路由匹配 \n\t * @param patterns 路由匹配符集合 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff match(List<String> patterns) {\n\t\tif(isHit)  {\n\t\t\tisHit = SaRouter.isMatchCurrURI(patterns);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 路由匹配排除 \n\t * @param patterns 路由匹配符排除集合 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff notMatch(List<String> patterns) {\n\t\tif(isHit)  {\n\t\t\tisHit = !SaRouter.isMatchCurrURI(patterns);\n\t\t}\n\t\treturn this;\n\t}\n\n\t// ----------------- Method匹配 \n\n\t/**\n\t * Http请求方法匹配 (Enum) \n\t * @param methods Http请求方法断言数组  \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff match(SaHttpMethod... methods) {\n\t\tif(isHit)  {\n\t\t\tisHit = SaRouter.isMatchCurrMethod(methods);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Http请求方法匹配排除 (Enum) \n\t * @param methods Http请求方法断言排除数组  \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff notMatch(SaHttpMethod... methods) {\n\t\tif(isHit)  {\n\t\t\tisHit = !SaRouter.isMatchCurrMethod(methods);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Http请求方法匹配 (String) \n\t * @param methods Http请求方法断言数组  \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff matchMethod(String... methods) {\n\t\tif(isHit)  {\n\t\t\tSaHttpMethod [] arr = SaHttpMethod.toEnumArray(methods);\n\t\t\tisHit = SaRouter.isMatchCurrMethod(arr);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Http请求方法匹配排除 (String) \n\t * @param methods Http请求方法断言排除数组  \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff notMatchMethod(String... methods) {\n\t\tif(isHit)  {\n\t\t\tSaHttpMethod [] arr = SaHttpMethod.toEnumArray(methods);\n\t\t\tisHit = !SaRouter.isMatchCurrMethod(arr);\n\t\t}\n\t\treturn this;\n\t}\n\n\n\t// ----------------- 条件匹配 \n\n\t/**\n\t * 根据 boolean 值进行匹配 \n\t * @param flag boolean值 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff match(boolean flag) {\n\t\tif(isHit)  {\n\t\t\tisHit = flag;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 根据 boolean 值进行匹配排除 \n\t * @param flag boolean值 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff notMatch(boolean flag) {\n\t\tif(isHit)  {\n\t\t\tisHit = !flag;\n\t\t}\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 根据自定义方法进行匹配 (lazy)  \n\t * @param fun 自定义方法\n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff match(SaParamRetFunction<Object, Boolean> fun) {\n\t\tif(isHit)  {\n\t\t\tisHit = fun.run(this);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 根据自定义方法进行匹配排除 (lazy) \n\t * @param fun 自定义排除方法\n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff notMatch(SaParamRetFunction<Object, Boolean> fun) {\n\t\tif(isHit)  {\n\t\t\tisHit = !fun.run(this);\n\t\t}\n\t\treturn this;\n\t}\n\t\n\t\n\t// ----------------- 函数校验执行 \n\n\t/**\n\t * 执行校验函数 (无参) \n\t * @param fun 要执行的函数 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff check(SaFunction fun) {\n\t\tif(isHit)  {\n\t\t\tfun.run();\n\t\t}\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 执行校验函数 (带参) \n\t * @param fun 要执行的函数 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff check(SaParamFunction<SaRouterStaff> fun) {\n\t\tif(isHit)  {\n\t\t\tfun.run(this);\n\t\t}\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 自由匹配 （ 在free作用域里执行stop()不会跳出Auth函数，而是仅仅跳出free代码块 ）\n\t * @param fun 要执行的函数 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff free(SaParamFunction<SaRouterStaff> fun) {\n\t\tif(isHit)  {\n\t\t\ttry {\n\t\t\t\tfun.run(this);\n\t\t\t} catch (StopMatchException e) {\n\t\t\t\t// 跳出 free自由匹配代码块 \n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t\n\t// ----------------- 直接指定check函数 \n\t\n\t/**\n\t * 路由匹配，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符\n\t * @param fun 要执行的校验方法 \n\t * @return /\n\t */\n\tpublic SaRouterStaff match(String pattern, SaFunction fun) {\n\t\treturn this.match(pattern).check(fun);\n\t}\n\n\t/**\n\t * 路由匹配，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符\n\t * @param fun 要执行的校验方法 \n\t * @return /\n\t */\n\tpublic SaRouterStaff match(String pattern, SaParamFunction<SaRouterStaff> fun) {\n\t\treturn this.match(pattern).check(fun);\n\t}\n\n\t/**\n\t * 路由匹配 (并指定排除匹配符)，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符 \n\t * @param excludePattern 要排除的路由匹配符 \n\t * @param fun 要执行的方法 \n\t * @return /\n\t */\n\tpublic SaRouterStaff match(String pattern, String excludePattern, SaFunction fun) {\n\t\treturn this.match(pattern).notMatch(excludePattern).check(fun);\n\t}\n\n\t/**\n\t * 路由匹配 (并指定排除匹配符)，如果匹配成功则执行认证函数 \n\t * @param pattern 路由匹配符 \n\t * @param excludePattern 要排除的路由匹配符 \n\t * @param fun 要执行的方法 \n\t * @return /\n\t */\n\tpublic SaRouterStaff match(String pattern, String excludePattern, SaParamFunction<SaRouterStaff> fun) {\n\t\treturn this.match(pattern).notMatch(excludePattern).check(fun);\n\t}\n\t\n\t\n\t// ----------------- 提前退出 \n\n\t/**\n\t * 停止匹配，跳出函数 (在多个匹配链中一次性跳出Auth函数) \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff stop() {\n\t\tif(isHit) {\n\t\t\tthrow new StopMatchException();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 停止匹配，结束执行，向前端返回结果 \n\t * @return 对象自身 \n\t */\n\tpublic SaRouterStaff back() {\n\t\tif(isHit) {\n\t\t\tthrow new BackResultException(\"\");\n\t\t}\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 停止匹配，结束执行，向前端返回结果 \n\t * @return 对象自身 \n\t * @param result 要输出的结果 \n\t */\n\tpublic SaRouterStaff back(Object result) {\n\t\tif(isHit) {\n\t\t\tthrow new BackResultException(result);\n\t\t}\n\t\treturn this;\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.same;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SameTokenInvalidException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * Sa Same-Token 同源系统身份认证模块 - 模板方法类 \n * \n * <p> 解决同源系统互相调用时的身份认证校验， 例如：微服务网关请求转发鉴权、微服务RPC调用鉴权 \n * \n * @author click33\n * @since 1.32.0\n */\npublic class SaSameTemplate {\n\n\t/**\n\t * 提交 Same-Token 时，建议使用的参数名称 \n\t */\n\t public static final String SAME_TOKEN = \"SA-SAME-TOKEN\";\n\t\n\t// -------------------- 获取 & 校验 \n\t\n\t/**\n\t * 获取当前 Same-Token, 如果不存在，则立即创建并返回 \n\t * @return / \n\t */\n\tpublic String getToken() {\n\t\tString currentToken = getTokenNh(); \n\t\tif(SaFoxUtil.isEmpty(currentToken)) {\n\t\t\t// 注意这里的自刷新不能做到高并发可用 \n\t\t\tcurrentToken = refreshToken();\n\t\t}\n\t\treturn currentToken;\n\t}\n\n\t/**\n\t * 判断一个 Same-Token 是否有效 \n\t * @param token / \n\t * @return /\n\t */\n\tpublic boolean isValid(String token) {\n\t\t// 1、 如果传入的 token 为空，则立即返回 false \n\t\tif(SaFoxUtil.isEmpty(token)) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// 2、 验证当前 Same-Token 及 Past-Same-Token \n\t\treturn token.equals(getToken()) || token.equals(getPastTokenNh());\n\t}\n\n\t/**\n\t * 校验一个 Same-Token 是否有效 (如果无效则抛出异常) \n\t * @param token / \n\t */\n\tpublic void checkToken(String token) {\n\t\tif( ! isValid(token)) {\n\t\t\ttoken = (token == null ? \"\" : token);\n\t\t\tthrow new SameTokenInvalidException(\"无效Same-Token：\" + token).setCode(SaErrorCode.CODE_10301);\n\t\t}\n\t}\n\n\t/**\n\t * 校验当前 Request 上下文提供的 Same-Token 是否有效 (如果无效则抛出异常) \n\t */\n\tpublic void checkCurrentRequestToken() {\n\t\tcheckToken(SaHolder.getRequest().getHeader(SAME_TOKEN));\n\t}\n\t\n\t/**\n\t * 刷新一次 Same-Token (注意集群环境中不要多个服务重复调用) \n\t * @return 刷新后产生的新 Same-Token \n\t */\n\tpublic String refreshToken() {\n\t\t\n\t\t// 1. 先将当前 Same-Token 写入到 Past-Same-Token 中 \n\t\tString sameToken = getTokenNh(); \n\t\tif( ! SaFoxUtil.isEmpty(sameToken)) {\n\t\t\tsavePastToken(sameToken, getTokenTimeout());\n\t\t}\n\t\t\n\t\t// 2. 再刷新当前 Same-Token\n\t\tString newSameToken = createToken();\n\t\tsaveToken(newSameToken);\n\t\t\n\t\t// 3. 返回新的 Same-Token\n\t\treturn newSameToken;\n\t}\n\n\t\n\t// ------------------------------ 保存Token \n\t\n\t/**\n\t * 保存 Same-Token\n\t * @param token / \n\t */\n\tpublic void saveToken(String token) {\n\t\tif(SaFoxUtil.isEmpty(token)) {\n\t\t\treturn;\n\t\t}\n\t\tSaManager.getSaTokenDao().set(splicingTokenSaveKey(), token, SaManager.getConfig().getSameTokenTimeout());\n\t}\n\t\n\t/**\n\t * 保存 Past-Same-Token\n\t * @param token token\n\t * @param timeout 有效期（单位：秒）\n\t */\n\tpublic void savePastToken(String token, long timeout){\n\t\tif(SaFoxUtil.isEmpty(token)) {\n\t\t\treturn;\n\t\t}\n\t\tSaManager.getSaTokenDao().set(splicingPastTokenSaveKey(), token, timeout);\n\t}\n\t\n\t\n\t// -------------------- 获取Token \n\t\n\t/**\n\t * 获取 Same-Token，不做任何处理 \n\t * @return / \n\t */\n\tpublic String getTokenNh() {\n\t\treturn SaManager.getSaTokenDao().get(splicingTokenSaveKey());\n\t}\n\t\n\t/**\n\t * 获取 Past-Same-Token，不做任何处理 \n\t * @return / \n\t */\n\tpublic String getPastTokenNh() {\n\t\treturn SaManager.getSaTokenDao().get(splicingPastTokenSaveKey());\n\t}\n\n\t/**\n\t * 获取 Same-Token 的剩余有效期 (单位：秒) \n\t * @return / \n\t */\n\tpublic long getTokenTimeout() {\n\t\treturn SaManager.getSaTokenDao().getTimeout(splicingTokenSaveKey());\n\t}\n\t\n\n\t// -------------------- 创建Token \n\t\n\t/**\n\t * 创建一个 Same-Token \n\t * @return Token \n\t */\n\tpublic String createToken() {\n\t\treturn SaFoxUtil.getRandomString(64);\n\t}\n\n\n\t// -------------------- 拼接key \n\n\t/** \n\t * 拼接key：Same-Token 的存储 key\n\t * @return key\n\t */\n\tpublic String splicingTokenSaveKey() {\n\t\treturn SaManager.getConfig().getTokenName() + \":var:same-token\";\n\t}\n\n\t/** \n\t * 拼接key：次级 Same-Token 的存储 key\n\t * @return key\n\t */\n\tpublic String splicingPastTokenSaveKey() {\n\t\treturn SaManager.getConfig().getTokenName() + \":var:past-same-token\";\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/same/SaSameUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.same;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * Sa Same-Token 同源系统身份认证模块 - 工具类 \n * \n * <p> 解决同源系统互相调用时的身份认证校验， 例如：微服务网关请求转发鉴权、微服务RPC调用鉴权 \n * \n * @author click33\n * @since 1.32.0\n */\npublic class SaSameUtil {\n\n\tprivate SaSameUtil(){}\n\n\t/**\n\t * 提交 Same-Token 时，建议使用的参数名称 \n\t */\n\tpublic static final String SAME_TOKEN = SaSameTemplate.SAME_TOKEN;\n\n\t// -------------------- 获取 & 校验 \n\n\t/**\n\t * 获取当前 Same-Token, 如果不存在，则立即创建并返回 \n\t * @return / \n\t */\n\tpublic static String getToken() {\n\t\treturn SaManager.getSaSameTemplate().getToken();\n\t}\n\n\t/**\n\t * 判断一个 Same-Token 是否有效 \n\t * @param token / \n\t * @return /\n\t */\n\tpublic static boolean isValid(String token) {\n\t\treturn SaManager.getSaSameTemplate().isValid(token);\n\t}\n\n\t/**\n\t * 校验一个 Same-Token 是否有效 (如果无效则抛出异常) \n\t * @param token / \n\t */\n\tpublic static void checkToken(String token) {\n\t\tSaManager.getSaSameTemplate().checkToken(token);\n\t}\n\n\t/**\n\t * 校验当前 Request 上下文提供的 Same-Token 是否有效 (如果无效则抛出异常) \n\t */\n\tpublic static void checkCurrentRequestToken() {\n\t\tSaManager.getSaSameTemplate().checkCurrentRequestToken();\n\t}\n\n\t/**\n\t * 刷新一次 Same-Token (注意集群环境中不要多个服务重复调用) \n\t * @return 刷新后产生的新 Same-Token \n\t */\n\tpublic static String refreshToken() {\n\t\treturn SaManager.getSaSameTemplate().refreshToken();\n\t}\n\n\t\n\t// -------------------- 获取Token \n\n\t/**\n\t * 获取 Same-Token，不做任何处理 \n\t * @return / \n\t */\n\tpublic static String getTokenNh() {\n\t\treturn SaManager.getSaSameTemplate().getTokenNh();\n\t}\n\n\t/**\n\t * 获取 Past-Same-Token，不做任何处理 \n\t * @return / \n\t */\n\tpublic static String getPastTokenNh() {\n\t\treturn SaManager.getSaSameTemplate().getPastTokenNh();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/secure/BCrypt.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.secure;\n\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.SecureRandom;\n\n/**\n * BCrypt加密算法实现。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符，并将在内部被转化为448位的密钥。\n * <p>\n * 此类来自于https://github.com/jeremyh/jBCrypt/\n * <p>\n * 使用方法如下：\n * <p>\n * {@code\n * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());\n * }\n * <p>\n * 使用checkpw方法检查被加密的字符串是否与原始字符串匹配：\n * <p>\n * {@code\n * BCrypt.checkpw(candidate_password, stored_hash);\n * }\n * <p>\n * gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少，也决定了加密的复杂度:\n * <p>\n * {@code\n * String strong_salt = BCrypt.gensalt(10);\n * String stronger_salt = BCrypt.gensalt(12);\n * }\n *\n * @author Damien Miller\n * @since 1.29.0\n */\n@Deprecated\n@SuppressWarnings(\"all\")\npublic class BCrypt {\n    // BCrypt parameters\n    private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10;\n    private static final int BCRYPT_SALT_LEN = 16;\n\n    // Blowfish parameters\n    private static final int BLOWFISH_NUM_ROUNDS = 16;\n\n    // Initial contents of key schedule\n    private static final int[] P_orig = {0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7,\n            0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b};\n    private static final int[] S_orig = {0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8,\n            0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,\n            0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34,\n            0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af,\n            0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482,\n            0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,\n            0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72,\n            0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4,\n            0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857,\n            0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,\n            0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d,\n            0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01,\n            0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0,\n            0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,\n            0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb,\n            0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a,\n            0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d,\n            0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,\n            0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6,\n            0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d,\n            0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146,\n            0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,\n            0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366,\n            0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b,\n            0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6,\n            0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,\n            0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775,\n            0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca,\n            0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829,\n            0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,\n            0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a,\n            0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea,\n            0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa,\n            0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,\n            0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7,\n            0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941,\n            0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c,\n            0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,\n            0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c,\n            0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1,\n            0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e,\n            0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,\n            0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b,\n            0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7,\n            0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057,\n            0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,\n            0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591,\n            0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df,\n            0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28,\n            0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,\n            0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce,\n            0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315,\n            0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb,\n            0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,\n            0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9,\n            0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc,\n            0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b,\n            0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,\n            0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf,\n            0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e,\n            0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82,\n            0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,\n            0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71,\n            0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df,\n            0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166,\n            0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,\n            0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60,\n            0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0,\n            0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6};\n\n    // bcrypt IV: \"OrpheanBeholderScryDoubt\". The C implementation calls\n    // this \"ciphertext\", but it is really plaintext or an IV. We keep\n    // the name to make code comparison easier.\n    static private final int[] bf_crypt_ciphertext = {0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274};\n\n    // Table for Base64 encoding\n    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',\n            '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'};\n\n    // Table for Base64 decoding\n    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,\n            -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,\n            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};\n\n    // Expanded Blowfish key\n    private int[] P;\n    private int[] S;\n\n    /**\n     * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note that this is *not* compatible with the standard MIME-base64 encoding.\n     *\n     * @param d   the byte array to encode\n     * @param len the number of bytes to encode\n     * @return base64-encoded string\n     * @throws IllegalArgumentException if the length is invalid\n     */\n    private static String encode_base64(byte[] d, int len) throws IllegalArgumentException {\n        int off = 0;\n        StringBuilder rs = new StringBuilder();\n        int c1, c2;\n\n        if (len <= 0 || len > d.length)\n            throw new IllegalArgumentException(\"Invalid len\");\n\n        while (off < len) {\n            c1 = d[off++] & 0xff;\n            rs.append(base64_code[(c1 >> 2) & 0x3f]);\n            c1 = (c1 & 0x03) << 4;\n            if (off >= len) {\n                rs.append(base64_code[c1 & 0x3f]);\n                break;\n            }\n            c2 = d[off++] & 0xff;\n            c1 |= (c2 >> 4) & 0x0f;\n            rs.append(base64_code[c1 & 0x3f]);\n            c1 = (c2 & 0x0f) << 2;\n            if (off >= len) {\n                rs.append(base64_code[c1 & 0x3f]);\n                break;\n            }\n            c2 = d[off++] & 0xff;\n            c1 |= (c2 >> 6) & 0x03;\n            rs.append(base64_code[c1 & 0x3f]);\n            rs.append(base64_code[c2 & 0x3f]);\n        }\n        return rs.toString();\n    }\n\n    /**\n     * Look up the 3 bits base64-encoded by the specified character, range-checking againt conversion table\n     *\n     * @param x the base64-encoded value\n     * @return the decoded value of x\n     */\n    private static byte char64(char x) {\n        if ((int) x > index_64.length)\n            return -1;\n        return index_64[x];\n    }\n\n    /**\n     * Decode a string encoded using bcrypt's base64 scheme to a byte array.<br>\n     * Note that this is *not* compatible with the standard MIME-base64 encoding.\n     *\n     * @param s       the string to decode\n     * @param maxolen the maximum number of bytes to decode\n     * @return an array containing the decoded bytes\n     * @throws IllegalArgumentException if maxolen is invalid\n     */\n    private static byte[] decodeBase64(String s, int maxolen) throws IllegalArgumentException {\n        final StringBuilder rs = new StringBuilder();\n        int off = 0, slen = s.length(), olen = 0;\n        byte[] ret;\n        byte c1, c2, c3, c4, o;\n\n        if (maxolen <= 0)\n            throw new IllegalArgumentException(\"Invalid maxolen\");\n\n        while (off < slen - 1 && olen < maxolen) {\n            c1 = char64(s.charAt(off++));\n            c2 = char64(s.charAt(off++));\n            if (c1 == -1 || c2 == -1)\n                break;\n            o = (byte) (c1 << 2);\n            o |= (c2 & 0x30) >> 4;\n            rs.append((char) o);\n            if (++olen >= maxolen || off >= slen)\n                break;\n            c3 = char64(s.charAt(off++));\n            if (c3 == -1)\n                break;\n            o = (byte) ((c2 & 0x0f) << 4);\n            o |= (c3 & 0x3c) >> 2;\n            rs.append((char) o);\n            if (++olen >= maxolen || off >= slen)\n                break;\n            c4 = char64(s.charAt(off++));\n            o = (byte) ((c3 & 0x03) << 6);\n            o |= c4;\n            rs.append((char) o);\n            ++olen;\n        }\n\n        ret = new byte[olen];\n        for (off = 0; off < olen; off++)\n            ret[off] = (byte) rs.charAt(off);\n        return ret;\n    }\n\n    /**\n     * Blowfish encipher a single 64-bit block encoded as two 32-bit halves\n     *\n     * @param lr  an array containing the two 32-bit half blocks\n     * @param off the position in the array of the blocks\n     */\n    private void encipher(int[] lr, int off) {\n        int i, n, l = lr[off], r = lr[off + 1];\n\n        l ^= P[0];\n        for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2; ) {\n            // Feistel substitution on left word\n            n = S[(l >> 24) & 0xff];\n            n += S[0x100 | ((l >> 16) & 0xff)];\n            n ^= S[0x200 | ((l >> 8) & 0xff)];\n            n += S[0x300 | (l & 0xff)];\n            r ^= n ^ P[++i];\n\n            // Feistel substitution on right word\n            n = S[(r >> 24) & 0xff];\n            n += S[0x100 | ((r >> 16) & 0xff)];\n            n ^= S[0x200 | ((r >> 8) & 0xff)];\n            n += S[0x300 | (r & 0xff)];\n            l ^= n ^ P[++i];\n        }\n        lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1];\n        lr[off + 1] = l;\n    }\n\n    /**\n     * Cycically extract a word of key material\n     *\n     * @param data the string to extract the data from\n     * @param offp a \"pointer\" (as a one-entry array) to the current offset into data\n     * @return the next word of material from data\n     */\n    private static int streamToWord(byte[] data, int[] offp) {\n        int i;\n        int word = 0;\n        int off = offp[0];\n\n        for (i = 0; i < 4; i++) {\n            word = (word << 8) | (data[off] & 0xff);\n            off = (off + 1) % data.length;\n        }\n\n        offp[0] = off;\n        return word;\n    }\n\n    /**\n     * Initialise the Blowfish key schedule\n     */\n    private void init_key() {\n        P = P_orig.clone();\n        S = S_orig.clone();\n    }\n\n    /**\n     * Key the Blowfish cipher\n     *\n     * @param key an array containing the key\n     */\n    private void key(byte[] key) {\n        int i;\n        int[] koffp = {0};\n        int[] lr = {0, 0};\n        int plen = P.length, slen = S.length;\n\n        for (i = 0; i < plen; i++)\n            P[i] = P[i] ^ streamToWord(key, koffp);\n\n        for (i = 0; i < plen; i += 2) {\n            encipher(lr, 0);\n            P[i] = lr[0];\n            P[i + 1] = lr[1];\n        }\n\n        for (i = 0; i < slen; i += 2) {\n            encipher(lr, 0);\n            S[i] = lr[0];\n            S[i + 1] = lr[1];\n        }\n    }\n\n    /**\n     * 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\n     *\n     * @param data salt information\n     * @param key  password information\n     */\n    private void ekskey(byte[] data, byte[] key) {\n        int i;\n        int[] koffp = {0};\n        int[] doffp = {0};\n        int[] lr = {0, 0};\n        int plen = P.length, slen = S.length;\n\n        for (i = 0; i < plen; i++)\n            P[i] = P[i] ^ streamToWord(key, koffp);\n\n        for (i = 0; i < plen; i += 2) {\n            lr[0] ^= streamToWord(data, doffp);\n            lr[1] ^= streamToWord(data, doffp);\n            encipher(lr, 0);\n            P[i] = lr[0];\n            P[i + 1] = lr[1];\n        }\n\n        for (i = 0; i < slen; i += 2) {\n            lr[0] ^= streamToWord(data, doffp);\n            lr[1] ^= streamToWord(data, doffp);\n            encipher(lr, 0);\n            S[i] = lr[0];\n            S[i + 1] = lr[1];\n        }\n    }\n\n    /**\n     * 加密密文\n     *\n     * @param password   明文密码\n     * @param salt       加盐\n     * @param log_rounds hash中叠加的对数\n     * @param cdata      加密数据\n     * @return 加密后的密文\n     */\n    public byte[] crypt(byte[] password, byte[] salt, int log_rounds, int[] cdata) {\n        int rounds, i, j;\n        int clen = cdata.length;\n        byte[] ret;\n\n        if (log_rounds < 4 || log_rounds > 30)\n            throw new IllegalArgumentException(\"Bad number of rounds\");\n        rounds = 1 << log_rounds;\n        if (salt.length != BCRYPT_SALT_LEN)\n            throw new IllegalArgumentException(\"Bad salt length\");\n\n        init_key();\n        ekskey(salt, password);\n        for (i = 0; i != rounds; i++) {\n            key(password);\n            key(salt);\n        }\n\n        for (i = 0; i < 64; i++) {\n            for (j = 0; j < (clen >> 1); j++)\n                encipher(cdata, j << 1);\n        }\n\n        ret = new byte[clen * 4];\n        for (i = 0, j = 0; i < clen; i++) {\n            ret[j++] = (byte) ((cdata[i] >> 24) & 0xff);\n            ret[j++] = (byte) ((cdata[i] >> 16) & 0xff);\n            ret[j++] = (byte) ((cdata[i] >> 8) & 0xff);\n            ret[j++] = (byte) (cdata[i] & 0xff);\n        }\n        return ret;\n    }\n\n    /**\n     * 生成密文，使用长度为10的加盐方式\n     *\n     * @param password 需要加密的明文\n     * @return 密文\n     */\n    public static String hashpw(String password) {\n        return hashpw(password, gensalt());\n    }\n\n    /**\n     * 生成密文\n     *\n     * @param password 需要加密的明文\n     * @param salt     盐，使用{@link #gensalt()} 生成\n     * @return 密文\n     */\n    public static String hashpw(String password, String salt) {\n        BCrypt bcrypt;\n        String real_salt;\n        byte[] saltb;\n        byte[] hashed;\n        char minor = (char) 0;\n        int rounds, off;\n        StringBuilder rs = new StringBuilder();\n\n        if (salt.charAt(0) != '$' || salt.charAt(1) != '2')\n            throw new IllegalArgumentException(\"Invalid salt version\");\n        if (salt.charAt(2) == '$')\n            off = 3;\n        else {\n            minor = salt.charAt(2);\n            // pr#1560@Github\n            // 修正一个在Blowfish实现上的安全风险\n            if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$')\n                throw new IllegalArgumentException(\"Invalid salt revision\");\n            off = 4;\n        }\n\n        // Extract number of rounds\n        if (salt.charAt(off + 2) > '$')\n            throw new IllegalArgumentException(\"Missing salt rounds\");\n        rounds = Integer.parseInt(salt.substring(off, off + 2));\n\n        real_salt = salt.substring(off + 3, off + 25);\n        byte[] passwordb = (password + (minor >= 'a' ? \"\\000\" : \"\")).getBytes(StandardCharsets.UTF_8);\n        saltb = decodeBase64(real_salt, BCRYPT_SALT_LEN);\n\n        bcrypt = new BCrypt();\n        hashed = bcrypt.crypt(passwordb, saltb, rounds, bf_crypt_ciphertext.clone());\n\n        rs.append(\"$2\");\n        if (minor >= 'a')\n            rs.append(minor);\n        rs.append(\"$\");\n        if (rounds < 10)\n            rs.append(\"0\");\n        if (rounds > 30) {\n            throw new IllegalArgumentException(\"rounds exceeds maximum (30)\");\n        }\n        rs.append(rounds);\n        rs.append(\"$\");\n        rs.append(encode_base64(saltb, saltb.length));\n        rs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1));\n        return rs.toString();\n    }\n\n    /**\n     * 生成盐\n     *\n     * @param log_rounds hash中叠加的2的对数 - the work factor therefore increases as 2**log_rounds.\n     * @param random     {@link SecureRandom}\n     * @return an encoded salt value\n     */\n    public static String gensalt(int log_rounds, SecureRandom random) {\n        final StringBuilder rs = new StringBuilder();\n        byte[] rnd = new byte[BCRYPT_SALT_LEN];\n\n        random.nextBytes(rnd);\n\n        rs.append(\"$2a$\");\n        if (log_rounds < 10)\n            rs.append(\"0\");\n        if (log_rounds > 30) {\n            throw new IllegalArgumentException(\"log_rounds exceeds maximum (30)\");\n        }\n        rs.append(log_rounds);\n        rs.append(\"$\");\n        rs.append(encode_base64(rnd, rnd.length));\n        return rs.toString();\n    }\n\n    /**\n     * 生成盐\n     *\n     * @param log_rounds the log2 of the number of rounds of hashing to apply - the work factor therefore increases as 2**log_rounds.\n     * @return 盐\n     */\n    public static String gensalt(int log_rounds) {\n        return gensalt(log_rounds, new SecureRandom());\n    }\n\n    /**\n     * 生成盐\n     *\n     * @return 盐\n     */\n    public static String gensalt() {\n        return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);\n    }\n\n    /**\n     * 检查明文密码文本是否匹配加密后的文本\n     *\n     * @param plaintext 需要验证的明文密码\n     * @param hashed    密文\n     * @return 是否匹配\n     */\n    public static boolean checkpw(String plaintext, String hashed) {\n        byte[] hashed_bytes;\n        byte[] try_bytes;\n\n        String try_pw;\n        try {\n            try_pw = hashpw(plaintext, hashed);\n        } catch (Exception ignore) {\n            // 生成密文时错误直接返回false issue#1377@Github\n            return false;\n        }\n        hashed_bytes = hashed.getBytes(StandardCharsets.UTF_8);\n        try_bytes = try_pw.getBytes(StandardCharsets.UTF_8);\n        if (hashed_bytes.length != try_bytes.length) {\n            return false;\n        }\n        byte ret = 0;\n        for (int i = 0; i < try_bytes.length; i++)\n            ret |= hashed_bytes[i] ^ try_bytes[i];\n        return ret == 0;\n    }\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase32Util.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.secure;\n\nimport java.nio.charset.StandardCharsets;\n\n/**\n * Sa-Token Base32 工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaBase32Util {\n\n    private static final String BASE32_CHARS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\n    private static final int[] BASE32_LOOKUP = new int[256];\n\n    static {\n        // 初始化解码查找表\n        for (int i = 0; i < BASE32_CHARS.length(); i++) {\n            char c = BASE32_CHARS.charAt(i);\n            BASE32_LOOKUP[c] = i;\n            // 支持小写字母解码\n            if (c >= 'A' && c <= 'Z') {\n                BASE32_LOOKUP[Character.toLowerCase(c)] = i;\n            }\n        }\n    }\n\n    /**\n     * Base32 编码（byte[] 转 String）\n     */\n    public static String encodeBytesToString(byte[] bytes) {\n        if (bytes == null) return null;\n\n        StringBuilder result = new StringBuilder();\n        int buffer = 0;\n        int bufferSize = 0;\n\n        for (byte b : bytes) {\n            buffer = (buffer << 8) | (b & 0xFF);\n            bufferSize += 8;\n\n            while (bufferSize >= 5) {\n                bufferSize -= 5;\n                int index = (buffer >> bufferSize) & 0x1F;\n                result.append(BASE32_CHARS.charAt(index));\n            }\n        }\n\n        // 处理剩余位\n        if (bufferSize > 0) {\n            int index = (buffer << (5 - bufferSize)) & 0x1F;\n            result.append(BASE32_CHARS.charAt(index));\n        }\n\n        return result.toString();\n    }\n\n    /**\n     * Base32 解码（String 转 byte[]）\n     */\n    public static byte[] decodeStringToBytes(String text) {\n        if (text == null) return null;\n\n        text = text.replaceAll(\"=\", \"\").trim();\n        if (text.isEmpty()) return new byte[0];\n\n        int buffer = 0;\n        int bufferSize = 0;\n        int byteCount = (text.length() * 5 + 7) / 8;\n        byte[] bytes = new byte[byteCount];\n        int byteIndex = 0;\n\n        for (char c : text.toCharArray()) {\n            int value = BASE32_LOOKUP[c];\n            if (value == 0 && c != 'A') continue; // 跳过非法字符\n\n            buffer = (buffer << 5) | value;\n            bufferSize += 5;\n\n            if (bufferSize >= 8) {\n                bufferSize -= 8;\n                bytes[byteIndex++] = (byte) ((buffer >> bufferSize) & 0xFF);\n            }\n        }\n\n        // 处理最后一个字节\n        if (bufferSize > 0) {\n            bytes[byteIndex] = (byte) ((buffer << (8 - bufferSize)) & 0xFF);\n        }\n\n        return bytes;\n    }\n\n    /**\n     * Base32 编码（String 转 String）\n     */\n    public static String encode(String text) {\n        if (text == null) return null;\n        return encodeBytesToString(text.getBytes(StandardCharsets.UTF_8));\n    }\n\n    /**\n     * Base32 解码（String 转 String）\n     */\n    public static String decode(String base32Text) {\n        if (base32Text == null) return null;\n        byte[] bytes = decodeStringToBytes(base32Text);\n        return new String(bytes, StandardCharsets.UTF_8);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/secure/SaBase64Util.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.secure;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Base64;\n\n/**\n * Sa-Token Base64 工具类\n *\n * @author click33\n * @since 1.14.0\n */\npublic class SaBase64Util {\n\n\tprivate static final Base64.Encoder encoder = Base64.getEncoder();\n\tprivate static final Base64.Decoder decoder = Base64.getDecoder();\n\t\n\t/**\n\t * Base64编码，byte[] 转 String\n\t * @param bytes byte[]\n\t * @return 字符串\n\t */\n\tpublic static String encodeBytesToString(byte[] bytes){\n\t\treturn encoder.encodeToString(bytes);\n\t}\n\n\t/**\n\t * Base64解码，String 转 byte[]\n\t * @param text 字符串\n\t * @return byte[]\n\t */\n\tpublic static byte[] decodeStringToBytes(String text){\n\t\treturn decoder.decode(text);\n\t}\n\t\n\t/**\n\t * Base64编码，String 转 String\n\t * @param text 字符串\n\t * @return Base64格式字符串\n\t */\n\tpublic static String encode(String text){\n\t\treturn encoder.encodeToString(text.getBytes(StandardCharsets.UTF_8));\n\t}\n\n\t/**\n\t * Base64解码，String 转 String\n\t * @param base64Text Base64格式字符串\n\t * @return 字符串\n\t */\n\tpublic static String decode(String base64Text){\n\t\treturn new String(decoder.decode(base64Text), StandardCharsets.UTF_8);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/secure/SaSecureUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.secure;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\n\nimport javax.crypto.Cipher;\nimport javax.crypto.KeyGenerator;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\nimport java.security.*;\nimport java.security.interfaces.RSAPrivateKey;\nimport java.security.interfaces.RSAPublicKey;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.Base64;\nimport java.util.HashMap;\nimport java.util.UUID;\n\n/**\n * Sa-Token 常见加密算法工具类\n *\n * @author click33\n * @since 1.14.0\n */\npublic class SaSecureUtil {\n\n\tprivate SaSecureUtil() {\n\t}\n\n\t/**\n\t * Base64编码\n\t */\n\tprivate static final Base64.Encoder encoder = Base64.getEncoder();\n\n\t/**\n\t * Base64解码\n\t */\n\tprivate static final Base64.Decoder decoder = Base64.getDecoder();\n\n\t// ----------------------- 摘要加密 -----------------------\n\n\t/**\n\t * md5加密\n\t * @param str 指定字符串\n\t * @return 加密后的字符串\n\t */\n\tpublic static String md5(String str) {\n\t\tstr = (str == null ? \"\" : str);\n\t\tchar[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };\n\t\ttry {\n\t\t\tbyte[] btInput = str.getBytes();\n\t\t\tMessageDigest mdInst = MessageDigest.getInstance(\"MD5\");\n\t\t\tmdInst.update(btInput);\n\t\t\tbyte[] md = mdInst.digest();\n\t\t\tint j = md.length;\n\t\t\tchar[] strA = new char[j * 2];\n\t\t\tint k = 0;\n\t\t\tfor (byte byte0 : md) {\n\t\t\t\tstrA[k++] = hexDigits[byte0 >>> 4 & 0xf];\n\t\t\t\tstrA[k++] = hexDigits[byte0 & 0xf];\n\t\t\t}\n\t\t\treturn new String(strA);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12111);\n\t\t}\n\t}\n\n\t/**\n\t * sha1加密\n\t *\n\t * @param str 指定字符串\n\t * @return 加密后的字符串\n\t */\n\tpublic static String sha1(String str) {\n\t\ttry {\n\t\t\tstr = (str == null ? \"\" : str);\n\t\t\tMessageDigest messageDigest = MessageDigest.getInstance(\"SHA1\");\n\t\t\treturn getShaHexString(str, messageDigest);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12112);\n\t\t}\n\t}\n\n\t/**\n\t * sha256加密\n\t *\n\t * @param str 指定字符串\n\t * @return 加密后的字符串\n\t */\n\tpublic static String sha256(String str) {\n\t\ttry {\n\t\t\tstr = (str == null ? \"\" : str);\n\t\t\tMessageDigest messageDigest = MessageDigest.getInstance(\"SHA-256\");\n\t\t\treturn getShaHexString(str, messageDigest);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12113);\n\t\t}\n\t}\n\n\t/**\n\t * sha384加密\n\t *\n\t * @param str 指定字符串\n\t * @return 加密后的字符串\n\t */\n\tpublic static String sha384(String str) {\n\t\ttry {\n\t\t\tstr = (str == null ? \"\" : str);\n\t\t\tMessageDigest messageDigest = MessageDigest.getInstance(\"SHA-384\");\n\t\t\treturn getShaHexString(str, messageDigest);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_121131);\n\t\t}\n\t}\n\n\t/**\n\t * sha512加密\n\t *\n\t * @param str 指定字符串\n\t * @return 加密后的字符串\n\t */\n\tpublic static String sha512(String str) {\n\t\ttry {\n\t\t\tstr = (str == null ? \"\" : str);\n\t\t\tMessageDigest messageDigest = MessageDigest.getInstance(\"SHA-512\");\n\t\t\treturn getShaHexString(str, messageDigest);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_121132);\n\t\t}\n\t}\n\n\t/**\n\t * sha (Secure Hash Algorithm)加密 公共方法\n\t *\n\t * @param str 指定字符串\n\t * @param messageDigest 消息摘要\n\t * @return 加密后的字符串\n\t */\n\tprivate static String getShaHexString(String str, MessageDigest messageDigest) {\n\t\tmessageDigest.update(str.getBytes(StandardCharsets.UTF_8));\n\t\tbyte[] bytes = messageDigest.digest();\n\t\tStringBuilder builder = new StringBuilder();\n\t\tString temp;\n\t\tfor (byte aByte : bytes) {\n\t\t\ttemp = Integer.toHexString(aByte & 0xFF); // 获取无符号整数十六进制字符串\n\t\t\tif (temp.length() == 1) {\n\t\t\t\tbuilder.append(\"0\"); // 确保每个字节都用两个字符表示\n\t\t\t}\n\t\t\tbuilder.append(temp);\n\t\t}\n\n\t\treturn builder.toString();\n\t}\n\n\t/**\n\t * md5加盐加密: md5(md5(str) + md5(salt))\n\t * @param str 字符串\n\t * @param salt 盐\n\t * @return 加密后的字符串\n\t */\n\t@Deprecated\n\tpublic static String md5BySalt(String str, String salt) {\n\t\treturn md5(md5(str) + md5(salt));\n\t}\n\n\t/**\n\t * sha256加盐加密: sha256(sha256(str) + sha256(salt))\n\t * @param str 字符串\n\t * @param salt 盐\n\t * @return 加密后的字符串\n\t */\n\t@Deprecated\n\tpublic static String sha256BySalt(String str, String salt) {\n\t\treturn sha256(sha256(str) + sha256(salt));\n\t}\n\n\t// ----------------------- 对称加密 AES -----------------------\n\n    /**\n     * 默认密码算法\n     */\n    private static final String DEFAULT_CIPHER_ALGORITHM = \"AES/ECB/PKCS5Padding\";\n\n    /**\n     * AES加密\n     *\n     * @param key 加密的密钥\n     * @param text 需要加密的字符串\n     * @return 返回Base64转码后的加密数据\n     */\n    public static String aesEncrypt(String key, String text) {\n        try {\n            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);\n            byte[] byteContent = text.getBytes(StandardCharsets.UTF_8);\n            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));\n            byte[] result = cipher.doFinal(byteContent);\n            return encoder.encodeToString(result);\n \t\t} catch (Exception e) {\n \t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12114);\n \t\t}\n    }\n\n    /**\n     * AES解密\n     * @param key 加密的密钥\n     * @param text 已加密的密文\n     * @return 返回解密后的数据\n     */\n    public static String aesDecrypt(String key, String text) {\n       try {\n    \t   Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);\n           cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));\n           byte[] result = cipher.doFinal(decoder.decode(text));\n           return new String(result, StandardCharsets.UTF_8);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12115);\n\t\t}\n    }\n\n    /**\n     * 生成加密秘钥\n     * @param password 秘钥\n     * @return SecretKeySpec\n\t */\n    private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException {\n        KeyGenerator kg = KeyGenerator.getInstance(\"AES\");\n        SecureRandom random = SecureRandom.getInstance(\"SHA1PRNG\");\n        random.setSeed(password.getBytes());\n        kg.init(128, random);\n        SecretKey secretKey = kg.generateKey();\n        return new SecretKeySpec(secretKey.getEncoded(), \"AES\");\n    }\n\n\n\t// ----------------------- 非对称加密 RSA -----------------------\n\n\tprivate static final String ALGORITHM = \"RSA\";\n\n\tprivate static final int KEY_SIZE = 1024;\n\n\n\t// ---------- 5个常用方法\n\n\t/**\n\t * 生成密钥对\n\t * @return Map对象 (private=私钥, public=公钥)\n\t * @throws Exception 异常\n\t */\n\t@Deprecated\n\tpublic static HashMap<String, String> rsaGenerateKeyPair() throws Exception {\n\n\t\tKeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);\n\t\tKeyPair keyPair;\n\n\t\tkeyPairGenerator.initialize(KEY_SIZE,\n\t\t\t\tnew SecureRandom(UUID.randomUUID().toString().replaceAll(\"-\", \"\").getBytes()));\n\t\tkeyPair = keyPairGenerator.generateKeyPair();\n\n\t\tRSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();\n\t\tRSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();\n\n\t\tHashMap<String, String> map = new HashMap<>(16);\n\t\tmap.put(\"private\", encoder.encodeToString(rsaPrivateKey.getEncoded()));\n\t\tmap.put(\"public\", encoder.encodeToString(rsaPublicKey.getEncoded()));\n\t\treturn map;\n\t}\n\n\t/**\n\t * RSA公钥加密\n\t * @param publicKeyString 公钥\n\t * @param content 内容\n\t * @return 加密后内容\n\t */\n\t@Deprecated\n\tpublic static String rsaEncryptByPublic(String publicKeyString, String content) {\n\t\ttry {\n\t\t\t// 获得公钥对象\n\t\t\tPublicKey publicKey = getPublicKeyFromString(publicKeyString);\n\n\t\t\tCipher cipher = Cipher.getInstance(\"RSA\");\n\t\t\tcipher.init(Cipher.ENCRYPT_MODE, publicKey);\n\t\t\t// 该密钥能够加密的最大字节长度\n\t\t\tint splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8 - 11;\n\t\t\tbyte[][] arrays = splitBytes(content.getBytes(), splitLength);\n\t\t\tStringBuilder stringBuilder = new StringBuilder();\n\t\t\tfor (byte[] array : arrays) {\n\t\t\t\tstringBuilder.append(bytesToHexString(cipher.doFinal(array)));\n\t\t\t}\n\t\t\treturn stringBuilder.toString();\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12116);\n\t\t}\n\t}\n\n\t/**\n\t * RSA私钥加密\n\t * @param privateKeyString 私钥\n\t * @param content 内容\n\t * @return 加密后内容\n\t */\n\t@Deprecated\n\tpublic static String rsaEncryptByPrivate(String privateKeyString, String content) {\n\t\ttry {\n\t\t\tPrivateKey privateKey = getPrivateKeyFromString(privateKeyString);\n\n\t\t\tCipher cipher = Cipher.getInstance(\"RSA\");\n\t\t\tcipher.init(Cipher.ENCRYPT_MODE, privateKey);\n\t\t\t// 该密钥能够加密的最大字节长度\n\t\t\tint splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8 - 11;\n\t\t\tbyte[][] arrays = splitBytes(content.getBytes(), splitLength);\n\t\t\tStringBuilder stringBuilder = new StringBuilder();\n\t\t\tfor (byte[] array : arrays) {\n\t\t\t\tstringBuilder.append(bytesToHexString(cipher.doFinal(array)));\n\t\t\t}\n\t\t\treturn stringBuilder.toString();\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12117);\n\t\t}\n\t}\n\n\t/**\n\t * RSA公钥解密\n\t * @param publicKeyString 公钥\n\t * @param content 已加密内容\n\t * @return 解密后内容\n\t */\n\t@Deprecated\n\tpublic static String rsaDecryptByPublic(String publicKeyString, String content) {\n\n\t\ttry {\n\t\t\tPublicKey publicKey = getPublicKeyFromString(publicKeyString);\n\n\t\t\tCipher cipher = Cipher.getInstance(\"RSA\");\n\t\t\tcipher.init(Cipher.DECRYPT_MODE, publicKey);\n\t\t\t// 该密钥能够加密的最大字节长度\n\t\t\tint splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8;\n\t\t\tbyte[] contentBytes = hexStringToBytes(content);\n\t\t\tbyte[][] arrays = splitBytes(contentBytes, splitLength);\n\t\t\tStringBuilder stringBuilder = new StringBuilder();\n\t\t\tfor (byte[] array : arrays) {\n\t\t\t\tstringBuilder.append(new String(cipher.doFinal(array)));\n\t\t\t}\n\t\t\treturn stringBuilder.toString();\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12118);\n\t\t}\n\t}\n\n\t/**\n\t * RSA私钥解密\n\t * @param privateKeyString 公钥\n\t * @param content 已加密内容\n\t * @return 解密后内容\n\t */\n\t@Deprecated\n\tpublic static String rsaDecryptByPrivate(String privateKeyString, String content) {\n\t\ttry {\n\t\t\tPrivateKey privateKey = getPrivateKeyFromString(privateKeyString);\n\n\t\t\tCipher cipher = Cipher.getInstance(\"RSA\");\n\t\t\tcipher.init(Cipher.DECRYPT_MODE, privateKey);\n\t\t\t// 该密钥能够加密的最大字节长度\n\t\t\tint splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8;\n\t\t\tbyte[] contentBytes = hexStringToBytes(content);\n\t\t\tbyte[][] arrays = splitBytes(contentBytes, splitLength);\n\t\t\tStringBuilder stringBuilder = new StringBuilder();\n\t\t\tfor (byte[] array : arrays) {\n\t\t\t\tstringBuilder.append(new String(cipher.doFinal(array)));\n\t\t\t}\n\t\t\treturn stringBuilder.toString();\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12119);\n\t\t}\n\t}\n\n\n\t// ---------- 获取*钥\n\n\t/** 根据公钥字符串获取 公钥对象 */\n\tprivate static PublicKey getPublicKeyFromString(String key)\n\t\t\tthrows NoSuchAlgorithmException, InvalidKeySpecException {\n\n\t\t// 过滤掉\\r\\n\n\t\tkey = key.replace(\"\\r\\n\", \"\");\n\n\t\t// 取得公钥\n\t\tX509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(decoder.decode(key));\n\n\t\tKeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);\n\n\t\treturn keyFactory.generatePublic(x509KeySpec);\n\t}\n\n\t/** 根据私钥字符串获取 私钥对象 */\n\tprivate static PrivateKey getPrivateKeyFromString(String key)\n\t\t\tthrows NoSuchAlgorithmException, InvalidKeySpecException {\n\n\t\t// 过滤掉\\r\\n\n\t\tkey = key.replace(\"\\r\\n\", \"\");\n\n\t\t// 取得私钥\n\t\tPKCS8EncodedKeySpec x509KeySpec = new PKCS8EncodedKeySpec(decoder.decode(key));\n\n\t\tKeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);\n\n\t\treturn keyFactory.generatePrivate(x509KeySpec);\n\t}\n\n\n\t// ---------- 一些辅助方法\n\n\t/** 根据限定的每组字节长度，将字节数组分组 */\n\tprivate static byte[][] splitBytes(byte[] bytes, int splitLength) {\n\n\t\t// bytes与splitLength的余数\n\t\tint remainder = bytes.length % splitLength;\n\t\t// 数据拆分后的组数，余数不为0时加1\n\t\tint quotient = remainder != 0 ? bytes.length / splitLength + 1 : bytes.length / splitLength;\n\t\tbyte[][] arrays = new byte[quotient][];\n\t\tbyte[] array;\n\t\tfor (int i = 0; i < quotient; i++) {\n\t\t\t// 如果是最后一组（quotient-1）,同时余数不等于0，就将最后一组设置为remainder的长度\n\t\t\tif (i == quotient - 1 && remainder != 0) {\n\t\t\t\tarray = new byte[remainder];\n\t\t\t\tSystem.arraycopy(bytes, i * splitLength, array, 0, remainder);\n\t\t\t} else {\n\t\t\t\tarray = new byte[splitLength];\n\t\t\t\tSystem.arraycopy(bytes, i * splitLength, array, 0, splitLength);\n\t\t\t}\n\t\t\tarrays[i] = array;\n\t\t}\n\t\treturn arrays;\n\t}\n\n\t/** 将字节数组转换成16进制字符串 */\n\tprivate static String bytesToHexString(byte[] bytes) {\n\n\t\tStringBuilder sb = new StringBuilder(bytes.length);\n\t\tString temp;\n\t\tfor (byte aByte : bytes) {\n\t\t\ttemp = Integer.toHexString(0xFF & aByte);\n\t\t\tif (temp.length() < 2) {\n\t\t\t\tsb.append(0);\n\t\t\t}\n\t\t\tsb.append(temp);\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/** 将16进制字符串转换成字节数组 */\n\tprivate static byte[] hexStringToBytes(String hex) {\n\n\t\tint len = (hex.length() / 2);\n\t\thex = hex.toUpperCase();\n\t\tbyte[] result = new byte[len];\n\t\tchar[] chars = hex.toCharArray();\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tint pos = i * 2;\n\t\t\tresult[i] = (byte) (toByte(chars[pos]) << 4 | toByte(chars[pos + 1]));\n\t\t}\n\t\treturn result;\n\t}\n\n\t/** 将char转换为byte */\n\tprivate static byte toByte(char c) {\n\n\t\treturn (byte) \"0123456789ABCDEF\".indexOf(c);\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.secure.totp;\n\nimport cn.dev33.satoken.exception.TotpAuthException;\nimport cn.dev33.satoken.secure.SaBase32Util;\nimport cn.dev33.satoken.util.StrFormatter;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.ByteBuffer;\nimport java.security.GeneralSecurityException;\nimport java.security.SecureRandom;\nimport java.time.Instant;\n\n/**\n * TOTP 算法类，支持 生成/验证 动态一次性密码\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTotpTemplate {\n\n\t/**\n\t * 时间窗口步长（秒）\n\t */\n\tpublic int timeStep = 30;\n\n\t/**\n\t * 生成的验证码位数\n\t */\n\tpublic int codeDigits = 6;\n\n\t/**\n\t * 哈希算法（HmacSHA1、HmacSHA256等）\n\t */\n\tpublic String hmacAlgorithm = \"HmacSHA1\";\n\n\t/**\n\t * 密钥长度（字节，推荐16或32）\n\t */\n\tpublic int secretKeyLength = 16;\n\n\t/**\n\t * 构造函数 (使用默认参数)\n\t */\n\tpublic SaTotpTemplate() {\n\t}\n\n\t/**\n\t * 构造函数 (使用自定义参数)\n\t *\n\t * @param timeStep 时间窗口步长（秒）\n\t * @param codeDigits 生成的验证码位数\n\t * @param hmacAlgorithm 哈希算法（HmacSHA1、HmacSHA256等）\n\t * @param secretKeyLength 密钥长度（字节，推荐16或32）\n\t */\n\tpublic SaTotpTemplate(int timeStep, int codeDigits, String hmacAlgorithm, int secretKeyLength) {\n\t\tthis.timeStep = timeStep;\n\t\tthis.codeDigits = codeDigits;\n\t\tthis.hmacAlgorithm = hmacAlgorithm;\n\t\tthis.secretKeyLength = secretKeyLength;\n\t}\n\n\n\t/**\n\t * 生成随机密钥（Base32编码）\n\t *\n\t * @return /\n\t */\n\tpublic String generateSecretKey() {\n\t\tSecureRandom random = new SecureRandom();\n\t\tbyte[] bytes = new byte[secretKeyLength];\n\t\trandom.nextBytes(bytes);\n\t\treturn SaBase32Util.encodeBytesToString(bytes).replace(\"=\", \"\");\n\t}\n\n\t/**\n\t * 生成当前时间的 TOTP 验证码\n\t *\n\t * @param secretKey Base32 编码的密钥\n\t * @return /\n\t */\n\tpublic String _generateTOTP(String secretKey) {\n\t\treturn _generateTOTP(secretKey, Instant.now().getEpochSecond());\n\t}\n\n\t/**\n\t * 判断用户输入的 TOTP 是否有效\n\t *\n\t * @param secretKey Base32编码的密钥\n\t * @param code 用户输入的验证码\n\t * @param timeWindowOffset 允许的时间窗口偏移量（如1表示允许前后各1个时间窗口）\n\t * @return /\n\t */\n\tpublic boolean validateTOTP(String secretKey, String code, int timeWindowOffset) {\n\t\tlong currentWindow = Instant.now().getEpochSecond() / timeStep;\n\t\tfor (int i = -timeWindowOffset; i <= timeWindowOffset; i++) {\n\t\t\tString calculatedCode = _generateTOTP(secretKey, (currentWindow + i) * timeStep);\n\t\t\tif (calculatedCode.equals(code)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 校验用户输入的TOTP是否有效，如果无效则抛出异常\n\t *\n\t * @param secretKey Base32编码的密钥\n\t * @param code 用户输入的验证码\n\t * @param timeWindowOffset 允许的时间窗口偏移量（如1表示允许前后各1个时间窗口）\n\t */\n\tpublic void checkTOTP(String secretKey, String code, int timeWindowOffset) {\n\t\tif (!validateTOTP(secretKey, code, timeWindowOffset)) {\n\t\t\tthrow new TotpAuthException();\n\t\t}\n\t}\n\n\t/**\n\t * 生成谷歌认证器的扫码字符串 (形如：otpauth://totp/{account}?secret={secretKey})\n\t *\n\t * @param account  账户名\n\t * @return /\n\t */\n\tpublic String generateGoogleSecretKey(String account) {\n\t\treturn generateGoogleSecretKey(account, generateSecretKey());\n\t}\n\n\t/**\n\t * 生成谷歌认证器的扫码字符串 (形如：otpauth://totp/{account}?secret={secretKey})\n\t *\n\t * @param account  账户名\n\t * @param secretKey  TOTP 秘钥\n\t * @return /\n\t */\n\tpublic String generateGoogleSecretKey(String account, String secretKey) {\n\t\treturn StrFormatter.format(\"otpauth://totp/{}?secret={}\", account, secretKey);\n\t}\n\n\t/**\n\t * 生成谷歌认证器的扫码字符串 (形如：otpauth://totp/{issuer}:{account}?secret={secretKey}&issuer={issuer})\n\t *\n\t * @param account  账户名\n\t * @param issuer  签发者\n\t * @param secretKey  TOTP 秘钥\n\t * @return /\n\t */\n\tpublic String generateGoogleSecretKey(String account, String issuer, String secretKey) {\n\t\treturn StrFormatter.format(\"otpauth://totp/{}:{}?secret={}&issuer={}\", issuer, account, secretKey, issuer);\n\t}\n\n\tprotected String _generateTOTP(String secretKey, long time) {\n\t\t// Base32解码密钥\n\t\tbyte[] keyBytes = SaBase32Util.decodeStringToBytes(secretKey);\n\t\tbyte[] counterBytes = ByteBuffer.allocate(8).putLong(time / timeStep).array();\n\n\t\ttry {\n\t\t\t// 计算HMAC哈希\n\t\t\tMac hmac = Mac.getInstance(hmacAlgorithm);\n\t\t\thmac.init(new SecretKeySpec(keyBytes, hmacAlgorithm));\n\t\t\tbyte[] hash = hmac.doFinal(counterBytes);\n\n\t\t\t// 动态截断（RFC 6238）\n\t\t\tint offset = hash[hash.length - 1] & 0xF;\n\t\t\tint binary = ((hash[offset] & 0x7F) << 24)\n\t\t\t\t\t| ((hash[offset + 1] & 0xFF) << 16)\n\t\t\t\t\t| ((hash[offset + 2] & 0xFF) << 8)\n\t\t\t\t\t| (hash[offset + 3] & 0xFF);\n\n\t\t\t// 生成指定位数的验证码\n\t\t\tint otp = binary % (int) Math.pow(10, codeDigits);\n\t\t\treturn String.format(\"%0\" + codeDigits + \"d\", otp);\n\n\t\t} catch (GeneralSecurityException e) {\n\t\t\tthrow new RuntimeException(\"TOTP生成失败\", e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/secure/totp/SaTotpUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.secure.totp;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * TOTP 工具类，支持 生成/验证 动态一次性密码\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTotpUtil {\n\n\t/**\n\t * 生成随机密钥（Base32编码）\n\t *\n\t * @return /\n\t */\n\tpublic static String generateSecretKey() {\n\t\treturn SaManager.getSaTotpTemplate().generateSecretKey();\n\t}\n\n\t/**\n\t * 生成当前时间的TOTP验证码\n\t *\n\t * @param secretKey Base32编码的密钥\n\t * @return /\n\t */\n\tpublic static String generateTOTP(String secretKey) {\n\t\treturn SaManager.getSaTotpTemplate()._generateTOTP(secretKey);\n\t}\n\n\t/**\n\t * 判断用户输入的TOTP是否有效\n\t *\n\t * @param secretKey Base32编码的密钥\n\t * @param code 用户输入的验证码\n\t * @param timeWindowOffset 允许的时间窗口偏移量（如1表示允许前后各1个时间窗口）\n\t * @return /\n\t */\n\tpublic static boolean validateTOTP(String secretKey, String code, int timeWindowOffset) {\n\t\treturn SaManager.getSaTotpTemplate().validateTOTP(secretKey, code, timeWindowOffset);\n\t}\n\n\t/**\n\t * 校验用户输入的TOTP是否有效，如果无效则抛出异常\n\t *\n\t * @param secretKey Base32编码的密钥\n\t * @param code 用户输入的验证码\n\t * @param timeWindowOffset 允许的时间窗口偏移量（如1表示允许前后各1个时间窗口）\n\t */\n\tpublic static void checkTOTP(String secretKey, String code, int timeWindowOffset) {\n\t\tSaManager.getSaTotpTemplate().checkTOTP(secretKey, code, timeWindowOffset);\n\t}\n\n\t/**\n\t * 生成谷歌认证器的扫码字符串 (形如：otpauth://totp/{account}?secret={secretKey})\n\t *\n\t * @param account  账户名\n\t * @return /\n\t */\n\tpublic static String generateGoogleSecretKey(String account) {\n\t\treturn SaManager.getSaTotpTemplate().generateGoogleSecretKey(account);\n\t}\n\n\t/**\n\t * 生成谷歌认证器的扫码字符串 (形如：otpauth://totp/{account}?secret={secretKey})\n\t *\n\t * @param account  账户名\n\t * @param secretKey  TOTP 秘钥\n\t * @return /\n\t */\n\tpublic static String generateGoogleSecretKey(String account, String secretKey) {\n\t\treturn SaManager.getSaTotpTemplate().generateGoogleSecretKey(account, secretKey);\n\t}\n\n\t/**\n\t * 生成谷歌认证器的扫码字符串 (形如：otpauth://totp/{issuer}:{account}?secret={secretKey}&issuer={issuer})\n\t *\n\t * @param account  账户名\n\t * @param issuer  签发者\n\t * @param secretKey  TOTP 秘钥\n\t * @return /\n\t */\n\tpublic static String generateGoogleSecretKey(String account, String issuer, String secretKey) {\n\t\treturn SaManager.getSaTotpTemplate().generateGoogleSecretKey(account, issuer, secretKey);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/serializer/SaSerializerTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer;\n\n/**\n * 序列化器\n * \n * @author click33\n * @since 1.41.0\n */\npublic interface SaSerializerTemplate {\n\n\t/**\n\t * 序列化：对象 -> 字符串\n\t *\n\t * @param obj /\n\t * @return /\n\t */\n\tString objectToString(Object obj);\n\n\t/**\n\t * 反序列化：字符串 → 对象\n\t *\n\t * @param str /\n\t * @return /\n\t */\n\tObject stringToObject(String str);\n\n\t/**\n\t * 反序列化：字符串 → 对象 (指定类型)\n\t * <p>\n\t *     此方法目前仅为 json 序列化实现类 在 反序列化对象 传递类型信息\n\t * </p>\n\t *\n\t * @param str /\n\t * @return /\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tdefault <T> T stringToObject(String str, Class<T> type) {\n        return (T)stringToObject(str);\n    };\n\n\n\t/**\n\t * 序列化：对象 -> 字节数组\n\t *\n\t * @param obj /\n\t * @return /\n\t */\n\tbyte[] objectToBytes(Object obj);\n\n\t/**\n\t * 反序列化：字节数组 → 对象\n\t *\n\t * @param bytes /\n\t * @return /\n\t */\n\tObject bytesToObject(byte[] bytes);\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdk.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer.impl;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.serializer.SaSerializerTemplate;\n\nimport java.io.*;\n\n/**\n * 序列化器次级实现: jdk序列化\n *\n * @author click33\n * @since 1.41.0\n */\npublic interface SaSerializerTemplateForJdk extends SaSerializerTemplate {\n\n\t@Override\n\tdefault String objectToString(Object obj) {\n\t\tbyte[] bytes = objectToBytes(obj);\n\t\tif (bytes == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn bytesToString(bytes);\n    }\n\n\t@Override\n\tdefault Object stringToObject(String str) {\n\t\tif(str == null) {\n\t\t\treturn null;\n\t\t}\n\t\tbyte[] bytes = stringToBytes(str);\n\t\treturn bytesToObject(bytes);\n    }\n\n\t@Override\n\tdefault byte[] objectToBytes(Object obj) {\n\t\tif (obj == null) {\n\t\t\treturn null;\n\t\t}\n\t\ttry (\n\t\t\tByteArrayOutputStream baos = new ByteArrayOutputStream();\n\t\t\tObjectOutputStream oos = new ObjectOutputStream(baos)\n\t\t) {\n\t\t\toos.writeObject(obj);\n\t\t\treturn baos.toByteArray();\n\t\t} catch (IOException e) {\n\t\t\tthrow new SaTokenException(e);\n\t\t}\n\t}\n\n\t@Override\n\tdefault Object bytesToObject(byte[] bytes) {\n\t\tif(bytes == null) {\n\t\t\treturn null;\n\t\t}\n\t\ttry (\n\t\t\tByteArrayInputStream bais = new ByteArrayInputStream(bytes);\n\t\t\tObjectInputStream ois = new ObjectInputStream(bais)\n\t\t) {\n\t\t\treturn ois.readObject();\n\t\t} catch (IOException | ClassNotFoundException e) {\n\t\t\tthrow new SaTokenException(e);\n\t\t}\n\t}\n\n\t/**\n\t * byte[] 转换为 String\n\t * @param bytes /\n\t * @return /\n\t */\n\tString bytesToString(byte[] bytes);\n\n\t/**\n\t * String 转换为 byte[]\n\t * @param str /\n\t * @return /\n\t */\n\tbyte[] stringToBytes(String str);\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseBase64.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer.impl;\n\nimport java.util.Base64;\n\n/**\n * 序列化器: jdk序列化、Base64 编码 (体积+33%)\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerTemplateForJdkUseBase64 implements SaSerializerTemplateForJdk {\n\n\t@Override\n\tpublic String bytesToString(byte[] bytes) {\n\t\treturn Base64.getEncoder().encodeToString(bytes);\n\t}\n\n\t@Override\n\tpublic byte[] stringToBytes(String str) {\n\t\treturn Base64.getDecoder().decode(str);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseHex.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer.impl;\n\nimport cn.dev33.satoken.util.SaHexUtil;\n\n/**\n * 序列化器: jdk序列化、16 进制编码 (体积+100%)\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerTemplateForJdkUseHex implements SaSerializerTemplateForJdk {\n\n\t@Override\n\tpublic String bytesToString(byte[] bytes) {\n\t\treturn SaHexUtil.bytesToHex(bytes);\n\t}\n\n\t@Override\n\tpublic byte[] stringToBytes(String str) {\n\t\treturn SaHexUtil.hexToBytes(str);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJdkUseISO_8859_1.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer.impl;\n\nimport java.nio.charset.StandardCharsets;\n\n/**\n * 序列化器: jdk序列化、ISO-8859-1 编码 (体积无变化)\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerTemplateForJdkUseISO_8859_1 implements SaSerializerTemplateForJdk {\n\n\t@Override\n\tpublic String bytesToString(byte[] bytes) {\n\t\treturn new String(bytes, StandardCharsets.ISO_8859_1);\n\t}\n\n\t@Override\n\tpublic byte[] stringToBytes(String str) {\n\t\treturn str.getBytes(StandardCharsets.ISO_8859_1);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/serializer/impl/SaSerializerTemplateForJson.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer.impl;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.exception.ApiDisabledException;\nimport cn.dev33.satoken.serializer.SaSerializerTemplate;\n\n/**\n * 序列化器: 使用 json 转换器\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerTemplateForJson implements SaSerializerTemplate {\n\n\t@Override\n\tpublic String objectToString(Object obj) {\n\t\treturn SaManager.getSaJsonTemplate().objectToJson(obj);\n\t}\n\n\t@Override\n\tpublic Object stringToObject(String str) {\n\t\treturn SaManager.getSaJsonTemplate().jsonToObject(str);\n\t}\n\n\t@Override\n\tpublic <T>T stringToObject(String str, Class<T> type) {\n\t\treturn SaManager.getSaJsonTemplate().jsonToObject(str, type);\n\t}\n\n\t@Override\n\tpublic byte[] objectToBytes(Object obj) {\n\t\tthrow new ApiDisabledException(\"json 序列化器不支持 Object -> byte[]\");\n\t}\n\n\t@Override\n\tpublic Object bytesToObject(byte[] bytes) {\n\t\tthrow new ApiDisabledException(\"json 序列化器不支持 byte[] -> Object\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/session/SaSession.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.application.SaSetValueInterface;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.fun.SaTwoParamFunction;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * Session Model，会话作用域的读取值对象\n *\n * <p> 在一次会话范围内: 存值、取值。数据在注销登录后失效。</p>\n * <p>\n *    在 Sa-Token 中，SaSession 分为三种，分别是：\t<br>\n *     \t- Account-Session: 指的是框架为每个 账号id 分配的 SaSession。\t<br>\n * \t\t- Token-Session: 指的是框架为每个 token 分配的 SaSession。\t<br>\n * \t\t- Custom-Session: 指的是以一个 特定的值 作为SessionId，来分配的 SaSession。\t<br>\n * \t  <br>\n * \t  注意：以上分类仅为框架设计层面的概念区分，实际上它们的数据存储格式都是一致的。\n * </p>\n *\n * @author click33\n * @since 1.10.0\n */\npublic class SaSession implements SaSetValueInterface, Serializable {\n\n\t/**\n\t * \n\t */\n\tprivate static final long serialVersionUID = 1L;\n\n\t/**\n\t * 在 SaSession 上存储用户对象时建议使用的 key\n\t */\n\tpublic static final String USER = \"USER\";\n\n\t/**\n\t * 在 SaSession 上存储角色列表时建议使用的 key\n\t */\n\tpublic static final String ROLE_LIST = \"ROLE_LIST\";\n\n\t/**\n\t * 在 SaSession 上存储权限列表时建议使用的 key\n\t */\n\tpublic static final String PERMISSION_LIST = \"PERMISSION_LIST\";\n\n\t/**\n\t * 此 SaSession 的 id\n\t */\n\tprivate String id;\n\n\t/**\n\t * 此 SaSession 的 类型\n\t */\n\tprivate String type;\n\n\t/**\n\t * 所属 loginType\n\t */\n\tprivate String loginType;\n\n\t/**\n\t * 所属 loginId （当此 SaSession 属于 Account-Session 时，此值有效）\n\t */\n\tprivate Object loginId;\n\n\t/**\n\t * 所属 Token （当此 SaSession 属于 Token-Session 时，此值有效）\n\t */\n\tprivate String token;\n\n\t/**\n\t * 当前账号历史总计登录设备数量 （当此 SaSession 属于 Account-Session 时，此值有效）\n\t */\n\tprivate int historyTerminalCount;\n\n\t/**\n\t * 此 SaSession 的创建时间（13位时间戳）\n\t */\n\tprivate long createTime;\n\n\t/**\n\t * 所有挂载数据\n\t */\n\tprivate Map<String, Object> dataMap = new ConcurrentHashMap<>();\n\n\n\t// ----------------------- 构建相关\n\n\t/**\n\t * 构建一个 Session 对象\n\t */\n\tpublic SaSession() {\n\t\t/*\n\t\t * 当 Session 从 Redis 中反序列化取出时，框架会误以为创建了新的Session，\n\t\t * 因此此处不可以调用this(null); 避免监听器收到错误的通知 \n\t\t */\n\t\t// this(null);\n\t}\n\n\t/**\n\t * 构建一个 Session 对象\n\t * @param id Session的id\n\t */\n\tpublic SaSession(String id) {\n\t\tthis.id = id;\n\t\tthis.createTime = System.currentTimeMillis();\n \t\t// $$ 发布事件\n\t\tSaTokenEventCenter.doCreateSession(id);\n\t}\n\n\t/**\n\t * 获取：此 SaSession 的 id\n\t * @return /\n\t */\n\tpublic String getId() {\n\t\treturn this.id;\n\t}\n\n\t/**\n\t * 写入：此 SaSession 的 id\n\t * @param id /\n\t * @return 对象自身\n\t */\n\tpublic SaSession setId(String id) {\n\t\tthis.id = id;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取：此 SaSession 的 类型\n\t *\n\t * @return /\n\t */\n\tpublic String getType() {\n\t\treturn this.type;\n\t}\n\n\t/**\n\t * 设置：此 SaSession 的 类型\n\t *\n\t * @param type /\n\t * @return 对象自身\n\t */\n\tpublic SaSession setType(String type) {\n\t\tthis.type = type;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取：所属 loginType\n\t * @return /\n\t */\n\tpublic String getLoginType() {\n\t\treturn this.loginType;\n\t}\n\n\t/**\n\t * 设置：所属 loginType\n\t * @param loginType /\n\t * @return 对象自身\n\t */\n\tpublic SaSession setLoginType(String loginType) {\n\t\tthis.loginType = loginType;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取：所属 loginId （当此 SaSession 属于 Account-Session 时，此值有效）\n\t * @return /\n\t */\n\tpublic Object getLoginId() {\n\t\treturn this.loginId;\n\t}\n\n\t/**\n\t * 设置：所属 loginId （当此 SaSession 属于 Account-Session 时，此值有效）\n\t * @param loginId /\n\t * @return 对象自身\n\t */\n\tpublic SaSession setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取：所属 Token （当此 SaSession 属于 Token-Session 时，此值有效）\n\t * @return /\n\t */\n\tpublic String getToken() {\n\t\treturn this.token;\n\t}\n\n\t/**\n\t * 设置：所属 Token （当此 SaSession 属于 Token-Session 时，此值有效）\n\t * @param token /\n\t * @return 对象自身\n\t */\n\tpublic SaSession setToken(String token) {\n\t\tthis.token = token;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回：当前 SaSession 的创建时间（13位时间戳）\n\t * @return /\n\t */\n\tpublic long getCreateTime() {\n\t\treturn this.createTime;\n\t}\n\n\t/**\n\t * 写入：此 SaSession 的创建时间（13位时间戳）\n\t * @param createTime /\n\t * @return 对象自身\n\t */\n\tpublic SaSession setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\n\t// ----------------------- SaTerminalInfo 相关\n\n\t/**\n\t * 登录终端信息列表\n\t */\n\tprivate List<SaTerminalInfo> terminalList = new Vector<>();\n\n\t/**\n\t * 写入登录终端信息列表\n\t * @param terminalList /\n\t */\n\tpublic void setTerminalList(List<SaTerminalInfo> terminalList) {\n\t\tthis.terminalList = terminalList;\n\t}\n\n\t/**\n\t * 获取登录终端信息列表\n\t *\n\t * @return /\n\t */\n\tpublic List<SaTerminalInfo> getTerminalList() {\n\t\treturn terminalList;\n\t}\n\n\t/**\n\t * 获取 登录终端信息列表 (拷贝副本)\n\t *\n\t * @return /\n\t */\n\tpublic List<SaTerminalInfo> terminalListCopy() {\n\t\treturn new ArrayList<>(terminalList);\n\t}\n\n\t/**\n\t * 获取 登录终端信息列表 (拷贝副本)，根据 deviceType 筛选\n\t *\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return /\n\t */\n\tpublic List<SaTerminalInfo> getTerminalListByDeviceType(String deviceType) {\n\t\t// 返回全部\n\t\tif(deviceType == null) {\n\t\t\treturn terminalListCopy();\n\t\t}\n\t\t// 返回筛选后的\n\t\tList<SaTerminalInfo> copyList = terminalListCopy();\n\t\tList<SaTerminalInfo> newList = new ArrayList<>();\n\t\tfor (SaTerminalInfo terminal : copyList) {\n\t\t\tif(SaFoxUtil.equals(terminal.getDeviceType(), deviceType)) {\n\t\t\t\tnewList.add(terminal);\n\t\t\t}\n\t\t}\n\t\treturn newList;\n\t}\n\n\t/**\n\t * 获取 登录终端 token 列表\n\t *\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic List<String> getTokenValueListByDeviceType(String deviceType) {\n\t\tList<String> tokenValueList = new ArrayList<>();\n\t\tfor (SaTerminalInfo terminal : getTerminalListByDeviceType(deviceType)) {\n\t\t\ttokenValueList.add(terminal.getTokenValue());\n\t\t}\n\t\treturn tokenValueList;\n\t}\n\n\t/**\n\t * 查找一个终端信息，根据 tokenValue\n\t *\n\t * @param tokenValue /\n\t * @return /\n\t */\n\tpublic SaTerminalInfo getTerminal(String tokenValue) {\n\t\tfor (SaTerminalInfo terminal : terminalListCopy()) {\n\t\t\tif (SaFoxUtil.equals(terminal.getTokenValue(), tokenValue)) {\n\t\t\t\treturn terminal;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 添加一个终端信息\n\t *\n\t * @param terminalInfo /\n\t */\n\tpublic void addTerminal(SaTerminalInfo terminalInfo) {\n\t\t// 根据 tokenValue 值查重，如果存在旧的，则先删除\n\t\tSaTerminalInfo oldTerminal = getTerminal(terminalInfo.getTokenValue());\n\t\tif(oldTerminal != null) {\n\t\t\tterminalList.remove(oldTerminal);\n\t\t}\n\t\t// 然后添加新的\n\t\tthis.historyTerminalCount++;\n\t\tterminalInfo.setIndex(this.historyTerminalCount);\n\t\tterminalList.add(terminalInfo);\n\t\tupdate();\n\t}\n\n\t/**\n\t * 移除一个终端信息\n\t *\n\t * @param tokenValue token值 \n\t */\n\tpublic void removeTerminal(String tokenValue) {\n\t\tSaTerminalInfo terminalInfo = getTerminal(tokenValue);\n\t\tif (terminalList.remove(terminalInfo)) {\n\t\t\tupdate();\n\t\t}\n\t}\n\n\t/**\n\t * 获取 当前账号历史总计登录设备数量 （当此 SaSession 属于 Account-Session 时，此值有效）\n\t *\n\t * @return /\n\t */\n\tpublic int getHistoryTerminalCount() {\n\t\treturn this.historyTerminalCount;\n\t}\n\n\t/**\n\t * 设置 当前账号历史总计登录设备数量 （当此 SaSession 属于 Account-Session 时，此值有效）\n\t *\n\t * @param historyTerminalCount /\n\t */\n\tpublic void setHistoryTerminalCount(int historyTerminalCount) {\n\t\tthis.historyTerminalCount = historyTerminalCount;\n\t}\n\n\t/**\n\t * 遍历 terminalList 列表，执行特定函数\n\t *\n\t * @param function 需要执行的函数\n\t */\n\tpublic void forEachTerminalList(SaTwoParamFunction<SaSession, SaTerminalInfo> function) {\n\t\tfor (SaTerminalInfo terminalInfo: terminalListCopy()) {\n\t\t\tfunction.run(this, terminalInfo);\n\t\t}\n\t}\n\n\n\t/**\n\t * 判断指定设备 id 是否为可信任设备\n\t * @param deviceId /\n\t * @return /\n\t */\n\tpublic boolean isTrustDeviceId(String deviceId) {\n\t\tif(SaFoxUtil.isEmpty(deviceId)) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (SaTerminalInfo terminal : terminalListCopy()) {\n\t\t\tif (SaFoxUtil.equals(terminal.getDeviceId(), deviceId)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\n\t// ----------------------- 一些操作\n\n\t/**\n\t * 更新Session（从持久库更新刷新一下）\n\t */\n\tpublic void update() {\n\t\tSaManager.getSaTokenDao().updateSession(this);\n\t}\n\n\t/** 注销Session (从持久库删除) */\n\tpublic void logout() {\n\t\tSaManager.getSaTokenDao().deleteSession(this.id);\n \t\t// $$ 发布事件 \n\t\tSaTokenEventCenter.doLogoutSession(id);\n\t}\n\n\t/** 当 Session 上的 SaTerminalInfo 数量为零时，注销会话 */\n\tpublic void logoutByTerminalCountToZero() {\n\t\tif (terminalList.isEmpty()) {\n\t\t\tlogout();\n\t\t}\n\t}\n\n\t/**\n\t * 获取此Session的剩余存活时间 (单位: 秒) \n\t * @return 此Session的剩余存活时间 (单位: 秒)\n\t */\n\tpublic long timeout() {\n\t\treturn SaManager.getSaTokenDao().getSessionTimeout(this.id);\n\t}\n\t\n\t/**\n\t * 修改此Session的剩余存活时间\n\t * @param timeout 过期时间 (单位: 秒) \n\t */\n\tpublic void updateTimeout(long timeout) {\n\t\tSaManager.getSaTokenDao().updateSessionTimeout(this.id, timeout);\n\t}\n\t\n\t/**\n\t * 修改此Session的最小剩余存活时间 (只有在 Session 的过期时间低于指定的 minTimeout 时才会进行修改)\n\t * @param minTimeout 过期时间 (单位: 秒) \n\t */\n\tpublic void updateMinTimeout(long minTimeout) {\n\t\tlong min = trans(minTimeout);\n\t\tlong curr = trans(timeout());\n\t\tif(curr < min) {\n\t\t\tupdateTimeout(minTimeout);\n\t\t}\n\t}\n\n\t/**\n\t * 修改此Session的最大剩余存活时间 (只有在 Session 的过期时间高于指定的 maxTimeout 时才会进行修改)\n\t * @param maxTimeout 过期时间 (单位: 秒) \n\t */\n\tpublic void updateMaxTimeout(long maxTimeout) {\n\t\tlong max = trans(maxTimeout);\n\t\tlong curr = trans(timeout());\n\t\tif(curr > max) {\n\t\t\tupdateTimeout(maxTimeout);\n\t\t}\n\t}\n\t\n\t/**\n\t * value为 -1 时返回 Long.MAX_VALUE，否则原样返回 \n\t * @param value /\n\t * @return /\n\t */\n\tprotected long trans(long value) {\n\t\treturn value == SaTokenDao.NEVER_EXPIRE ? Long.MAX_VALUE : value;\n\t}\n\n\n\t// ----------------------- 存取值 (类型转换)\n\n\t// ---- 重写接口方法 \n\t\n\t/**\n\t * 取值 \n\t * @param key key \n\t * @return 值 \n\t */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn dataMap.get(key);\n\t}\n\t\n\t/**\n\t * 写值 \n\t * @param key   名称\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaSession set(String key, Object value) {\n\t\tdataMap.put(key, value);\n\t\tupdate();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写值 (只有在此 key 原本无值的情况下才会写入)\n\t * @param key   名称\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaSession setByNull(String key, Object value) {\n\t\tif( ! has(key)) {\n\t\t\tdataMap.put(key, value);\n\t\t\tupdate();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 删值\n\t * @param key 要删除的key\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaSession delete(String key) {\n\t\tdataMap.remove(key);\n\t\tupdate();\n\t\treturn this;\n\t}\n\n\n\t// ----------------------- 其它方法\n\n\t/**\n\t * 返回当前 Session 挂载数据的所有 key\n\t *\n\t * @return key 列表\n\t */\n\tpublic Set<String> keys() {\n\t\treturn dataMap.keySet();\n\t}\n\t\n\t/**\n\t * 清空所有挂载数据\n\t */\n\tpublic void clear() {\n\t\tdataMap.clear();\n\t\tupdate();\n\t}\n\n\t/**\n\t * 获取数据挂载集合（如果更新map里的值，请调用 session.update() 方法避免产生脏数据 ）\n\t *\n\t * @return 返回底层储存值的map对象\n\t */\n\tpublic Map<String, Object> getDataMap() {\n\t\treturn dataMap;\n\t}\n\n\t/**\n\t * 设置数据挂载集合 (改变底层对象引用，将 dataMap 整个对象替换)\n\t * @param dataMap 数据集合\n\t *\n\t * @return 对象自身\n\t */\n\tpublic SaSession setDataMap(Map<String, Object> dataMap) {\n\t\tthis.dataMap = dataMap;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入数据集合 (不改变底层对象引用，只将此 dataMap 所有数据进行替换)\n\t * @param dataMap 数据集合 \n\t */\n\tpublic SaSession refreshDataMap(Map<String, Object> dataMap) {\n\t\tthis.dataMap.clear();\n\t\tthis.dataMap.putAll(dataMap);\n\t\tthis.update();\n\t\treturn this;\n\t}\n\n\t//\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionCustomUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\n\n/**\n * 自定义 SaSession 工具类，快捷的读取、操作自定义 SaSession\n * \n * <p>样例：\n * <pre>\n * \t\t// 在一处代码写入数据 \n * \t\tSaSession session = SaSessionCustomUtil.getSessionById(\"role-\" + 1001);\n * \t\tsession.set(\"count\", 1);\n * \t\n * \t\t// 在另一处代码获取数据 \n * \t\tSaSession session = SaSessionCustomUtil.getSessionById(\"role-\" + 1001);\n * \t\tint count = session.getInt(\"count\");\n * \t\tSystem.out.println(\"count=\" + count);\n * </pre>\n * \n * @author click33\n * @since 1.10.0\n */\npublic class SaSessionCustomUtil {\n\n\tprivate SaSessionCustomUtil() {\n\t}\n\t\n\t/**\n\t * 添加上指定前缀，防止恶意伪造数据\n\t */\n\tpublic static String sessionKey = \"custom\";\n\n\t/**\n\t * 拼接Key: 在存储自定义 SaSession 时应该使用的 key\n\t *\n\t * @param sessionId 会话id\n\t * @return sessionId\n\t */\n\tpublic static String splicingSessionKey(String sessionId) {\n\t\treturn SaManager.getConfig().getTokenName() + \":\" + sessionKey + \":session:\" + sessionId;\n\t}\n\n\t/**\n\t * 判断：指定 key 的 SaSession 是否存在\n\t * \n\t * @param sessionId SaSession 的 id\n\t * @return 是否存在\n\t */\n\tpublic static boolean isExists(String sessionId) {\n\t\treturn SaManager.getSaTokenDao().getSession(splicingSessionKey(sessionId)) != null;\n\t}\n\n\t/**\n\t * 获取指定 key 的 SaSession 对象, 如果此 SaSession 尚未在 DB 创建，isCreate 参数代表是否则新建并返回\n\t * \n\t * @param sessionId SaSession 的 id\n\t * @param isCreate  如果此 SaSession 尚未在 DB 创建，是否新建并返回\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionById(String sessionId, boolean isCreate) {\n\t\tSaSession session = SaManager.getSaTokenDao().getSession(splicingSessionKey(sessionId));\n\t\tif (session == null && isCreate) {\n\t\t\tsession = SaStrategy.instance.createSession.apply(splicingSessionKey(sessionId));\n\t\t\tsession.setType(SaTokenConsts.SESSION_TYPE__CUSTOM);\n\t\t\tSaManager.getSaTokenDao().setSession(session, SaManager.getConfig().getTimeout());\t\t\n\t\t}\n\t\treturn session;\n\t}\n\n\t/**\n\t * 获取指定 key 的 SaSession, 如果此 SaSession 尚未在 DB 创建，则新建并返回\n\t * \n\t * @param sessionId SaSession 的 id\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionById(String sessionId) {\n\t\treturn getSessionById(sessionId, true);\n\t}\n\n\t/**\n\t * 删除指定 key 的 SaSession\n\t * \n\t * @param sessionId SaSession 的 id\n\t */\n\tpublic static void deleteSessionById(String sessionId) {\n\t\tSaManager.getSaTokenDao().deleteSession(splicingSessionKey(sessionId));\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/session/SaTerminalInfo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 登录设备终端信息 Model\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTerminalInfo implements Serializable {\n\n\t/**\n\t *\n\t */\n\tprivate static final long serialVersionUID = 1406115065849845073L;\n\n\t/**\n\t * 登录会话索引值 (该账号第几个登录的设备, 从 1 开始)\n\t */\n\tprivate int index;\n\n\t/**\n\t * Token 值\n\t */\n\tprivate String tokenValue;\n\n\t/**\n\t * 所属设备类型，例如：PC、WEB、HD、MOBILE、APP\n\t */\n\tprivate String deviceType;\n\n\t/**\n\t * 登录设备唯一标识，例如：kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM\n\t */\n\tprivate String deviceId;\n\n\t/**\n\t * 此次登录的自定义扩展数据 (只允许在登录前设定，登录后不建议更改)\n\t */\n\tprivate Map<String, Object> extraData;\n\n\t/**\n\t * 创建时间\n\t */\n\tprivate long createTime;\n\n\t/**\n\t * 构建一个\n\t */\n\tpublic SaTerminalInfo() {\n\t}\n\n\t/**\n\t * 构建一个\n\t *\n\t * @param index \t\t登录会话索引值 (该账号第几个登录的设备)\n\t * @param tokenValue  \tToken 值\n\t * @param deviceType \t所属设备类型\n\t * @param extraData \t\t\t此客户端登录的挂载数据\n\t */\n\tpublic SaTerminalInfo(int index, String tokenValue, String deviceType, Map<String, Object> extraData) {\n\t\tthis.index = index;\n\t\tthis.tokenValue = tokenValue;\n\t\tthis.deviceType = deviceType;\n\t\tthis.extraData = extraData;\n\t\tthis.createTime = System.currentTimeMillis();\n\t}\n\n\t// 扩展方法\n\n\t/**\n\t * 此次登录的自定义扩展数据 (只允许在登录前设定，登录后不建议更改)\n\t * @param key 键\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\tpublic SaTerminalInfo setExtra(String key, Object value) {\n\t\tif(this.extraData == null) {\n\t\t\tthis.extraData = new LinkedHashMap<>();\n\t\t}\n\t\tthis.extraData.put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 此次登录的自定义扩展数据\n\t * @param key 键\n\t * @return 扩展数据的值\n\t */\n\tpublic Object getExtra(String key) {\n\t\tif(this.extraData == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.extraData.get(key);\n\t}\n\n\t/**\n\t * 判断是否设置了扩展数据\n\t * @return /\n\t */\n\tpublic boolean haveExtraData() {\n\t\treturn extraData != null && !extraData.isEmpty();\n\t}\n\n\n\n\t// -------------------- get/set --------------------\n\n\t/**\n\t * 获取 登录会话索引值 (该账号第几个登录的设备)\n\t *\n\t * @return index 登录会话索引值 (该账号第几个登录的设备)\n\t */\n\tpublic int getIndex() {\n\t\treturn this.index;\n\t}\n\n\t/**\n\t * 设置 登录会话索引值 (该账号第几个登录的设备)\n\t *\n\t * @param index 登录会话索引值 (该账号第几个登录的设备)\n\t * @return 对象自身\n\t */\n\tpublic SaTerminalInfo setIndex(int index) {\n\t\tthis.index = index;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return Token 值\n\t */\n\tpublic String getTokenValue() {\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * 写入 Token 值\n\t *\n\t * @param tokenValue /\n\t * @return 对象自身\n\t */\n\tpublic SaTerminalInfo setTokenValue(String tokenValue) {\n\t\tthis.tokenValue = tokenValue;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 所属设备类型\n\t */\n\tpublic String getDeviceType() {\n\t\treturn deviceType;\n\t}\n\n\t/**\n\t * 写入所属设备类型\n\t * \n\t * @param deviceType /\n\t * @return 对象自身\n\t */\n\tpublic SaTerminalInfo setDeviceType(String deviceType) {\n\t\tthis.deviceType = deviceType;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 登录设备唯一标识\n\t *\n\t * @return deviceId 登录设备唯一标识\n\t */\n\tpublic String getDeviceId() {\n\t\treturn this.deviceId;\n\t}\n\n\t/**\n\t * 设置 登录设备唯一标识，例如：kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM\n\t *\n\t * @param deviceId 登录设备唯一标识，例如：kQwIOrYvnXmSDkwEiFngrKidMcdrgKorXmSDkwEiFngrKidM\n\t * @return 对象自身\n\t */\n\tpublic SaTerminalInfo setDeviceId(String deviceId) {\n\t\tthis.deviceId = deviceId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 此客户端登录的挂载数据\n\t *\n\t * @return /\n\t */\n\tpublic Map<String, Object> getExtraData() {\n\t\treturn this.extraData;\n\t}\n\n\t/**\n\t * 设置 此客户端登录的挂载数据\n\t *\n\t * @param extraData /\n\t * @return 对象自身\n\t */\n\tpublic SaTerminalInfo setExtraData(Map<String, Object> extraData) {\n\t\tthis.extraData = extraData;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 创建时间\n\t *\n\t * @return createTime 创建时间\n\t */\n\tpublic long getCreateTime() {\n\t\treturn this.createTime;\n\t}\n\n\t/**\n\t * 设置 创建时间\n\t *\n\t * @param createTime 创建时间\n\t * @return 对象自身\n\t */\n\tpublic SaTerminalInfo setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\t//\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaTerminalInfo [\" +\n\t\t\t\t\"index=\" + index +\n\t\t\t\t\", tokenValue='\" + tokenValue +\n\t\t\t\t\", deviceType='\" + deviceType +\n\t\t\t\t\", deviceId='\" + deviceId +\n\t\t\t\t\", extraData=\" + extraData +\n\t\t\t\t\", createTime=\" + createTime +\n\t\t\t\t']';\n\t}\n\n\t/*\n\t * Expand in the future:\n\t * \t\tdeviceName  登录设备端名称，一般为浏览器名称\n\t * \t\tsystemName  登录设备操作系统名称\n\t * \t\tloginIp  登录IP地址\n\t * \t\taddress  登录设备地理位置\n\t * \t\tloginTime  登录时间\n\t */\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session.raw;\n\nimport cn.dev33.satoken.session.SaSession;\n\n/**\n * SaSession 读写工具类 委托\n * \n * @author click33\n * @since 1.42.0\n */\npublic class SaRawSessionDelegator {\n\n\t/**\n\t * raw session 类型\n\t */\n\tpublic String type;\n\n\tpublic SaRawSessionDelegator(String type) {\n\t\tthis.type = type;\n\t}\n\n\t/**\n\t * 判断：指定 SaSession 是否存在\n\t *\n\t * @param valueId /\n\t * @return 是否存在\n\t */\n\tpublic boolean isExists(Object valueId) {\n\t\treturn SaRawSessionUtil.isExists(type, valueId);\n\t}\n\n\t/**\n\t * 获取指定 SaSession 对象, 如果此 SaSession 尚未在 Cache 创建，isCreate 参数代表是否则新建并返回\n\t *\n\t * @param valueId /\n\t * @param isCreate  如果此 SaSession 尚未在 DB 创建，是否新建并返回\n\t * @return SaSession 对象\n\t */\n\tpublic SaSession getSessionById(Object valueId, boolean isCreate) {\n\t\treturn SaRawSessionUtil.getSessionById(type, valueId, isCreate);\n\t}\n\n\t/**\n\t * 获取指定 SaSession, 如果此 SaSession 尚未在 DB 创建，则新建并返回\n\t *\n\t * @param valueId /\n\t * @return SaSession 对象\n\t */\n\tpublic SaSession getSessionById(Object valueId) {\n\t\treturn SaRawSessionUtil.getSessionById(type, valueId);\n\t}\n\n\t/**\n\t * 删除指定 SaSession\n\t *\n\t * @param valueId /\n\t */\n\tpublic void deleteSessionById(Object valueId) {\n\t\tSaRawSessionUtil.deleteSessionById(type, valueId);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session.raw;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\n/**\n * SaSession 读写工具类\n * \n * @author click33\n * @since 1.42.0\n */\npublic class SaRawSessionUtil {\n\n\tprivate SaRawSessionUtil() {\n\t}\n\n\t/**\n\t * 拼接Key: 在存储 SaSession 时应该使用的 key\n\t *\n\t * @param type 类型\n\t * @param valueId 唯一标识\n\t * @return sessionId\n\t */\n\tpublic static String splicingSessionKey(String type, Object valueId) {\n\t\treturn SaManager.getConfig().getTokenName() + \":raw-session:\" + type + \":\" + valueId;\n\t}\n\n\t/**\n\t * 判断：指定 SaSession 是否存在\n\t *\n\t * @param type /\n\t * @param valueId /\n\t * @return 是否存在\n\t */\n\tpublic static boolean isExists(String type, Object valueId) {\n\t\treturn SaManager.getSaTokenDao().getSession(splicingSessionKey(type, valueId)) != null;\n\t}\n\n\t/**\n\t * 获取指定 SaSession 对象, 如果此 SaSession 尚未在 Cache 创建，isCreate 参数代表是否则新建并返回\n\t *\n\t * @param type /\n\t * @param valueId /\n\t * @param isCreate  如果此 SaSession 尚未在 DB 创建，是否新建并返回\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionById(String type, Object valueId, boolean isCreate) {\n\t\tString sessionId = splicingSessionKey(type, valueId);\n\t\tSaSession session = SaManager.getSaTokenDao().getSession(sessionId);\n\t\tif (session == null && isCreate) {\n\t\t\tsession = SaStrategy.instance.createSession.apply(sessionId);\n\t\t\tsession.setType(type);\n\t\t\tSaManager.getSaTokenDao().setSession(session, SaManager.getConfig().getTimeout());\n\t\t}\n\t\treturn session;\n\t}\n\n\t/**\n\t * 获取指定 SaSession, 如果此 SaSession 尚未在 DB 创建，则新建并返回\n\t *\n\t * @param type /\n\t * @param valueId /\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionById(String type, Object valueId) {\n\t\treturn getSessionById(type, valueId, true);\n\t}\n\n\t/**\n\t * 删除指定 SaSession\n\t *\n\t * @param type /\n\t * @param valueId /\n\t */\n\tpublic static void deleteSessionById(String type, Object valueId) {\n\t\tSaManager.getSaTokenDao().deleteSession(splicingSessionKey(type, valueId));\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\n\nimport java.util.Map;\n\n/**\n * <h2> 请更换为 new SaLoginParameter() </h2>\n *\n * 快速、简洁的构建：调用 `StpUtil.login()` 时的 [ 配置参数 SaLoginParameter ]\n *\n * <pre>\n *     \t// 例如：在登录时指定 token 有效期为七天，代码如下：\n *     \tStpUtil.login(10001, SaLoginConfig.setTimeout(60 * 60 * 24 * 7));\n *\n *     \t// 上面的代码与下面的代码等价\n *     \tStpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));\n * </pre>\n * \n * @author click33\n * @since 1.29.0\n */\n@Deprecated\npublic class SaLoginConfig {\n\t\n\tprivate SaLoginConfig() {\n\t}\n\n\t/**\n\t * @param device 此次登录的客户端设备类型 \n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setDevice(String device) {\n\t\treturn create().setDeviceType(device);\n\t}\n\n\t/**\n\t * @param isLastingCookie 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setIsLastingCookie(Boolean isLastingCookie) {\n\t\treturn create().setIsLastingCookie(isLastingCookie);\n\t}\n\n\t/**\n\t * @param timeout 指定此次登录token的有效期, 单位:秒 （如未指定，自动取全局配置的timeout值）\n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setTimeout(long timeout) {\n\t\treturn create().setTimeout(timeout);\n\t}\n\n\t/**\n\t * @param activeTimeout 指定此次登录 token 最低活跃频率，单位：秒（如未指定，自动取全局配置的 activeTimeout 值）\n\t * @return 对象自身\n\t */\n\tpublic static SaLoginParameter setActiveTimeout(long activeTimeout) {\n\t\treturn create().setActiveTimeout(activeTimeout);\n\t}\n\n\t/**\n\t * @param extraData 扩展信息（只在jwt模式下生效）\n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setExtraData(Map<String, Object> extraData) {\n\t\treturn create().setExtraData(extraData);\n\t}\n\n\t/**\n\t * @param token 预定Token（预定本次登录生成的Token值）\n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setToken(String token) {\n\t\treturn create().setToken(token);\n\t}\n\n\t/**\n\t * 写入扩展数据（只在jwt模式下生效） \n\t * @param key 键\n\t * @param value 值 \n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setExtra(String key, Object value) {\n\t\treturn create().setExtra(key, value);\n\t}\n\n\t/**\n\t * @param isWriteHeader 是否在登录后将 Token 写入到响应头\n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setIsWriteHeader(Boolean isWriteHeader) {\n\t\treturn create().setIsWriteHeader(isWriteHeader);\n\t}\n\n\t/**\n\t * 设置 本次登录挂载到 TokenSign 的数据\n\t *\n\t * @param tokenSignTag /\n\t * @return 登录参数 Model\n\t */\n\tpublic static SaLoginParameter setTokenSignTag(Map<String, Object> tokenSignTag) {\n\t\treturn create().setTerminalExtraData(tokenSignTag);\n\t}\n\n\t/**\n\t * 静态方法获取一个 SaLoginParameter 对象\n\t * @return SaLoginParameter 对象\n\t */\n\tpublic static SaLoginParameter create() {\n\t\treturn new SaLoginParameter(SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/SaLoginModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp;\n\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\n\n/**\n * <h2> 请更改为 SaLoginParameter </h2>\n * 在调用 `StpUtil.login()` 时的 配置参数 Model，决定登录的一些细节行为 <br>\n *\n * @author click33\n * @since 1.13.2\n */\n@Deprecated\npublic class SaLoginModel extends SaLoginParameter {\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/SaTokenInfo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp;\n\n/**\n * Token 信息 Model: 用来描述一个 Token 的常见参数。\n *\n * <p>\n *     例如：<br>\n *     <pre>\n *     {\n *         \"tokenName\": \"satoken\",           // token名称\n *         \"tokenValue\": \"e67b99f1-3d7a-4a8d-bb2f-e888a0805633\",      // token值\n *         \"isLogin\": true,                  // 此token是否已经登录\n *         \"loginId\": \"10001\",               // 此token对应的LoginId，未登录时为null\n *         \"loginType\": \"login\",              // 账号类型标识\n *         \"tokenTimeout\": 2591977,          // token剩余有效期 (单位: 秒)\n *         \"sessionTimeout\": 2591977,        // Account-Session剩余有效时间 (单位: 秒)\n *         \"tokenSessionTimeout\": -2,        // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)\n *         \"tokenActiveTimeout\": -1,       // Token 距离被冻结还剩多少时间 (单位: 秒)\n *         \"loginDevice\": \"DEF\"   // 登录设备类型\n *     }\n *     </pre>\n * </p>\n * \n * @author click33\n * @since 1.10.0\n */\npublic class SaTokenInfo {\n\n\t/** token 名称 */\n\tpublic String tokenName;\n\n\t/** token 值 */\n\tpublic String tokenValue;\n\n\t/** 此 token 是否已经登录 */\n\tpublic Boolean isLogin;\n\n\t/** 此 token 对应的 LoginId，未登录时为 null */\n\tpublic Object loginId;\n\n\t/** 多账号体系下的账号类型 */\n\tpublic String loginType;\n\n\t/** token 剩余有效期（单位: 秒） */\n\tpublic long tokenTimeout;\n\n\t/** Account-Session 剩余有效时间（单位: 秒） */\n\tpublic long sessionTimeout;\n\n\t/** Token-Session 剩余有效时间（单位: 秒） */\n\tpublic long tokenSessionTimeout;\n\n\t/** token 距离被冻结还剩多少时间（单位: 秒） */\n\tpublic long tokenActiveTimeout;\n\n\t/** 登录设备类型 */\n\tpublic String loginDeviceType;\n\n\t/** 自定义数据（暂无意义，留作扩展） */\n\tpublic String tag;\n\t\n\n\n\t/**\n\t * @return token 名称\n\t */\n\tpublic String getTokenName() {\n\t\treturn tokenName;\n\t}\n\n\t/**\n\t * @param tokenName token 名称\n\t */\n\tpublic void setTokenName(String tokenName) {\n\t\tthis.tokenName = tokenName;\n\t}\n\n\t/**\n\t * @return token 值\n\t */\n\tpublic String getTokenValue() {\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * @param tokenValue token 值\n\t */\n\tpublic void setTokenValue(String tokenValue) {\n\t\tthis.tokenValue = tokenValue;\n\t}\n\n\t/**\n\t * @return 此 token 是否已经登录\n\t */\n\tpublic Boolean getIsLogin() {\n\t\treturn isLogin;\n\t}\n\n\t/**\n\t * @param isLogin 此 token 是否已经登录\n\t */\n\tpublic void setIsLogin(Boolean isLogin) {\n\t\tthis.isLogin = isLogin;\n\t}\n\n\t/**\n\t * @return 此 token 对应的LoginId，未登录时为null\n\t */\n\tpublic Object getLoginId() {\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * @param loginId 此 token 对应的LoginId，未登录时为null\n\t */\n\tpublic void setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t}\n\n\t/**\n\t * @return 多账号体系下的账号类型\n\t */\n\tpublic String getLoginType() {\n\t\treturn loginType;\n\t}\n\n\t/**\n\t * @param loginType 多账号体系下的账号类型\n\t */\n\tpublic void setLoginType(String loginType) {\n\t\tthis.loginType = loginType;\n\t}\n\n\t/**\n\t * @return token 剩余有效期（单位: 秒）\n\t */\n\tpublic long getTokenTimeout() {\n\t\treturn tokenTimeout;\n\t}\n\n\t/**\n\t * @param tokenTimeout token剩余有效期（单位: 秒）\n\t */\n\tpublic void setTokenTimeout(long tokenTimeout) {\n\t\tthis.tokenTimeout = tokenTimeout;\n\t}\n\n\t/**\n\t * @return Account-Session 剩余有效时间（单位: 秒）\n\t */\n\tpublic long getSessionTimeout() {\n\t\treturn sessionTimeout;\n\t}\n\n\t/**\n\t * @param sessionTimeout Account-Session剩余有效时间（单位: 秒）\n\t */\n\tpublic void setSessionTimeout(long sessionTimeout) {\n\t\tthis.sessionTimeout = sessionTimeout;\n\t}\n\n\t/**\n\t * @return Token-Session剩余有效时间（单位: 秒）\n\t */\n\tpublic long getTokenSessionTimeout() {\n\t\treturn tokenSessionTimeout;\n\t}\n\n\t/**\n\t * @param tokenSessionTimeout Token-Session剩余有效时间（单位: 秒）\n\t */\n\tpublic void setTokenSessionTimeout(long tokenSessionTimeout) {\n\t\tthis.tokenSessionTimeout = tokenSessionTimeout;\n\t}\n\n\t/**\n\t * @return token 距离被冻结还剩多少时间（单位: 秒）\n\t */\n\tpublic long getTokenActiveTimeout() {\n\t\treturn tokenActiveTimeout;\n\t}\n\n\t/**\n\t * @param tokenActiveTimeout token 距离被冻结还剩多少时间（单位: 秒）\n\t */\n\tpublic void setTokenActiveTimeout(long tokenActiveTimeout) {\n\t\tthis.tokenActiveTimeout = tokenActiveTimeout;\n\t}\n\n\t/**\n\t * @return 登录设备类型\n\t */\n\tpublic String getLoginDeviceType() {\n\t\treturn loginDeviceType;\n\t}\n\n\t/**\n\t * @param loginDeviceType 登录设备类型\n\t */\n\tpublic void setLoginDeviceType(String loginDeviceType) {\n\t\tthis.loginDeviceType = loginDeviceType;\n\t}\n\n\t/**\n\t * @return 自定义数据（暂无意义，留作扩展）\n\t */\n\tpublic String getTag() {\n\t\treturn tag;\n\t}\n\n\t/**\n\t * @param tag 自定义数据（暂无意义，留作扩展）\n\t */\n\tpublic void setTag(String tag) {\n\t\tthis.tag = tag;\n\t}\n\n\t/**\n\t * toString\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaTokenInfo [tokenName=\" + tokenName + \", tokenValue=\" + tokenValue + \", isLogin=\" + isLogin\n\t\t\t\t+ \", loginId=\" + loginId + \", loginType=\" + loginType + \", tokenTimeout=\" + tokenTimeout\n\t\t\t\t+ \", sessionTimeout=\" + sessionTimeout + \", tokenSessionTimeout=\" + tokenSessionTimeout\n\t\t\t\t+ \", tokenActiveTimeout=\" + tokenActiveTimeout + \", loginDeviceType=\" + loginDeviceType + \", tag=\" + tag\n\t\t\t\t+ \"]\";\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterface.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp;\n\nimport cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo;\n\nimport java.util.List;\n\n/**\n * 权限数据源加载接口\n *\n * <p>\n *     在使用权限校验 API 之前，你必须实现此接口，告诉框架哪些用户拥有哪些权限。<br>\n *     框架默认不对数据进行缓存，如果你的数据是从数据库中读取的，一般情况下你需要手动实现数据的缓存读写。\n * </p>\n * \n * @author click33\n * @since 1.10.0\n */\npublic interface StpInterface {\n\n\t/**\n\t * 返回指定账号id所拥有的权限码集合 \n\t * \n\t * @param loginId  账号id\n\t * @param loginType 账号类型\n\t * @return 该账号id具有的权限码集合\n\t */\n\tList<String> getPermissionList(Object loginId, String loginType);\n\n\t/**\n\t * 返回指定账号id所拥有的角色标识集合 \n\t * \n\t * @param loginId  账号id\n\t * @param loginType 账号类型\n\t * @return 该账号id具有的角色标识集合\n\t */\n\tList<String> getRoleList(Object loginId, String loginType);\n\n\t/**\n\t * 返回指定账号 id 是否被封禁\n\t *\n\t * @param loginId  账号id\n\t * @param service 业务标识符\n\t * @return 描述该账号是否封禁的包装信息对象\n\t */\n\tdefault SaDisableWrapperInfo isDisabled(Object loginId, String service) {\n\t\treturn SaDisableWrapperInfo.createNotDisabled();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/StpInterfaceDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 对 {@link StpInterface} 接口默认的实现类\n * <p>\n * 如果开发者没有实现 StpInterface 接口，则框架会使用此默认实现类，所有方法都返回空集合，即：用户不具有任何权限和角色。\n * \n * @author click33\n * @since 1.10.0\n */\npublic class StpInterfaceDefaultImpl implements StpInterface {\n\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\treturn new ArrayList<>();\n\t}\n\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\treturn new ArrayList<>();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaCookieConfig;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaCookie;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.*;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaTwoParamFunction;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.stp.parameter.enums.SaLogoutMode;\nimport cn.dev33.satoken.stp.parameter.enums.SaLogoutRange;\nimport cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode;\nimport cn.dev33.satoken.stp.parameter.enums.SaReplacedRange;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport cn.dev33.satoken.util.SaValue2Box;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\nimport static cn.dev33.satoken.exception.NotLoginException.*;\n\n\n/**\n * Sa-Token 权限认证，逻辑实现类\n *\n * <p>\n *     Sa-Token 的核心，框架大多数功能均由此类提供具体逻辑实现。\n * </p>\n *\n * @author click33\n * @since 1.10.0\n */\npublic class StpLogic {\n\n\t/**\n\t * 账号类型标识，多账号体系时（一个系统多套用户表）用此值区分具体要校验的是哪套用户，比如：login、user、admin\n\t */\n\tpublic String loginType;\n\n\t/**\n\t * 初始化 StpLogic, 并指定账号类型\n\t *\n\t * @param loginType 账号类型标识\n\t */\n\tpublic StpLogic(String loginType) {\n\t\tsetLoginType(loginType);\n\t}\n\n\t/**\n\t * 获取当前 StpLogic 账号类型标识\n\t *\n\t * @return /\n\t */\n\tpublic String getLoginType(){\n\t\treturn loginType;\n\t}\n\n\t/**\n\t * 安全的重置当前账号类型\n\t *\n\t * @param loginType 账号类型标识\n\t * @return 对象自身\n\t */\n\tpublic StpLogic setLoginType(String loginType){\n\n\t\t// 先清除此 StpLogic 在全局 SaManager 中的记录\n\t\tif(SaFoxUtil.isNotEmpty(this.loginType)) {\n\t\t\tSaManager.removeStpLogic(this.loginType);\n\t\t}\n\n\t\t// 赋值\n\t\tthis.loginType = loginType;\n\n\t\t// 将新的 loginType -> StpLogic 映射关系 put 到 SaManager 全局集合中，以便后续根据 LoginType 进行查找此对象\n\t\tSaManager.putStpLogic(this);\n\n\t\treturn this;\n\t}\n\n\tprivate SaTokenConfig config;\n\n\t/**\n\t * 写入当前 StpLogic 单独使用的配置对象\n\t *\n\t * @param config 配置对象\n\t * @return 对象自身\n\t */\n\tpublic StpLogic setConfig(SaTokenConfig config) {\n\t\tthis.config = config;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 返回当前 StpLogic 使用的配置对象，如果当前 StpLogic 没有配置，则返回 null\n\t *\n\t * @return /\n\t */\n\tpublic SaTokenConfig getConfig() {\n\t\treturn config;\n\t}\n\n\t/**\n\t * 返回当前 StpLogic 使用的配置对象，如果当前 StpLogic 没有配置，则返回全局配置对象\n\t *\n\t * @return /\n\t */\n\tpublic SaTokenConfig getConfigOrGlobal() {\n\t\tSaTokenConfig cfg = getConfig();\n\t\tif(cfg != null) {\n\t\t\treturn cfg;\n\t\t}\n\t\treturn SaManager.getConfig();\n\t}\n\n\n\n\t// ------------------- 获取 token 相关 -------------------\n\n\t/**\n\t * 返回 token 名称，此名称在以下地方体现：Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀\n\t *\n\t * @return /\n\t */\n\tpublic String getTokenName() {\n\t\treturn splicingKeyTokenName();\n\t}\n\n\t/**\n\t * 为指定账号创建一个 token （只是把 token 创建出来，并不持久化存储）\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型\n\t * @param timeout 过期时间\n\t * @param extraData 扩展信息\n\t * @return 生成的tokenValue\n\t */\n\tpublic String createTokenValue(Object loginId, String deviceType, long timeout, Map<String, Object> extraData) {\n\t\treturn SaStrategy.instance.createToken.apply(loginId, loginType);\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t */\n\tpublic void setTokenValue(String tokenValue){\n\t\tsetTokenValue(tokenValue, createSaLoginParameter());\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param cookieTimeout Cookie存活时间(秒)\n\t */\n\tpublic void setTokenValue(String tokenValue, int cookieTimeout){\n\t\tsetTokenValue(tokenValue, createSaLoginParameter().setTimeout(cookieTimeout));\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param loginParameter 登录参数\n\t */\n\tpublic void setTokenValue(String tokenValue, SaLoginParameter loginParameter){\n\n\t\t// 先判断一下，如果提供 token 为空，则不执行任何动作\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 1、将 token 写入到当前请求的 Storage 存储器里\n\t\tsetTokenValueToStorage(tokenValue);\n\n\t\t// 2. 将 token 写入到当前会话的 Cookie 里\n\t\tif (getConfigOrGlobal().getIsReadCookie()) {\n\t\t\tsetTokenValueToCookie(tokenValue, loginParameter.getCookie(), loginParameter.getCookieTimeout());\n\t\t}\n\n\t\t// 3. 将 token 写入到当前请求的响应头中\n\t\tif(loginParameter.getIsWriteHeader()) {\n\t\t\tsetTokenValueToResponseHeader(tokenValue);\n\t\t}\n\t}\n\n\t/**\n\t * 将 token 写入到当前请求的 Storage 存储器里\n\t *\n\t * @param tokenValue 要保存的 token 值\n\t */\n\tpublic void setTokenValueToStorage(String tokenValue){\n\t\t// 1、获取当前请求的 Storage 存储器\n\t\tSaStorage storage = SaHolder.getStorage();\n\n\t\t// 2、保存 token\n\t\t//\t- 如果没有配置前缀模式，直接保存\n\t\t// \t- 如果配置了前缀模式，则拼接上前缀保存\n\t\tString tokenPrefix = getConfigOrGlobal().getTokenPrefix();\n\t\tif( SaFoxUtil.isEmpty(tokenPrefix) ) {\n\t\t\tstorage.set(splicingKeyJustCreatedSave(), tokenValue);\n\t\t} else {\n\t\t\tstorage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue);\n\t\t}\n\n\t\t// 3、以无前缀的方式再写入一次\n\t\tstorage.set(SaTokenConsts.JUST_CREATED_NOT_PREFIX, tokenValue);\n\t}\n\n\t/**\n\t * 将 token 写入到当前会话的 Cookie 里\n\t *\n\t * @param tokenValue token 值\n\t * @param cookieTimeout Cookie存活时间（单位：秒，填-1代表为内存Cookie，浏览器关闭后消失）\n\t */\n\tpublic void setTokenValueToCookie(String tokenValue, int cookieTimeout){\n\t\tsetTokenValueToCookie(tokenValue, null, cookieTimeout);\n\t}\n\n\t/**\n\t * 将 token 写入到当前会话的 Cookie 里\n\t *\n\t * @param tokenValue token 值\n\t * @param cookieConfig Cookie 配置项\n\t * @param cookieTimeout Cookie存活时间（单位：秒，填-1代表为内存Cookie，浏览器关闭后消失）\n\t */\n\tpublic void setTokenValueToCookie(String tokenValue, SaCookieConfig cookieConfig, int cookieTimeout){\n\t\tif(cookieConfig == null) {\n\t\t\tcookieConfig = getConfigOrGlobal().getCookie();\n\t\t}\n\t\tSaCookie cookie = new SaCookie()\n\t\t\t\t.setName(getTokenName())\n\t\t\t\t.setValue(tokenValue)\n\t\t\t\t.setMaxAge(cookieTimeout)\n\t\t\t\t.setDomain(cookieConfig.getDomain())\n\t\t\t\t.setPath(cookieConfig.getPath())\n\t\t\t\t.setSecure(cookieConfig.getSecure())\n\t\t\t\t.setHttpOnly(cookieConfig.getHttpOnly())\n\t\t\t\t.setSameSite(cookieConfig.getSameSite())\n\t\t\t\t.setExtraAttrs(cookieConfig.getExtraAttrs())\n\t\t\t\t;\n\t\tSaHolder.getResponse().addCookie(cookie);\n\t}\n\n\t/**\n\t * 将 token 写入到当前请求的响应头中\n\t *\n\t * @param tokenValue token 值\n\t */\n\tpublic void setTokenValueToResponseHeader(String tokenValue){\n\t\t// 写入到响应头\n\t\tString tokenName = getTokenName();\n\t\tSaResponse response = SaHolder.getResponse();\n\t\tresponse.setHeader(tokenName, tokenValue);\n\n\t\t// 此处必须在响应头里指定 Access-Control-Expose-Headers: token-name，否则前端无法读取到这个响应头\n\t\tresponse.addHeader(SaResponse.ACCESS_CONTROL_EXPOSE_HEADERS, tokenName);\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值\n\t *\n\t * @return 当前tokenValue\n\t */\n\tpublic String getTokenValue(){\n\t\treturn getTokenValue(false);\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值\n\t *\n\t * @param noPrefixThrowException 如果提交的 token 不带有指定的前缀，是否抛出异常\n\t * @return 当前tokenValue\n\t */\n\tpublic String getTokenValue(boolean noPrefixThrowException){\n\n\t\t// 1、获取前端提交的 token （包含前缀值）\n\t\tString tokenValue = getTokenValueNotCut();\n\n\t\t// 2、如果全局配置打开了前缀模式，则二次处理一下\n\t\tString tokenPrefix = getConfigOrGlobal().getTokenPrefix();\n\t\tif(SaFoxUtil.isNotEmpty(tokenPrefix)) {\n\n\t\t\t// 情况2.1：如果提交的 token 为空，则转为 null\n\t\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\t\ttokenValue = null;\n\t\t\t}\n\n\t\t\t// 情况2.2：如果 token 有值，但是并不是以指定的前缀开头\n\t\t\telse if(! tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT)) {\n\t\t\t\tif(noPrefixThrowException) {\n\t\t\t\t\tthrow NotLoginException.newInstance(loginType, NO_PREFIX, NO_PREFIX_MESSAGE + \"，prefix=\" + tokenPrefix, null).setCode(SaErrorCode.CODE_11017);\n\t\t\t\t} else {\n\t\t\t\t\ttokenValue = null;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 情况2.3：代码至此，说明 token 有值，且是以指定的前缀开头的，现在裁剪掉前缀\n\t\t\telse {\n\t\t\t\ttokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length());\n\t\t\t}\n\t\t}\n\n\t\t// 3、返回\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值 （不裁剪前缀）\n\t *\n\t * @return /\n\t */\n\tpublic String getTokenValueNotCut(){\n\n\t\t// 获取相应对象\n\t\tSaStorage storage = SaHolder.getStorage();\n\t\tSaRequest request = SaHolder.getRequest();\n\t\tSaTokenConfig config = getConfigOrGlobal();\n\t\tString keyTokenName = getTokenName();\n\t\tString tokenValue = null;\n\n\t\t// 1. 先尝试从 Storage 存储器里读取\n\t\tif(storage.get(splicingKeyJustCreatedSave()) != null) {\n\t\t\ttokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave()));\n\t\t}\n\t\t// 2. 再尝试从 请求体 里面读取\n\t\tif(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadBody()){\n\t\t\ttokenValue = request.getParam(keyTokenName);\n\t\t}\n\t\t// 3. 再尝试从 header 头里读取\n\t\tif(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadHeader()){\n\t\t\ttokenValue = request.getHeader(keyTokenName);\n\t\t}\n\t\t// 4. 最后尝试从 cookie 里读取\n\t\tif(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadCookie()){\n\t\t\ttokenValue = request.getCookieValue(keyTokenName);\n\t\t\tif(SaFoxUtil.isNotEmpty(tokenValue) && config.getCookieAutoFillPrefix()) {\n\t\t\t\ttokenValue = config.getTokenPrefix() + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue;\n\t\t\t}\n\t\t}\n\n\t\t// 5. 至此，不管有没有读取到，都不再尝试了，直接返回\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值，如果获取不到则抛出异常\n\t *\n\t * @return /\n\t */\n\tpublic String getTokenValueNotNull(){\n\t\tString tokenValue = getTokenValue(true);\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\tthrow NotLoginException.newInstance(loginType, NOT_TOKEN, NOT_TOKEN_MESSAGE, null).setCode(SaErrorCode.CODE_11001);\n\t\t}\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * 获取当前会话的 token 参数信息\n\t *\n\t * @return token 参数信息\n\t */\n\tpublic SaTokenInfo getTokenInfo() {\n\t\tSaTokenInfo info = new SaTokenInfo();\n\t\tinfo.tokenName = getTokenName();\n\t\tinfo.tokenValue = getTokenValue();\n\t\tinfo.isLogin = isLogin();\n\t\tinfo.loginId = getLoginIdDefaultNull();\n\t\tinfo.loginType = getLoginType();\n\t\tinfo.tokenTimeout = getTokenTimeout();\n\t\tinfo.sessionTimeout = getSessionTimeout();\n\t\tinfo.tokenSessionTimeout = getTokenSessionTimeout();\n\t\tinfo.tokenActiveTimeout = getTokenActiveTimeout();\n\t\tinfo.loginDeviceType = getLoginDeviceType();\n\t\treturn info;\n\t}\n\n\n\t// ------------------- 登录相关操作 -------------------\n\n\t// --- 登录\n\n\t/**\n\t * 会话登录\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t */\n\tpublic void login(Object id) {\n\t\tlogin(id, createSaLoginParameter());\n\t}\n\n\t/**\n\t * 会话登录，并指定登录设备类型\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param deviceType 设备类型\n\t */\n\tpublic void login(Object id, String deviceType) {\n\t\tlogin(id, createSaLoginParameter().setDeviceType(deviceType));\n\t}\n\n\t/**\n\t * 会话登录，并指定是否 [记住我]\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param isLastingCookie 是否为持久Cookie，值为 true 时记住我，值为 false 时关闭浏览器需要重新登录\n\t */\n\tpublic void login(Object id, boolean isLastingCookie) {\n\t\tlogin(id, createSaLoginParameter().setIsLastingCookie(isLastingCookie));\n\t}\n\n\t/**\n\t * 会话登录，并指定此次登录 token 的有效期, 单位:秒\n\t *\n\t * @param id      账号id，建议的类型：（long | int | String）\n\t * @param timeout 此次登录 token 的有效期, 单位:秒\n\t */\n\tpublic void login(Object id, long timeout) {\n\t\tlogin(id, createSaLoginParameter().setTimeout(timeout));\n\t}\n\n\t/**\n\t * 会话登录，并指定所有登录参数 Model\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model\n\t */\n\tpublic void login(Object id, SaLoginParameter loginParameter) {\n\t\t// 1、创建会话\n\t\tString token = createLoginSession(id, loginParameter);\n\n\t\t// 2、在当前客户端注入 token\n\t\tsetTokenValue(token, loginParameter);\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic String createLoginSession(Object id) {\n\t\treturn createLoginSession(id, createSaLoginParameter());\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model\n\t * @return 返回会话令牌\n\t */\n\tpublic String createLoginSession(Object id, SaLoginParameter loginParameter) {\n\n\t\t// 1、先检查一下，传入的参数是否有效\n\t\tcheckLoginArgs(id, loginParameter);\n\n\t\t// 2、给这个账号分配一个可用的 token\n\t\tString tokenValue = distUsableToken(id, loginParameter);\n\n\t\t// 3、获取此账号的 Account-Session , 续期\n\t\tSaSession session = getSessionByLoginId(id, true, loginParameter.getTimeout());\n\t\tsession.updateMinTimeout(loginParameter.getTimeout());\n\n\t\t// 4、在 Account-Session 上记录本次登录的终端信息\n\t\tSaTerminalInfo terminalInfo = new SaTerminalInfo()\n\t\t\t\t.setDeviceType(loginParameter.getDeviceType())\n\t\t\t\t.setDeviceId(loginParameter.getDeviceId())\n\t\t\t\t.setTokenValue(tokenValue)\n\t\t\t\t.setExtraData(loginParameter.getTerminalExtraData())\n\t\t\t\t.setCreateTime(System.currentTimeMillis());\n\t\tsession.addTerminal(terminalInfo);\n\n\t\t// 5、保存 token -> id 的映射关系，方便日后根据 token 找账号 id\n\t\tsaveTokenToIdMapping(tokenValue, id, loginParameter.getTimeout());\n\n\t\t// 6、写入这个 token 的最后活跃时间 token-last-active\n\t\tif(isOpenCheckActiveTimeout()) {\n\t\t\tsetLastActiveToNow(tokenValue, loginParameter.getActiveTimeout(), loginParameter.getTimeout());\n\t\t}\n\n\t\t// 7、如果该 token 对应的 Token-Session 已经存在，则需要给其续期\n\t\tSaSession tokenSession = getTokenSessionByToken(tokenValue, loginParameter.getRightNowCreateTokenSession());\n\t\tif(tokenSession != null) {\n\t\t\ttokenSession.updateMinTimeout(loginParameter.getTimeout());\n\t\t}\n\n\t\t// 8、$$ 发布全局事件：账号 xxx 登录成功\n\t\tSaTokenEventCenter.doLogin(loginType, id, tokenValue, loginParameter);\n\n\t\t// 9、检查此账号会话数量是否超出最大值，如果超过，则按照登录时间顺序，把最开始登录的给注销掉\n\t\tif(loginParameter.getMaxLoginCount() != -1) {\n\t\t\tlogoutByMaxLoginCount(id, session, null, loginParameter.getMaxLoginCount(), loginParameter.getOverflowLogoutMode());\n\t\t}\n\n\t\t// 10、一切处理完毕，返回会话凭证 token\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * 为指定账号 id 的登录操作，分配一个可用的 token\n\t *\n\t * @param id 账号id\n\t * @param loginParameter 此次登录的参数Model\n\t * @return 返回 token\n\t */\n\tprotected String distUsableToken(Object id, SaLoginParameter loginParameter) {\n\n\t\t// 1、获取全局配置的 isConcurrent 参数\n\t\tif( ! loginParameter.getIsConcurrent()) {\n\t\t\t//    如果配置为：不允许一个账号多地同时登录，则需要根据配置选择：\n\t\t\t//    \t一.将这个账号的历史登录会话标记为：被顶下线\n\t\t\t//    \t二.提示错误并拒绝本次登录\n\t\t\tif (loginParameter.getReplacedLoginExitMode() == SaReplacedLoginExitMode.OLD_DEVICE){\n\t\t\t\tif(loginParameter.getReplacedRange() == SaReplacedRange.CURR_DEVICE_TYPE) {\n\t\t\t\t\treplaced(id, loginParameter.getDeviceType());\n\t\t\t\t}\n\t\t\t\tif(loginParameter.getReplacedRange() == SaReplacedRange.ALL_DEVICE_TYPE) {\n\t\t\t\t\treplaced(id, createSaLogoutParameter());\n\t\t\t\t}\n\t\t\t} else if (loginParameter.getReplacedLoginExitMode() == SaReplacedLoginExitMode.NEW_DEVICE){\n\t\t\t\tList<SaTerminalInfo> terminalListByLoginId = getTerminalListByLoginId(id);\n                // 只有当存在有效地会话时才拒绝登录\n                boolean hasActiveSession = terminalListByLoginId.stream()\n                        .anyMatch(terminal -> isValidToken(terminal.getTokenValue()));\n                if (hasActiveSession) {\n                    throw new SaTokenException(\"登录失败：当前账号已在其它客户端登录\").setCode(SaErrorCode.CODE_11004);\n                }\n\t\t\t}\n\t\t}\n\n\t\t// 2、如果调用者预定了要生成的 token，则直接返回这个预定的值，框架无需再操心了\n\t\tif(SaFoxUtil.isNotEmpty(loginParameter.getToken())) {\n\t\t\treturn loginParameter.getToken();\n\t\t}\n\n\t\t// 3、只有在配置了 [ 允许一个账号多地同时登录 ] 时，才尝试复用旧 token，这样可以避免不必要地查询，节省开销\n\t\tif(loginParameter.getIsConcurrent()) {\n\n\t\t\t// 3.1、如果配置了允许复用旧 token\n\t\t\tif(isSupportShareToken() && loginParameter.getIsShare()) {\n\n\t\t\t\t// 根据 账号id + 设备类型，尝试获取旧的 token\n\t\t\t\tString tokenValue = getTokenValueByLoginId(id, loginParameter.getDeviceType());\n\n\t\t\t\t// 如果有值，那就直接复用\n\t\t\t\tif(SaFoxUtil.isNotEmpty(tokenValue)) {\n\t\t\t\t\treturn tokenValue;\n\t\t\t\t}\n\n\t\t\t\t// 如果没值，那还是要继续往下走，尝试新建 token\n\t\t\t\t// ↓↓↓\n\t\t\t}\n\t\t}\n\n\t\t// 4、如果代码走到此处，说明未能成功复用旧 token，需要根据算法新建 token\n\t\treturn SaStrategy.instance.generateUniqueToken.execute(\n\t\t\t\t\"token\",\n\t\t\t\tgetConfigOfMaxTryTimes(loginParameter),\n\t\t\t\t() -> {\n\t\t\t\t\treturn createTokenValue(id, loginParameter.getDeviceType(), loginParameter.getTimeout(), loginParameter.getExtraData());\n\t\t\t\t},\n\t\t\t\ttokenValue -> {\n\t\t\t\t\treturn getLoginIdNotHandle(tokenValue) == null;\n\t\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * 校验登录时的参数有效性，如果有问题会打印警告或抛出异常\n\t *\n\t * @param id 账号id\n\t * @param loginParameter 此次登录的参数Model\n\t */\n\tprotected void checkLoginArgs(Object id, SaLoginParameter loginParameter) {\n\n\t\t// 1、账号 id 不能为空\n\t\tif(SaFoxUtil.isEmpty(id)) {\n\t\t\tthrow new SaTokenException(\"loginId 不能为空\").setCode(SaErrorCode.CODE_11002);\n\t\t}\n\n\t\t// 2、账号 id 不能是异常标记值\n\t\tif(NotLoginException.ABNORMAL_LIST.contains(id.toString())) {\n\t\t\tthrow new SaTokenException(\"loginId 不能为以下值：\" + NotLoginException.ABNORMAL_LIST);\n\t\t}\n\n\t\t// 3、账号 id 不能是复杂类型\n\t\tif( ! SaFoxUtil.isBasicType(id.getClass())) {\n\t\t\tSaManager.log.warn(\"loginId 应该为简单类型，例如：String | int | long，不推荐使用复杂类型：\" + id.getClass());\n\t\t}\n\n\t\t// 4、判断当前 StpLogic 是否支持 extra 扩展参数\n\t\tif( ! isSupportExtra()) {\n\t\t\t// 如果不支持，开发者却传入了 extra 扩展参数，那么就打印警告信息\n\t\t\tif(loginParameter.haveExtraData()) {\n\t\t\t\tSaManager.log.warn(\"当前 StpLogic 不支持 extra 扩展参数模式，传入的 extra 参数将被忽略\");\n\t\t\t}\n\t\t}\n\n\t\t// 5、如果全局配置未启动动态 activeTimeout 功能，但是此次登录却传入了 activeTimeout 参数，那么就打印警告信息\n\t\tif( ! getConfigOrGlobal().getDynamicActiveTimeout() && loginParameter.getActiveTimeout() != null) {\n\t\t\tSaManager.log.warn(\"当前全局配置未开启动态 activeTimeout 功能，传入的 activeTimeout 参数将被忽略\");\n\t\t}\n\n\t}\n\n\t/**\n\t * 获取指定账号 id 的登录会话数据，如果获取不到则创建并返回\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic String getOrCreateLoginSession(Object id) {\n\t\tString tokenValue = getTokenValueByLoginId(id);\n\t\tif(tokenValue == null) {\n\t\t\ttokenValue = createLoginSession(id, createSaLoginParameter());\n\t\t}\n\t\treturn tokenValue;\n\t}\n\n\t// --- 注销 (根据 token)\n\n\t/**\n\t * 在当前客户端会话注销\n\t */\n\tpublic void logout() {\n\t\tlogout(createSaLogoutParameter());\n\t}\n\n\t/**\n\t * 在当前客户端会话注销，根据注销参数\n\t */\n\tpublic void logout(SaLogoutParameter logoutParameter) {\n\t\t// 1、如果本次请求连 Token 都没有提交，那么它本身也不属于登录状态，此时无需执行任何操作\n\t\tString tokenValue = getTokenValue();\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 2、如果打开了 Cookie 模式，则先把 Cookie 数据清除掉\n\t\tif(getConfigOrGlobal().getIsReadCookie()){\n\t\t\tSaCookieConfig cfg = getConfigOrGlobal().getCookie();\n\t\t\tSaCookie cookie = new SaCookie()\n\t\t\t\t\t.setName(getTokenName())\n\t\t\t\t\t.setValue(null)\n\t\t\t\t\t// 有效期指定为0，做到以增代删\n\t\t\t\t\t.setMaxAge(0)\n\t\t\t\t\t.setDomain(cfg.getDomain())\n\t\t\t\t\t.setPath(cfg.getPath())\n\t\t\t\t\t.setSecure(cfg.getSecure())\n\t\t\t\t\t.setHttpOnly(cfg.getHttpOnly())\n\t\t\t\t\t.setSameSite(cfg.getSameSite())\n\t\t\t\t\t;\n\t\t\tSaHolder.getResponse().addCookie(cookie);\n\t\t}\n\n\t\t// 3、然后从当前 Storage 存储器里删除 Token\n\t\tSaStorage storage = SaHolder.getStorage();\n\t\tstorage.delete(splicingKeyJustCreatedSave());\n\n\t\t// 4、清除当前上下文的 [ 活跃度校验 check 标记 ]\n\t\tstorage.delete(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY);\n\n\t\t// 5、清除这个 token 的其它相关信息\n\t\tif(logoutParameter.getRange() == SaLogoutRange.TOKEN) {\n\t\t\tlogoutByTokenValue(tokenValue, logoutParameter);\n\t\t} else {\n\t\t\tObject loginId = getLoginIdByTokenNotThinkFreeze(tokenValue);\n\t\t\tif(loginId != null) {\n\t\t\t\tif(!logoutParameter.getIsKeepFreezeOps() && isFreeze(tokenValue)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlogout(loginId, logoutParameter);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic void logoutByTokenValue(String tokenValue) {\n\t\tlogoutByTokenValue(tokenValue, createSaLogoutParameter());\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token、注销参数\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\t_logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.LOGOUT));\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic void kickoutByTokenValue(String tokenValue) {\n\t\tkickoutByTokenValue(tokenValue, createSaLogoutParameter());\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\t_logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.KICKOUT));\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic void replacedByTokenValue(String tokenValue) {\n\t\treplacedByTokenValue(tokenValue, createSaLogoutParameter());\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\t_logoutByTokenValue(tokenValue, logoutParameter.setMode(SaLogoutMode.REPLACED));\n\t}\n\n\t/**\n\t * [work] 注销下线，根据指定 token 、注销参数\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void _logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\n\t\t// 1、判断一下：如果此 token 映射的是一个无效 loginId，则此处立即返回，不需要再往下处理了\n\t\t// \t\t如果不提前截止，则后续的操作可能会写入意外数据\n\t\tObject loginId = getLoginIdByTokenNotThinkFreeze(tokenValue);\n\t\tif( SaFoxUtil.isEmpty(loginId) ) {\n\t\t\treturn;\n\t\t}\n\t\tif(!logoutParameter.getIsKeepFreezeOps() && isFreeze(tokenValue)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 2、清除这个 token 的最后活跃时间记录\n\t\tif(isOpenCheckActiveTimeout()) {\n\t\t\tclearLastActive(tokenValue);\n\t\t}\n\n\t\t// 3、清除 Token-Session\n\t\tif( ! logoutParameter.getIsKeepTokenSession()) {\n\t\t\tdeleteTokenSession(tokenValue);\n\t\t}\n\n\t\t// 4、清理或更改 Token 映射\n\t\t// 5、发布事件通知\n\t\t// \t\tSaLogoutMode.LOGOUT：注销下线\n\t\tif(logoutParameter.getMode() == SaLogoutMode.LOGOUT) {\n\t\t\tdeleteTokenToIdMapping(tokenValue);\n\t\t\tSaTokenEventCenter.doLogout(loginType, loginId, tokenValue);\n\t\t}\n\t\t// \t\tSaLogoutMode.LOGOUT：踢人下线\n\t\tif(logoutParameter.getMode() == SaLogoutMode.KICKOUT) {\n\t\t\tupdateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);\n\t\t\tSaTokenEventCenter.doKickout(loginType, loginId, tokenValue);\n\t\t}\n\t\t//\t\tSaLogoutMode.REPLACED：顶人下线\n\t\tif(logoutParameter.getMode() == SaLogoutMode.REPLACED) {\n\t\t\tupdateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);\n\t\t\tSaTokenEventCenter.doReplaced(loginType, loginId, tokenValue);\n\t\t}\n\n\t\t// 6、清理这个账号的 Account-Session 上的 terminal 信息，并且尝试注销掉 Account-Session\n\t\tSaSession session = getSessionByLoginId(loginId, false);\n\t\tif(session != null) {\n\t\t\tsession.removeTerminal(tokenValue);\n\t\t\tsession.logoutByTerminalCountToZero();\n\t\t}\n\t}\n\n\t// --- 注销 (根据 loginId)\n\n\t/**\n\t * 会话注销，根据账号id\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic void logout(Object loginId) {\n\t\tlogout(loginId, createSaLogoutParameter());\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 设备类型\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型)\n\t */\n\tpublic void logout(Object loginId, String deviceType) {\n\t\tlogout(loginId, createSaLogoutParameter().setDeviceType(deviceType));\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 注销参数\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void logout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\t_logout(loginId, logoutParameter.setMode(SaLogoutMode.LOGOUT));\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic void kickout(Object loginId) {\n\t\tkickout(loginId, createSaLogoutParameter());\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型)\n\t */\n\tpublic void kickout(Object loginId, String deviceType) {\n\t\tkickout(loginId, createSaLogoutParameter().setDeviceType(deviceType));\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void kickout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\t_logout(loginId, logoutParameter.setMode(SaLogoutMode.KICKOUT));\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic void replaced(Object loginId) {\n\t\treplaced(loginId, createSaLogoutParameter());\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 （填 null 代表顶替该账号的所有设备类型）\n\t */\n\tpublic void replaced(Object loginId, String deviceType) {\n\t\treplaced(loginId, createSaLogoutParameter().setDeviceType(deviceType));\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void replaced(Object loginId, SaLogoutParameter logoutParameter) {\n\t\t_logout(loginId, logoutParameter.setMode(SaLogoutMode.REPLACED));\n\t}\n\n\t/**\n\t * [work] 会话注销，根据账号id 和 注销参数\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void _logout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\t// 1、获取此账号的 Account-Session，上面记录了此账号的所有登录客户端数据\n\t\tSaSession session = getSessionByLoginId(loginId, false);\n\t\tif(session != null) {\n\n\t\t\t// 2、遍历此 SaTerminalInfo 客户端列表，清除相关数据\n\t\t\tList<SaTerminalInfo> terminalList = session.terminalListCopy();\n\t\t\tfor (SaTerminalInfo terminal: terminalList) {\n\t\t\t\t// 不符合 deviceType 的跳过\n\t\t\t\tif( ! SaFoxUtil.isEmpty(logoutParameter.getDeviceType()) && ! logoutParameter.getDeviceType().equals(terminal.getDeviceType())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// 不符合 deviceId 的跳过\n\t\t\t\tif( ! SaFoxUtil.isEmpty(logoutParameter.getDeviceId()) && ! logoutParameter.getDeviceId().equals(terminal.getDeviceId())) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t_removeTerminal(session, terminal, logoutParameter);\n\t\t\t}\n\n\t\t\t// 3、如果代码走到这里的时候，此账号已经没有客户端在登录了，则直接注销掉这个 Account-Session\n\t\t\tif(logoutParameter.getMode() == SaLogoutMode.REPLACED) {\n\t\t\t\t// 因为调用顶替下线时，一般都是在新客户端正在登录，所以此种情况不需要清除该账号的 Account-Session\n\t\t\t\t// 如果清除了 Account-Session，将可能导致 Account-Session 被注销后又立刻创建出来，造成不必要的性能浪费\n\t\t\t} else {\n\t\t\t\tsession.logoutByTerminalCountToZero();\n\t\t\t}\n\t\t}\n\t}\n\n\t// --- 注销 (会话管理辅助方法)\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (注销下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) {\n\t\t_removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.LOGOUT));\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) {\n\t\t_removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.KICKOUT));\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) {\n\t\t_removeTerminal(session, terminal, createSaLogoutParameter().setMode(SaLogoutMode.REPLACED));\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (内部方法，仅为减少重复代码，外部调用意义不大)\n\t * @param session Account-Session\n\t * @param terminal 设备信息\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void _removeTerminal(SaSession session, SaTerminalInfo terminal, SaLogoutParameter logoutParameter) {\n\n\t\tObject loginId = session.getLoginId();\n\t\tString tokenValue = terminal.getTokenValue();\n\n\t\t// 1、从 Account-Session 上清除此设备信息\n\t\tsession.removeTerminal(tokenValue);\n\n\t\t// 2、清除这个 token 的最后活跃时间记录\n\t\tif(isOpenCheckActiveTimeout()) {\n\t\t\tclearLastActive(tokenValue);\n\t\t}\n\n\t\t// 3、清除这个 token 的 Token-Session 对象\n\t\tif( ! logoutParameter.getIsKeepTokenSession()) {\n\t\t\tdeleteTokenSession(tokenValue);\n\t\t}\n\n\t\t// 4、清理或更改 Token 映射\n\t\t// 5、发布事件通知\n\t\t// \t\tSaLogoutMode.LOGOUT：注销下线\n\t\tif(logoutParameter.getMode() == SaLogoutMode.LOGOUT) {\n\t\t\tdeleteTokenToIdMapping(tokenValue);\n\t\t\tSaTokenEventCenter.doLogout(loginType, loginId, tokenValue);\n\t\t}\n\t\t// \t\tSaLogoutMode.LOGOUT：踢人下线\n\t\tif(logoutParameter.getMode() == SaLogoutMode.KICKOUT) {\n\t\t\tupdateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);\n\t\t\tSaTokenEventCenter.doKickout(loginType, loginId, tokenValue);\n\t\t}\n\t\t//\t\tSaLogoutMode.REPLACED：顶人下线\n\t\tif(logoutParameter.getMode() == SaLogoutMode.REPLACED) {\n\t\t\tupdateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);\n\t\t\tSaTokenEventCenter.doReplaced(loginType, loginId, tokenValue);\n\t\t}\n\t}\n\n\t/**\n\t * 如果指定账号 id、设备类型的登录客户端已经超过了指定数量，则按照登录时间顺序，把最开始登录的给注销掉\n\t *\n\t * @param loginId 账号id\n\t * @param session 此账号的 Account-Session 对象，可填写 null，框架将自动获取\n\t * @param deviceType 设备类型（填 null 代表注销此账号所有设备类型的登录）\n\t * @param maxLoginCount 最大登录数量，超过此数量的将被注销\n\t * @param logoutMode 超出的客户端将以何种方式被注销\n\t */\n\tpublic void logoutByMaxLoginCount(Object loginId, SaSession session, String deviceType, int maxLoginCount, SaLogoutMode logoutMode) {\n\n\t\t// 1、如果调用者提供的  Account-Session 对象为空，则我们先手动获取一下\n\t\tif(session == null) {\n\t\t\tsession = getSessionByLoginId(loginId, false);\n\t\t\tif(session == null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// 2、获取这个账号指定设备类型下的所有登录客户端\n\t\tList<SaTerminalInfo> list = session.getTerminalListByDeviceType(deviceType);\n\n\t\t// 3、按照登录时间倒叙，超过 maxLoginCount 数量的，全部注销掉\n\t\tfor (int i = 0; i < list.size() - maxLoginCount; i++) {\n\t\t\t_removeTerminal(session, list.get(i), createSaLogoutParameter().setMode(logoutMode));\n\t\t}\n\n\t\t// 4、如果代码走到这里的时候，此账号已经没有客户端在登录了，则直接注销掉这个 Account-Session\n\t\tsession.logoutByTerminalCountToZero();\n\t}\n\n\n\t// ---- 会话查询\n\n\t/**\n\t * 判断当前会话是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic boolean isLogin() {\n\t\t// 判断条件：\n\t\t// \t\t1、获取到的 loginId 不为 null，\n\t\t// \t\t2、并且不在异常项集合里（此项在 getLoginIdDefaultNull() 方法里完成判断）\n\t\treturn getLoginIdDefaultNull() != null;\n\t}\n\n\t/**\n\t * 判断指定账号是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic boolean isLogin(Object loginId) {\n\t\t// 判断条件：能否根据 loginId 查询到对应的 terminal 值\n\t\treturn !getTerminalListByLoginId(loginId, null).isEmpty();\n\t}\n\n\t/**\n\t * 检验当前会话是否已经登录，如未登录，则抛出异常\n\t */\n\tpublic void checkLogin() {\n\t\t// 效果与 getLoginId() 相同，只是 checkLogin() 更加语义化一些\n\t\tgetLoginId();\n\t}\n\n\t/**\n\t * 获取当前会话账号id，如果未登录，则抛出异常\n\t *\n\t * @return 账号id\n\t */\n\tpublic Object getLoginId() {\n\n\t\t// 1、先判断一下当前会话是否正在 [ 临时身份切换 ], 如果是则返回临时身份\n\t\tif(isSwitch()) {\n\t\t\treturn getSwitchLoginId();\n\t\t}\n\n\t\t// 2、如果前端没有提交 token，则抛出异常: 未能读取到有效 token\n\t\tString tokenValue = getTokenValue(true);\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\tthrow NotLoginException.newInstance(loginType, NOT_TOKEN, NOT_TOKEN_MESSAGE, null).setCode(SaErrorCode.CODE_11011);\n\t\t}\n\n\t\t// 3、查找此 token 对应的 loginId，如果找不到则抛出：token 无效\n\t\tString loginId = getLoginIdNotHandle(tokenValue);\n\t\tif(SaFoxUtil.isEmpty(loginId)) {\n\t\t\tthrow NotLoginException.newInstance(loginType, INVALID_TOKEN, INVALID_TOKEN_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11012);\n\t\t}\n\n\t\t// 4、如果这个 token 指向的是值是：过期标记，则抛出：token 已过期\n\t\tif(loginId.equals(NotLoginException.TOKEN_TIMEOUT)) {\n\t\t\tthrow NotLoginException.newInstance(loginType, TOKEN_TIMEOUT, TOKEN_TIMEOUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11013);\n\t\t}\n\n\t\t// 5、如果这个 token 指向的是值是：被顶替标记，则抛出：token 已被顶下线\n\t\tif(loginId.equals(NotLoginException.BE_REPLACED)) {\n\t\t\tthrow NotLoginException.newInstance(loginType, BE_REPLACED, BE_REPLACED_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11014);\n\t\t}\n\n\t\t// 6、如果这个 token 指向的是值是：被踢下线标记，则抛出：token 已被踢下线\n\t\tif(loginId.equals(NotLoginException.KICK_OUT)) {\n\t\t\tthrow NotLoginException.newInstance(loginType, KICK_OUT, KICK_OUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11015);\n\t\t}\n\n\t\t// 7、token 活跃频率检查\n\t\tcheckActiveTimeoutByConfig(tokenValue);\n\n\t\t// ------ 至此，loginId 已经是一个合法的值，代表当前会话是一个正常的登录状态了\n\n\t\t// 8、返回 loginId\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回默认值\n\t *\n\t * @param <T> 返回类型\n\t * @param defaultValue 默认值\n\t * @return 登录id\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getLoginId(T defaultValue) {\n\t\t// 1、先正常获取一下当前会话的 loginId\n\t\tObject loginId = getLoginIdDefaultNull();\n\n\t\t// 2、如果 loginId 为 null，则返回默认值\n\t\tif(loginId == null) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\t// 3、loginId 不为 null，则开始尝试类型转换\n\t\tif(defaultValue == null) {\n\t\t\treturn (T) loginId;\n\t\t}\n\t\treturn (T) SaFoxUtil.getValueByType(loginId, defaultValue.getClass());\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回null\n\t *\n\t * @return 账号id\n\t */\n\tpublic Object getLoginIdDefaultNull() {\n\n\t\t// 1、先判断一下当前会话是否正在 [ 临时身份切换 ], 如果是则返回临时身份\n\t\tif(isSwitch()) {\n\t\t\treturn getSwitchLoginId();\n\t\t}\n\n\t\t// 2、如果前端连 token 都没有提交，则直接返回 null\n\t\tString tokenValue = getTokenValue();\n\t\tif(tokenValue == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 3、根据 token 找到对应的 loginId，如果 loginId 为 null 或者属于异常标记里面，均视为未登录, 统一返回 null\n\t\tObject loginId = getLoginIdNotHandle(tokenValue);\n\t\tif( ! isValidLoginId(loginId) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 4、如果 token 已被冻结，也返回 null\n\t\tif(getTokenActiveTimeoutByToken(tokenValue) == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 5、执行到此，证明此 loginId 已经是个正常合法的账号id了，可以返回\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 String 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic String getLoginIdAsString() {\n\t\treturn String.valueOf(getLoginId());\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 int 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic int getLoginIdAsInt() {\n\t\treturn Integer.parseInt(String.valueOf(getLoginId()));\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 long 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic long getLoginIdAsLong() {\n\t\treturn Long.parseLong(String.valueOf(getLoginId()));\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶、被冻结等状态，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n\tpublic Object getLoginIdByToken(String tokenValue) {\n\n\t\tObject loginId = getLoginIdByTokenNotThinkFreeze(tokenValue);\n\n\t\tif( SaFoxUtil.isNotEmpty(loginId) ) {\n\t\t\t// 如果 token 已被冻结，也返回 null\n\t\t\tlong activeTimeout = getTokenActiveTimeoutByToken(tokenValue);\n\t\t\tif(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结)，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n\tpublic Object getLoginIdByTokenNotThinkFreeze(String tokenValue) {\n\n\t\t// 1、如果提供的 token 为空，则直接返回 null\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 2、查找此 token 对应的 loginId，如果找不到或找的到但属于无效值，则返回 null\n\t\tString loginId = getLoginIdNotHandle(tokenValue);\n\t\tif( ! isValidLoginId(loginId) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 3、返回\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id （不做任何特殊处理）\n\t *\n\t * @param tokenValue token 值\n\t * @return 账号id\n\t */\n\tpublic String getLoginIdNotHandle(String tokenValue) {\n\t\treturn getSaTokenDao().get(splicingKeyTokenValue(tokenValue));\n\t}\n\n\t/**\n\t * 获取当前 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param key 键值\n\t * @return 对应的扩展数据\n\t */\n\tpublic Object getExtra(String key) {\n\t\tthrow new ApiDisabledException(\"只有在集成 sa-token-jwt 插件后才可以使用 extra 扩展参数\").setCode(SaErrorCode.CODE_11031);\n\t}\n\n\t/**\n\t * 获取指定 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param tokenValue 指定的 Token 值\n\t * @param key 键值\n\t * @return 对应的扩展数据\n\t */\n\tpublic Object getExtra(String tokenValue, String key) {\n\t\tthrow new ApiDisabledException(\"只有在集成 sa-token-jwt 插件后才可以使用 extra 扩展参数\").setCode(SaErrorCode.CODE_11031);\n\t}\n\n\t// ---- 其它操作\n\n\t/**\n\t * 判断一个 loginId 是否是有效的 (判断标准：不为 null、空字符串，且不在异常标记项里面)\n\t *\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic boolean isValidLoginId(Object loginId) {\n\t\treturn SaFoxUtil.isNotEmpty(loginId) && !NotLoginException.ABNORMAL_LIST.contains(loginId.toString());\n\t}\n\n\t/**\n\t * 判断一个 token 是否是有效的 (判断标准：使用此 token 查询到的 loginId 不为 Empty )\n\t *\n\t * @param tokenValue /\n\t * @return /\n\t */\n\tpublic boolean isValidToken(String tokenValue) {\n\t\tObject loginId = getLoginIdByToken(tokenValue);\n\t\treturn SaFoxUtil.isNotEmpty(loginId);\n\t}\n\n\t/**\n\t * 存储 token - id 映射关系\n\t *\n\t * @param tokenValue token值\n\t * @param loginId 账号id\n\t * @param timeout 会话有效期 (单位: 秒)\n\t */\n\tpublic void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) {\n\t\tgetSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout);\n\t}\n\n\t/**\n\t * 更改 token - id 映射关系\n\t *\n\t * @param tokenValue token值\n\t * @param loginId 新的账号Id值\n\t */\n\tpublic void updateTokenToIdMapping(String tokenValue, Object loginId) {\n\t\t// 先判断一下，是否传入了空值\n\t\tSaTokenException.notTrue(SaFoxUtil.isEmpty(loginId), \"loginId 不能为空\", SaErrorCode.CODE_11003);\n\n\t\t// 更新缓存中的 token 指向\n\t\tgetSaTokenDao().update(splicingKeyTokenValue(tokenValue), loginId.toString());\n\t}\n\n\t/**\n\t * 删除 token - id 映射\n\t *\n\t * @param tokenValue token值\n\t */\n\tpublic void deleteTokenToIdMapping(String tokenValue) {\n\t\tgetSaTokenDao().delete(splicingKeyTokenValue(tokenValue));\n\t}\n\n\n\t// ------------------- Account-Session 相关 -------------------\n\n\t/**\n\t * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建，isCreate = 是否立即新建并返回\n\t *\n\t * @param sessionId SessionId\n\t * @param isCreate 是否新建\n\t * @param timeout 如果这个 SaSession 是新建的，则使用此值作为过期值（单位：秒），可填 null，代表使用全局 timeout 值\n\t * @param appendOperation 如果这个 SaSession 是新建的，则要追加执行的动作，可填 null，代表无追加动作\n\t * @return Session对象\n\t */\n\tpublic SaSession getSessionBySessionId(String sessionId, boolean isCreate, Long timeout, Consumer<SaSession> appendOperation) {\n\n\t\t// 如果提供的 sessionId 为 null，则直接返回 null\n\t\tif(SaFoxUtil.isEmpty(sessionId)) {\n\t\t\tthrow new SaTokenException(\"SessionId 不能为空\").setCode(SaErrorCode.CODE_11072);\n\t\t}\n\n\t\t// 先检查这个 SaSession 是否已经存在，如果不存在且 isCreate=true，则新建并返回\n\t\tSaSession session = getSaTokenDao().getSession(sessionId);\n\n\t\tif(session == null && isCreate) {\n\t\t\t// 创建这个 SaSession\n\t\t\tsession = SaStrategy.instance.createSession.apply(sessionId);\n\n\t\t\t// 追加操作\n\t\t\tif(appendOperation != null) {\n\t\t\t\tappendOperation.accept(session);\n\t\t\t}\n\n\t\t\t// 如果未提供 timeout，则根据相应规则设定默认的 timeout\n\t\t\tif(timeout == null) {\n\t\t\t\t// 如果是 Token-Session，则使用对用 token 的有效期，使 token 和 token-session 保持相同ttl，同步失效\n\t\t\t\tif(SaTokenConsts.SESSION_TYPE__TOKEN.equals(session.getType())) {\n\t\t\t\t\ttimeout = getTokenTimeout(session.getToken());\n\t\t\t\t\tif(timeout == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\t\t\t\ttimeout = getConfigOrGlobal().getTimeout();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// 否则使用全局配置的 timeout\n\t\t\t\t\ttimeout = getConfigOrGlobal().getTimeout();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 将这个 SaSession 入库\n\t\t\tgetSaTokenDao().setSession(session, timeout);\n\t\t}\n\t\treturn session;\n\t}\n\n\t/**\n\t * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建，则返回 null\n\t *\n\t * @param sessionId SessionId\n\t * @return Session对象\n\t */\n\tpublic SaSession getSessionBySessionId(String sessionId) {\n\t\treturn getSessionBySessionId(sessionId, false, null, null);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @param isCreate 是否新建\n\t * @param timeout 如果这个 SaSession 是新建的，则使用此值作为过期值（单位：秒），可填 null，代表使用全局 timeout 值\n\t * @return SaSession 对象\n\t */\n\tpublic SaSession getSessionByLoginId(Object loginId, boolean isCreate, Long timeout) {\n\t\tif(SaFoxUtil.isEmpty(loginId)) {\n\t\t\tthrow new SaTokenException(\"Account-Session 获取失败：loginId 不能为空\");\n\t\t}\n\t\treturn getSessionBySessionId(splicingKeySession(loginId), isCreate, timeout, session -> {\n\t\t\t// 这里是该 Account-Session 首次创建时才会被执行的方法：\n\t\t\t// \t\t设定这个 SaSession 的各种基础信息：类型、账号体系、账号id\n\t\t\tsession.setType(SaTokenConsts.SESSION_TYPE__ACCOUNT);\n\t\t\tsession.setLoginType(getLoginType());\n\t\t\tsession.setLoginId(loginId);\n\t\t});\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @param isCreate 是否新建\n\t * @return SaSession 对象\n\t */\n\tpublic SaSession getSessionByLoginId(Object loginId, boolean isCreate) {\n\t\treturn getSessionByLoginId(loginId, isCreate, null);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @return SaSession 对象\n\t */\n\tpublic SaSession getSessionByLoginId(Object loginId) {\n\t\treturn getSessionByLoginId(loginId, true, null);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param isCreate 是否新建\n\t * @return Session对象\n\t */\n\tpublic SaSession getSession(boolean isCreate) {\n\t\treturn getSessionByLoginId(getLoginId(), isCreate);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic SaSession getSession() {\n\t\treturn getSession(true);\n\t}\n\n\n\t// ------------------- Token-Session 相关 -------------------\n\n\t/**\n\t * 获取指定 token 的 Token-Session，如果该 SaSession 尚未创建，isCreate代表是否新建并返回\n\t *\n\t * @param tokenValue token值\n\t * @param isCreate 是否新建\n\t * @return session对象\n\t */\n\tpublic SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) {\n\t\t// 1、token 为空，不允许创建\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\tthrow new SaTokenException(\"Token-Session 获取失败：token 为空\").setCode(SaErrorCode.CODE_11073);\n\t\t}\n\n\t\t// 2、如果能查询到旧记录，则直接返回\n\t\tString sessionId = splicingKeyTokenSession(tokenValue);\n\t\tSaSession tokenSession = getSaTokenDao().getSession(sessionId);\n\t\tif(tokenSession != null) {\n\t\t\treturn tokenSession;\n\t\t}\n\t\t// 以下是查不到的情况\n\n\t\t// 3、指定了不需要创建，返回 null\n\t\tif( ! isCreate) {\n\t\t\treturn null;\n\t\t}\n\t\t// 以下是需要创建的情况\n\n\t\t// 4、检查一下这个 token 是否为有效 token，无效 token 不允许创建\n\t\tif(getConfigOrGlobal().getTokenSessionCheckLogin() && ! isValidToken(tokenValue)) {\n\t\t\tthrow new SaTokenException(\"Token-Session 获取失败，token 无效: \" + tokenValue).setCode(SaErrorCode.CODE_11074);\n\t\t}\n\n\t\t// 5、创建 Token-Session 并返回\n\t\treturn getSessionBySessionId(sessionId, true, null, session -> {\n\t\t\t// 这里是该 Token-Session 首次创建时才会被执行的方法：\n\t\t\t// \t\t设定这个 SaSession 的各种基础信息：类型、账号体系、Token 值\n\t\t\tsession.setType(SaTokenConsts.SESSION_TYPE__TOKEN);\n\t\t\tsession.setLoginType(getLoginType());\n\t\t\tsession.setToken(tokenValue);\n\t\t});\n\t}\n\n\t/**\n\t * 获取指定 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param tokenValue Token值\n\t * @return Session对象\n\t */\n\tpublic SaSession getTokenSessionByToken(String tokenValue) {\n\t\treturn getTokenSessionByToken(tokenValue, true);\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session，如果该 SaSession 尚未创建，isCreate代表是否新建并返回\n\t *\n\t * @param isCreate 是否新建\n\t * @return Session对象\n\t */\n\tpublic SaSession getTokenSession(boolean isCreate) {\n\t\tString tokenValue = getTokenValue();\n\t\tcheckActiveTimeoutByConfig(tokenValue);\n\t\treturn getTokenSessionByToken(tokenValue, isCreate);\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic SaSession getTokenSession() {\n\t\treturn getTokenSession(true);\n\t}\n\n\t/**\n\t * 获取当前匿名 Token-Session （可在未登录情况下使用的 Token-Session）\n\t *\n\t * @param isCreate 在 Token-Session 尚未创建的情况是否新建并返回\n\t * @return Token-Session 对象\n\t */\n\tpublic SaSession getAnonTokenSession(boolean isCreate) {\n\t\t/*\n\t\t * 情况1、如果调用方提供了有效 Token，则：直接返回其 [Token-Session]\n\t\t * 情况2、如果调用方提供了无效 Token，或根本没有提供 Token，则：创建新 Token -> 返回 [ Token-Session ]\n\t\t */\n\t\tString tokenValue = getTokenValue();\n\n\t\t// q1 —— 判断这个 Token 是否有效，两个条件符合其一即可：\n\t\t/*\n\t\t * 条件1、能查出 Token-Session\n\t\t * 条件2、能查出 LoginId\n\t\t */\n\t\tif(SaFoxUtil.isNotEmpty(tokenValue)) {\n\n\t\t\t// 符合条件1\n\t\t\tSaSession session = getTokenSessionByToken(tokenValue, false);\n\t\t\tif(session != null) {\n\t\t\t\treturn session;\n\t\t\t}\n\n\t\t\t// 符合条件2\n\t\t\tString loginId = getLoginIdNotHandle(tokenValue);\n\t\t\tif(isValidLoginId(loginId)) {\n\t\t\t\treturn getTokenSessionByToken(tokenValue, isCreate);\n\t\t\t}\n\t\t}\n\n\t\t// q2 —— 此时q2分两种情况：\n\t\t/*\n\t\t * 情况 2.1、isCreate=true：说明调用方想让框架帮其创建一个 SaSession，那框架就创建并返回\n\t\t * 情况 2.2、isCreate=false：说明调用方并不想让框架帮其创建一个 SaSession，那框架就直接返回 null\n\t\t */\n\t\tif(isCreate) {\n\t\t\t// 随机创建一个 Token\n\t\t\ttokenValue = SaStrategy.instance.generateUniqueToken.execute(\n\t\t\t\t\t\"token\",\n\t\t\t\t\tgetConfigOfMaxTryTimes(createSaLoginParameter()),\n\t\t\t\t\t() -> {\n\t\t\t\t\t\treturn createTokenValue(null, null, getConfigOrGlobal().getTimeout(), null);\n\t\t\t\t\t},\n\t\t\t\t\ttoken -> {\n\t\t\t\t\t\treturn getTokenSessionByToken(token, false) == null;\n\t\t\t\t\t}\n\t\t\t);\n\n\t\t\t// 写入此 token 的最后活跃时间\n\t\t\tif(isOpenCheckActiveTimeout()) {\n\t\t\t\tsetLastActiveToNow(tokenValue, null, null);\n\t\t\t}\n\n\t\t\t// 在当前上下文写入此 TokenValue\n\t\t\tsetTokenValue(tokenValue);\n\n\t\t\t// 返回其 Token-Session 对象\n\t\t\tfinal String finalTokenValue = tokenValue;\n\t\t\treturn getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, getConfigOrGlobal().getTimeout(), session -> {\n\t\t\t\t// 这里是该 Anon-Token-Session 首次创建时才会被执行的方法：\n\t\t\t\t// \t\t设定这个 SaSession 的各种基础信息：类型、账号体系、Token 值\n\t\t\t\tsession.setType(SaTokenConsts.SESSION_TYPE__ANON);\n\t\t\t\tsession.setLoginType(getLoginType());\n\t\t\t\tsession.setToken(finalTokenValue);\n\t\t\t});\n\t\t}\n\t\telse {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 获取当前匿名 Token-Session （可在未登录情况下使用的Token-Session）\n\t *\n\t * @return Token-Session 对象\n\t */\n\tpublic SaSession getAnonTokenSession() {\n\t\treturn getAnonTokenSession(true);\n\t}\n\n\t/**\n\t * 删除指定 token 的 Token-Session\n\t *\n\t * @param tokenValue token值\n\t */\n\tpublic void deleteTokenSession(String tokenValue) {\n\t\tgetSaTokenDao().delete(splicingKeyTokenSession(tokenValue));\n\t}\n\n\n\t// ------------------- Active-Timeout token 最低活跃度 验证相关 -------------------\n\n\t/**\n\t * 写入指定 token 的 [ 最后活跃时间 ] 为当前时间戳 √√√\n\t *\n\t * @param tokenValue 指定token\n\t * @param activeTimeout 这个 token 的最低活跃频率，单位：秒，填 null 代表使用全局配置的 activeTimeout 值\n\t * @param timeout 保存数据时使用的 ttl 值，单位：秒，填 null 代表使用全局配置的 timeout 值\n\t */\n\tprotected void setLastActiveToNow(String tokenValue, Long activeTimeout, Long timeout) {\n\n\t\t// 如果提供的 timeout 为null，则使用全局配置的 timeout 值\n\t\tSaTokenConfig config = getConfigOrGlobal();\n\t\tif(timeout == null) {\n\t\t\ttimeout = config.getTimeout();\n\t\t}\n\t\t// activeTimeout 变量无需赋值默认值，因为当缓存中没有这个值时，会自动使用全局配置的值\n\n\t\t// 将此 token 的 [ 最后活跃时间 ] 标记为当前时间戳\n\t\tString key = splicingKeyLastActiveTime(tokenValue);\n\t\tString value = String.valueOf(System.currentTimeMillis());\n\t\tif(config.getDynamicActiveTimeout() && activeTimeout != null) {\n\t\t\tvalue += \",\" + activeTimeout;\n\t\t}\n\t\tgetSaTokenDao().set(key, value, timeout);\n\t}\n\n\t/**\n\t * 续签指定 token：将这个 token 的 [ 最后活跃时间 ] 更新为当前时间戳\n\t *\n\t * @param tokenValue 指定token\n\t */\n\tpublic void updateLastActiveToNow(String tokenValue) {\n\t\tString key = splicingKeyLastActiveTime(tokenValue);\n\t\tString value = new SaValue2Box(System.currentTimeMillis(), getTokenUseActiveTimeout(tokenValue)).toString();\n\t\tgetSaTokenDao().update(key, value);\n\t}\n\n\t/**\n\t * 续签当前 token：(将 [最后操作时间] 更新为当前时间戳)\n\t * <h2>\n\t * \t\t请注意: 即使 token 已被冻结 也可续签成功，\n\t * \t\t如果此场景下需要提示续签失败，可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可\n\t * </h2>\n\t */\n\tpublic void updateLastActiveToNow() {\n\t\tupdateLastActiveToNow(getTokenValue());\n\t}\n\n\t/**\n\t * 清除指定 Token 的 [ 最后活跃时间记录 ]\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tprotected void clearLastActive(String tokenValue) {\n\t\tgetSaTokenDao().delete(splicingKeyLastActiveTime(tokenValue));\n\t}\n\n\t/**\n\t * 判断指定 token 是否已被冻结\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic boolean isFreeze(String tokenValue) {\n\n\t\t// 1、获取这个 token 的剩余活跃有效期\n\t\tlong activeTimeout = getTokenActiveTimeoutByToken(tokenValue);\n\n\t\t// 2、值为 -1 代表此 token 已经被设置永不冻结\n\t\tif(activeTimeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 3、值为 -2 代表已被冻结\n\t\tif(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 根据全局配置决定是否校验指定 token 的活跃度\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic void checkActiveTimeoutByConfig(String tokenValue) {\n\t\tif(isOpenCheckActiveTimeout()) {\n\t\t\t// storage.get(key, () -> {}) 可以避免一次请求多次校验，造成不必要的性能消耗\n\t\t\tSaHolder.getStorage().get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> {\n\n\t\t\t\t// 1、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制，如果是则代表其已被冻结，需要抛出：token 已被冻结\n\t\t\t\tcheckActiveTimeout(tokenValue);\n\n\t\t\t\t// 2、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 （注意此处的续签是在续 active-timeout，而非 timeout）\n\t\t\t\tif(SaStrategy.instance.autoRenew.apply(this)) {\n\t\t\t\t\tupdateLastActiveToNow(tokenValue);\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * 检查指定 token 是否已被冻结，如果是则抛出异常\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic void checkActiveTimeout(String tokenValue) {\n\t\tif (isFreeze(tokenValue)) {\n\t\t\tthrow NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11016);\n\t\t}\n\t}\n\n\t/**\n\t * 检查当前 token 是否已被冻结，如果是则抛出异常\n\t */\n\tpublic void checkActiveTimeout() {\n\t\tcheckActiveTimeout(getTokenValue());\n\t}\n\n\t/**\n\t * 获取指定 token 在缓存中的 activeTimeout 值，如果不存在则返回 null\n\t *\n\t * @param tokenValue 指定token\n\t * @return /\n\t */\n\tpublic Long getTokenUseActiveTimeout(String tokenValue) {\n\t\t// 在未启用动态 activeTimeout 功能时，直接返回 null\n\t\tif( ! getConfigOrGlobal().getDynamicActiveTimeout()) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 先取出这个 token 的最后活跃时间值\n\t\tString key = splicingKeyLastActiveTime(tokenValue);\n\t\tString value = getSaTokenDao().get(key);\n\n\t\t// 解析，无值的情况下返回 null\n\t\tSaValue2Box box = new SaValue2Box(value);\n\t\treturn box.getValue2AsLong(null);\n\t}\n\n\t/**\n\t * 获取指定 token 在缓存中的 activeTimeout 值，如果不存在则返回全局配置的 activeTimeout 值\n\t *\n\t * @param tokenValue 指定token\n\t * @return /\n\t */\n\tpublic long getTokenUseActiveTimeoutOrGlobalConfig(String tokenValue) {\n\t\tLong activeTimeout = getTokenUseActiveTimeout(tokenValue);\n\t\tif(activeTimeout == null) {\n\t\t\treturn getConfigOrGlobal().getActiveTimeout();\n\t\t}\n\t\treturn activeTimeout;\n\t}\n\n\t/**\n\t * 获取指定 token 的最后活跃时间（13位时间戳），如果不存在则返回 -2\n\t *\n\t * @param tokenValue 指定token\n\t * @return /\n\t */\n\tpublic long getTokenLastActiveTime(String tokenValue) {\n\t\t// 1、如果提供的 token 为 null，则返回 -2\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\n\t\t// 2、获取这个 token 的最后活跃时间，13位时间戳\n\t\tString key = splicingKeyLastActiveTime(tokenValue);\n\t\tString lastActiveTimeString = getSaTokenDao().get(key);\n\n\t\t// 3、查不到，返回-2\n\t\tif(lastActiveTimeString == null) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\n\t\t// 4、根据逗号切割字符串\n\t\treturn new SaValue2Box(lastActiveTimeString).getValue1AsLong();\n\t}\n\n\t/**\n\t * 获取当前 token 的最后活跃时间（13位时间戳），如果不存在则返回 -2\n\t *\n\t * @return /\n\t */\n\tpublic long getTokenLastActiveTime() {\n\t\treturn getTokenLastActiveTime(getTokenValue());\n\t}\n\n\n\t// ------------------- 过期时间相关 -------------------\n\n\t/**\n\t * 获取当前会话 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic long getTokenTimeout() {\n\t\treturn getTokenTimeout(getTokenValue());\n\t}\n\n\t/**\n\t * 获取指定 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @param token 指定token\n\t * @return token剩余有效时间\n\t */\n\tpublic long getTokenTimeout(String token) {\n\t\treturn getSaTokenDao().getTimeout(splicingKeyTokenValue(token));\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @param loginId 指定loginId\n\t * @return token剩余有效时间\n\t */\n\tpublic long getTokenTimeoutByLoginId(Object loginId) {\n\t\treturn getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValueByLoginId(loginId)));\n\t}\n\n\t/**\n\t * 获取当前登录账号的 Account-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic long getSessionTimeout() {\n\t\treturn getSessionTimeoutByLoginId(getLoginIdDefaultNull());\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @param loginId 指定loginId\n\t * @return token剩余有效时间\n\t */\n\tpublic long getSessionTimeoutByLoginId(Object loginId) {\n\t\treturn getSaTokenDao().getSessionTimeout(splicingKeySession(loginId));\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic long getTokenSessionTimeout() {\n\t\treturn getTokenSessionTimeoutByTokenValue(getTokenValue());\n\t}\n\n\t/**\n\t * 获取指定 token 的 Token-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @param tokenValue 指定token\n\t * @return token 剩余有效时间\n\t */\n\tpublic long getTokenSessionTimeoutByTokenValue(String tokenValue) {\n\t\treturn getSaTokenDao().getSessionTimeout(splicingKeyTokenSession(tokenValue));\n\t}\n\n\t/**\n\t * 获取当前 token 剩余活跃有效期：当前 token 距离被冻结还剩多少时间（单位: 秒，返回 -1 代表永不冻结，-2 代表没有这个值或 token 已被冻结了）\n\t *\n\t * @return /\n\t */\n\tpublic long getTokenActiveTimeout() {\n\t\treturn getTokenActiveTimeoutByToken(getTokenValue());\n\t}\n\n\t/**\n\t * 获取指定 token 剩余活跃有效期：这个 token 距离被冻结还剩多少时间（单位: 秒，返回 -1 代表永不冻结，-2 代表没有这个值或 token 已被冻结了）\n\t *\n\t * @param tokenValue 指定 token\n\t * @return /\n\t */\n\tpublic long getTokenActiveTimeoutByToken(String tokenValue) {\n\n\t\t// 如果全局配置了永不冻结, 则返回 -1\n\t\tif( ! isOpenCheckActiveTimeout() ) {\n\t\t\treturn SaTokenDao.NEVER_EXPIRE;\n\t\t}\n\n\t\t// ------ 开始查询\n\n\t\t// 先获取这个 token 的最后活跃时间，13位时间戳\n\t\tlong lastActiveTime = getTokenLastActiveTime(tokenValue);\n\t\tif(lastActiveTime == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\n\t\t// 实际时间差\n\t\tlong timeDiff = (System.currentTimeMillis() - lastActiveTime) / 1000;\n\t\t// 该 token 允许的时间差\n\t\tlong allowTimeDiff = getTokenUseActiveTimeoutOrGlobalConfig(tokenValue);\n\t\tif(allowTimeDiff == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t// 如果允许的时间差为 -1 ，则代表永不冻结，此处需要立即返回 -1 ，无需后续计算\n\t\t\treturn SaTokenDao.NEVER_EXPIRE;\n\t\t}\n\n\t\t// 校验这个时间差是否超过了允许的值\n\t\t//    计算公式为: 允许的最大时间差 - 实际时间差，判断是否 < 0， 如果是则代表已经被冻结 ，返回-2\n\t\tlong activeTimeout = allowTimeDiff - timeDiff;\n\t\tif(activeTimeout < 0) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t} else {\n\t\t\t// 否则代表没冻结，返回剩余有效时间\n\t\t\treturn activeTimeout;\n\t\t}\n\t}\n\n\t/**\n\t * 对当前 token 的 timeout 值进行续期\n\t *\n\t * @param timeout 要修改成为的有效时间 (单位: 秒)\n\t */\n\tpublic void renewTimeout(long timeout) {\n\t\t// 1、续期缓存数据\n\t\tString tokenValue = getTokenValue();\n\t\trenewTimeout(tokenValue, timeout);\n\n\t\t// 2、续期客户端 Cookie 有效期\n\t\tif(getConfigOrGlobal().getIsReadCookie()) {\n\t\t\t// 如果 timeout = -1，代表永久，但是一般浏览器不支持永久 Cookie，所以此处设置为 int 最大值\n\t\t\t// 如果 timeout 大于 int 最大值，会造成数据溢出，所以也要将其设置为 int 最大值\n\t\t\tif(timeout == SaTokenDao.NEVER_EXPIRE || timeout > Integer.MAX_VALUE) {\n\t\t\t\ttimeout = Integer.MAX_VALUE;\n\t\t\t}\n\t\t\tsetTokenValueToCookie(tokenValue, (int)timeout);\n\t\t}\n\t}\n\n\t/**\n\t * 对指定 token 的 timeout 值进行续期\n\t *\n\t * @param tokenValue 指定 token\n\t * @param timeout 要修改成为的有效时间 (单位: 秒，填 -1 代表要续为永久有效)\n\t */\n\tpublic void renewTimeout(String tokenValue, long timeout) {\n\n\t\t// 1、如果 token 指向的 loginId 为空，或者属于异常项时，不进行续期操作\n\t\tObject loginId = getLoginIdByToken(tokenValue);\n\t\tif(loginId == null) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 2、检查 token 合法性\n\t\tSaSession session = getSessionByLoginId(loginId);\n\t\tif(session == null) {\n\t\t\tthrow new SaTokenException(\"未能查询到对应 Access-Session 会话，无法续期\");\n\t\t}\n\t\tif(session.getTerminal(tokenValue) == null) {\n\t\t\tthrow new SaTokenException(\"未能查询到对应终端信息，无法续期\");\n\t\t}\n\n\t\t// 3、续期此 token 本身的有效期 （改 ttl）\n\t\tSaTokenDao dao = getSaTokenDao();\n\t\tdao.updateTimeout(splicingKeyTokenValue(tokenValue), timeout);\n\n\t\t// 4、续期此 token 的 Token-Session 有效期\n\t\tSaSession tokenSession = getTokenSessionByToken(tokenValue, false);\n\t\tif(tokenSession != null) {\n\t\t\ttokenSession.updateTimeout(timeout);\n\t\t}\n\n\t\t// 5、续期此 token 指向的账号的 Account-Session 有效期\n\t\tsession.updateMinTimeout(timeout);\n\n\t\t// 6、更新此 token 的最后活跃时间\n\t\tif(isOpenCheckActiveTimeout()) {\n\t\t\tdao.updateTimeout(splicingKeyLastActiveTime(tokenValue), timeout);\n\t\t}\n\n\t\t// 7、$$ 发布事件：某某 token 被续期了\n\t\tSaTokenEventCenter.doRenewTimeout(loginType, loginId, tokenValue, timeout);\n\t}\n\n\n\t// ------------------- 角色认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的角色集合\n\t *\n\t * @return /\n\t */\n\tpublic List<String> getRoleList() {\n\t\treturn getRoleList(getLoginId());\n\t}\n\n\t/**\n\t * 获取：指定账号的角色集合\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic List<String> getRoleList(Object loginId) {\n\t\treturn SaManager.getStpInterface().getRoleList(loginId, loginType);\n\t}\n\n\t/**\n\t * 判断：当前账号是否拥有指定角色, 返回 true 或 false\n\t *\n\t * @param role 角色\n\t * @return /\n\t */\n\tpublic boolean hasRole(String role) {\n\t\ttry {\n\t\t\treturn hasRole(getLoginId(), role);\n\t\t} catch (NotLoginException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 判断：指定账号是否含有指定角色标识, 返回 true 或 false\n\t *\n\t * @param loginId 账号id\n\t * @param role 角色标识\n\t * @return 是否含有指定角色标识\n\t */\n\tpublic boolean hasRole(Object loginId, String role) {\n\t\treturn hasElement(getRoleList(loginId), role);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n\tpublic boolean hasRoleAnd(String... roleArray){\n\t\ttry {\n\t\t\tcheckRoleAnd(roleArray);\n\t\t\treturn true;\n\t\t} catch (NotLoginException | NotRoleException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n\tpublic boolean hasRoleOr(String... roleArray){\n\t\ttry {\n\t\t\tcheckRoleOr(roleArray);\n\t\t\treturn true;\n\t\t} catch (NotLoginException | NotRoleException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识, 如果验证未通过，则抛出异常: NotRoleException\n\t *\n\t * @param role 角色标识\n\t */\n\tpublic void checkRole(String role) {\n\t\tif( ! hasRole(getLoginId(), role)) {\n\t\t\tthrow new NotRoleException(role, this.loginType).setCode(SaErrorCode.CODE_11041);\n\t\t}\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n\tpublic void checkRoleAnd(String... roleArray){\n\t\t// 先获取当前是哪个账号id\n\t\tObject loginId = getLoginId();\n\n\t\t// 如果没有指定要校验的角色，那么直接跳过\n\t\tif(roleArray == null || roleArray.length == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 开始校验\n\t\tList<String> roleList = getRoleList(loginId);\n\t\tfor (String role : roleArray) {\n\t\t\tif(!hasElement(roleList, role)) {\n\t\t\t\tthrow new NotRoleException(role, this.loginType).setCode(SaErrorCode.CODE_11041);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n\tpublic void checkRoleOr(String... roleArray){\n\t\t// 先获取当前是哪个账号id\n\t\tObject loginId = getLoginId();\n\n\t\t// 如果没有指定权限，那么直接跳过\n\t\tif(roleArray == null || roleArray.length == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 开始校验\n\t\tList<String> roleList = getRoleList(loginId);\n\t\tfor (String role : roleArray) {\n\t\t\tif(hasElement(roleList, role)) {\n\t\t\t\t// 有的话提前退出\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// 代码至此，说明一个都没通过，需要抛出无角色异常\n\t\tthrow new NotRoleException(roleArray[0], this.loginType).setCode(SaErrorCode.CODE_11041);\n\t}\n\n\n\t// ------------------- 权限认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的权限码集合\n\t *\n\t * @return /\n\t */\n\tpublic List<String> getPermissionList() {\n\t\treturn getPermissionList(getLoginId());\n\t}\n\n\t/**\n\t * 获取：指定账号的权限码集合\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic List<String> getPermissionList(Object loginId) {\n\t\treturn SaManager.getStpInterface().getPermissionList(loginId, loginType);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic boolean hasPermission(String permission) {\n\t\ttry {\n\t\t\treturn hasPermission(getLoginId(), permission);\n\t\t} catch (NotLoginException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 判断：指定账号 id 是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param loginId 账号 id\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic boolean hasPermission(Object loginId, String permission) {\n\t\treturn hasElement(getPermissionList(loginId), permission);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，必须全部具有 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n\tpublic boolean hasPermissionAnd(String... permissionArray){\n\t\ttry {\n\t\t\tcheckPermissionAnd(permissionArray);\n\t\t\treturn true;\n\t\t} catch (NotLoginException | NotPermissionException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n\tpublic boolean hasPermissionOr(String... permissionArray){\n\t\ttry {\n\t\t\tcheckPermissionOr(permissionArray);\n\t\t\treturn true;\n\t\t} catch (NotLoginException | NotPermissionException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限, 如果验证未通过，则抛出异常: NotPermissionException\n\t *\n\t * @param permission 权限码\n\t */\n\tpublic void checkPermission(String permission) {\n\t\tif( ! hasPermission(getLoginId(), permission)) {\n\t\t\tthrow new NotPermissionException(permission, this.loginType).setCode(SaErrorCode.CODE_11051);\n\t\t}\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic void checkPermissionAnd(String... permissionArray){\n\t\t// 先获取当前是哪个账号id\n\t\tObject loginId = getLoginId();\n\n\t\t// 如果没有指定权限，那么直接跳过\n\t\tif(permissionArray == null || permissionArray.length == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 开始校验\n\t\tList<String> permissionList = getPermissionList(loginId);\n\t\tfor (String permission : permissionArray) {\n\t\t\tif(!hasElement(permissionList, permission)) {\n\t\t\t\tthrow new NotPermissionException(permission, this.loginType).setCode(SaErrorCode.CODE_11051);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic void checkPermissionOr(String... permissionArray){\n\t\t// 先获取当前是哪个账号id\n\t\tObject loginId = getLoginId();\n\n\t\t// 如果没有指定要校验的权限，那么直接跳过\n\t\tif(permissionArray == null || permissionArray.length == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 开始校验\n\t\tList<String> permissionList = getPermissionList(loginId);\n\t\tfor (String permission : permissionArray) {\n\t\t\tif(hasElement(permissionList, permission)) {\n\t\t\t\t// 有的话提前退出\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// 代码至此，说明一个都没通过，需要抛出无权限异常\n\t\tthrow new NotPermissionException(permissionArray[0], this.loginType).setCode(SaErrorCode.CODE_11051);\n\t}\n\n\n\n\t// ------------------- id 反查 token 相关操作 -------------------\n\n\t/**\n\t * 获取指定账号 id 的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @return token值\n\t */\n\tpublic String getTokenValueByLoginId(Object loginId) {\n\t\treturn getTokenValueByLoginId(loginId, null);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return token值\n\t */\n\tpublic String getTokenValueByLoginId(Object loginId, String deviceType) {\n\t\tList<String> tokenValueList = getTokenValueListByLoginId(loginId, deviceType);\n\t\treturn tokenValueList.isEmpty() ? null : tokenValueList.get(tokenValueList.size() - 1);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有相关 token\n\t */\n\tpublic List<String> getTokenValueListByLoginId(Object loginId) {\n\t\treturn getTokenValueListByLoginId(loginId, null);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic List<String> getTokenValueListByLoginId(Object loginId, String deviceType) {\n\t\t// 如果该账号的 Account-Session 为 null，说明此账号尚没有客户端在登录，此时返回空集合\n\t\tSaSession session = getSessionByLoginId(loginId, false);\n\t\tif(session == null) {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\n\t\t// 按照设备类型进行筛选\n\t\treturn session.getTokenValueListByDeviceType(deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic List<SaTerminalInfo> getTerminalListByLoginId(Object loginId) {\n\t\treturn getTerminalListByLoginId(loginId, null);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic List<SaTerminalInfo> getTerminalListByLoginId(Object loginId, String deviceType) {\n\t\t// 如果该账号的 Account-Session 为 null，说明此账号尚没有客户端在登录，此时返回空集合\n\t\tSaSession session = getSessionByLoginId(loginId, false);\n\t\tif(session == null) {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\n\t\t// 按照设备类型进行筛选\n\t\treturn session.getTerminalListByDeviceType(deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合，执行特定函数\n\t *\n\t * @param loginId 账号id\n\t * @param function 需要执行的函数\n\t */\n\tpublic void forEachTerminalList(Object loginId, SaTwoParamFunction<SaSession, SaTerminalInfo> function) {\n\t\t// 如果该账号的 Account-Session 为 null，说明此账号尚没有客户端在登录，此时无需遍历\n\t\tSaSession session = getSessionByLoginId(loginId, false);\n\t\tif(session == null) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 遍历\n\t\tsession.forEachTerminalList(function);\n\t}\n\n\t/**\n\t * 返回当前 token 指向的 SaTerminalInfo 设备信息，如果 token 无效则返回 null\n\t *\n\t * @return /\n\t */\n\tpublic SaTerminalInfo getTerminalInfo() {\n\t\treturn getTerminalInfoByToken(getTokenValue());\n\t}\n\n\t/**\n\t * 返回指定 token 指向的 SaTerminalInfo 设备信息，如果 Token 无效则返回 null\n\t *\n\t * @param tokenValue 指定 token\n\t * @return /\n\t */\n\tpublic SaTerminalInfo getTerminalInfoByToken(String tokenValue) {\n\t\t// 1、如果 token 为 null，直接提前返回\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 2、判断 Token 是否有效\n\t\tObject loginId = getLoginIdNotHandle(tokenValue);\n\t\tif( ! isValidLoginId(loginId)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 3、判断 Account-Session 是否存在\n\t\tSaSession session = getSessionByLoginId(loginId, false);\n\t\tif(session == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 4、判断 Token 是否已被冻结\n\t\tif(isFreeze(tokenValue)) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// 5、遍历 Account-Session 上的客户端 token 列表，寻找当前 token 对应的设备类型\n\t\tList<SaTerminalInfo> terminalList = session.terminalListCopy();\n\t\tfor (SaTerminalInfo terminal : terminalList) {\n\t\t\tif(terminal.getTokenValue().equals(tokenValue)) {\n\t\t\t\treturn terminal;\n\t\t\t}\n\t\t}\n\n\t\t// 6、没有找到，还是返回 null\n\t\treturn null;\n\t}\n\n\t/**\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic String getLoginDeviceType() {\n\t\treturn getLoginDeviceTypeByToken(getTokenValue());\n\t}\n\n\t/**\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic String getLoginDeviceTypeByToken(String tokenValue) {\n\t\tSaTerminalInfo terminalInfo = getTerminalInfoByToken(tokenValue);\n\t\treturn terminalInfo == null ? null : terminalInfo.getDeviceType();\n\t}\n\n\t/**\n\t * 返回当前会话的登录设备 ID\n\t *\n\t * @return /\n\t */\n\tpublic String getLoginDeviceId() {\n\t\treturn getLoginDeviceIdByToken(getTokenValue());\n\t}\n\n\t/**\n\t * 返回指定 token 会话的登录设备 ID\n\t *\n\t * @param tokenValue 指定token\n\t * @return /\n\t */\n\tpublic String getLoginDeviceIdByToken(String tokenValue) {\n\t\tSaTerminalInfo terminalInfo = getTerminalInfoByToken(tokenValue);\n\t\treturn terminalInfo == null ? null : terminalInfo.getDeviceId();\n\t}\n\n\t/**\n\t * 判断对于指定 loginId 来讲，指定设备 id 是否为可信任设备\n\t * @param deviceId /\n\t * @return /\n\t */\n\tpublic boolean isTrustDeviceId(Object userId, String deviceId) {\n\t\t// 先查询此账号的 Account-Session，如果连 Account-Session 都没有，那么此账号尚未登录，直接返回 false\n\t\tSaSession session = getSessionByLoginId(userId, false);\n\t\tif(session == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 判断\n\t\treturn session.isTrustDeviceId(deviceId);\n\t}\n\n\n\t// ------------------- 会话管理 -------------------\n\n\t/**\n\t * 根据条件查询缓存中所有的 token\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return token集合\n\t */\n\tpublic List<String> searchTokenValue(String keyword, int start, int size, boolean sortType) {\n\t\treturn getSaTokenDao().searchData(splicingKeyTokenValue(\"\"), (keyword == null ? \"\" : keyword), start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 SessionId\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量  (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic List<String> searchSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn getSaTokenDao().searchData(splicingKeySession(\"\"), (keyword == null ? \"\" : keyword), start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 Token-Session-Id\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic List<String> searchTokenSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn getSaTokenDao().searchData(splicingKeyTokenSession(\"\"), (keyword == null ? \"\" : keyword), start, size, sortType);\n\t}\n\n\n\t// ------------------- 账号封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号\n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic void disable(Object loginId, long time) {\n\t\tdisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.DEFAULT_DISABLE_LEVEL, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁 (true=已被封禁, false=未被封禁)\n\t *\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic boolean isDisable(Object loginId) {\n\t\treturn isDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.MIN_DISABLE_LEVEL);\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic void checkDisable(Object loginId) {\n\t\tcheckDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, SaTokenConsts.MIN_DISABLE_LEVEL);\n\t}\n\n\t/**\n\t * 获取：指定账号剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic long getDisableTime(Object loginId) {\n\t\treturn getDisableTime(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE);\n\t}\n\n\t/**\n\t * 解封：指定账号\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic void untieDisable(Object loginId) {\n\t\tuntieDisable(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE);\n\t}\n\n\n\t// ------------------- 分类封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号的指定服务\n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定服务\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic void disable(Object loginId, String service, long time) {\n\t\tdisableLevel(loginId, service, SaTokenConsts.DEFAULT_DISABLE_LEVEL, time);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务 是否已被封禁（true=已被封禁, false=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务\n\t * @return /\n\t */\n\tpublic boolean isDisable(Object loginId, String service) {\n\t\treturn isDisableLevel(loginId, service, SaTokenConsts.MIN_DISABLE_LEVEL);\n\t}\n\n\t/**\n\t * 校验：指定账号 指定服务 是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个\n\t */\n\tpublic void checkDisable(Object loginId, String... services) {\n\t\tif(services != null) {\n\t\t\tfor (String service : services) {\n\t\t\t\tcheckDisableLevel(loginId, service, SaTokenConsts.MIN_DISABLE_LEVEL);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获取：指定账号 指定服务 剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务\n\t * @return see note\n\t */\n\tpublic long getDisableTime(Object loginId, String service) {\n\t\treturn getSaTokenDao().getTimeout(splicingKeyDisable(loginId, service));\n\t}\n\n\t/**\n\t * 解封：指定账号、指定服务\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个\n\t */\n\tpublic void untieDisable(Object loginId, String... services) {\n\n\t\t// 先检查提供的参数是否有效\n\t\tif(SaFoxUtil.isEmpty(loginId)) {\n\t\t\tthrow new SaTokenException(\"请提供要解禁的账号\").setCode(SaErrorCode.CODE_11062);\n\t\t}\n\t\tif(services == null || services.length == 0) {\n\t\t\tthrow new SaTokenException(\"请提供要解禁的服务\").setCode(SaErrorCode.CODE_11063);\n\t\t}\n\n\t\t// 遍历逐个解禁\n\t\tfor (String service : services) {\n\t\t\t// 解封\n\t\t\tgetSaTokenDao().delete(splicingKeyDisable(loginId, service));\n\n\t\t\t// $$ 发布事件\n\t\t\tSaTokenEventCenter.doUntieDisable(loginType, loginId, service);\n\t\t}\n\t}\n\n\n\t// ------------------- 阶梯封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 指定封禁等级\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic void disableLevel(Object loginId, int level, long time) {\n\t\tdisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level, time);\n\t}\n\n\t/**\n\t * 封禁：指定账号的指定服务，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 指定封禁等级\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic void disableLevel(Object loginId, String service, int level, long time) {\n\t\t// 先检查提供的参数是否有效\n\t\tif(SaFoxUtil.isEmpty(loginId)) {\n\t\t\tthrow new SaTokenException(\"请提供要封禁的账号\").setCode(SaErrorCode.CODE_11062);\n\t\t}\n\t\tif(SaFoxUtil.isEmpty(service)) {\n\t\t\tthrow new SaTokenException(\"请提供要封禁的服务\").setCode(SaErrorCode.CODE_11063);\n\t\t}\n\t\tif(level < SaTokenConsts.MIN_DISABLE_LEVEL && level != 0) {\n\t\t\tthrow new SaTokenException(\"封禁等级不可以小于最小值：\" + SaTokenConsts.MIN_DISABLE_LEVEL + \" (0除外)\").setCode(SaErrorCode.CODE_11064);\n\t\t}\n\n\t\t// 打上封禁标记\n\t\tgetSaTokenDao().set(splicingKeyDisable(loginId, service), String.valueOf(level), time);\n\n\t\t// $$ 发布事件\n\t\tSaTokenEventCenter.doDisable(loginType, loginId, service, level, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁到指定等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 指定封禁等级\n\t * @return /\n\t */\n\tpublic boolean isDisableLevel(Object loginId, int level) {\n\t\treturn isDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务，是否已被封禁到指定等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 指定封禁等级\n\t * @return /\n\t */\n\tpublic boolean isDisableLevel(Object loginId, String service, int level) {\n\t\t// 1、先前置检查一下这个账号是否被封禁了\n\t\tint disableLevel = getDisableLevel(loginId, service);\n\t\tif(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 2、再判断被封禁的等级是否达到了指定级别\n\t\treturn disableLevel >= level;\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic void checkDisableLevel(Object loginId, int level) {\n\t\tcheckDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE, level);\n\t}\n\n\t/**\n\t * 校验：指定账号的指定服务，是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic void checkDisableLevel(Object loginId, String service, int level) {\n\t\t// 1、先前置检查一下这个账号是否被封禁了\n\t\tint disableLevel = getDisableLevel(loginId, service);\n\t\tif(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 2、再判断被封禁的等级是否达到了指定级别\n\t\tif(disableLevel >= level) {\n\t\t\tthrow new DisableServiceException(loginType, loginId, service, disableLevel, level, getDisableTime(loginId, service))\n\t\t\t\t\t.setCode(SaErrorCode.CODE_11061);\n\t\t}\n\t}\n\n\t/**\n\t * 获取：指定账号被封禁的等级，如果未被封禁则返回-2\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic int getDisableLevel(Object loginId) {\n\t\treturn getDisableLevel(loginId, SaTokenConsts.DEFAULT_DISABLE_SERVICE);\n\t}\n\n\t/**\n\t * 获取：指定账号的 指定服务 被封禁的等级，如果未被封禁则返回-2\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @return /\n\t */\n\tpublic int getDisableLevel(Object loginId, String service) {\n\t\t// 1、先从缓存中查询数据，缓存中有值，以缓存值优先\n\t\tString value = getSaTokenDao().get(splicingKeyDisable(loginId, service));\n\t\tif(SaFoxUtil.isNotEmpty(value)) {\n\t\t\treturn SaFoxUtil.getValueByType(value, int.class);\n\t\t}\n\n\t\t// 2、如果缓存中无数据，则从\"数据加载器\"中再次查询\n\t\tSaDisableWrapperInfo disableWrapperInfo = SaManager.getStpInterface().isDisabled(loginId, service);\n\n\t\t// 如果返回值 disableTime 有效，则代表返回结果需要写入缓存\n\t\tif(disableWrapperInfo.disableTime == SaTokenDao.NEVER_EXPIRE || disableWrapperInfo.disableTime > 0) {\n\t\t\tdisableLevel(loginId, service, disableWrapperInfo.disableLevel, disableWrapperInfo.disableTime);\n\t\t}\n\n\t\t// 返回查询结果\n\t\treturn disableWrapperInfo.disableLevel;\n\t}\n\n\n\t// ------------------- 临时身份切换 -------------------\n\n\t/**\n\t * 临时切换身份为指定账号id\n\t *\n\t * @param loginId 指定loginId\n\t */\n\tpublic void switchTo(Object loginId) {\n\t\tSaHolder.getStorage().set(splicingKeySwitch(), loginId);\n\t}\n\n\t/**\n\t * 结束临时切换身份\n\t */\n\tpublic void endSwitch() {\n\t\tSaHolder.getStorage().delete(splicingKeySwitch());\n\t}\n\n\t/**\n\t * 判断当前请求是否正处于 [ 身份临时切换 ] 中\n\t *\n\t * @return /\n\t */\n\tpublic boolean isSwitch() {\n\t\treturn SaHolder.getStorage().get(splicingKeySwitch()) != null;\n\t}\n\n\t/**\n\t * 返回 [ 身份临时切换 ] 的 loginId\n\t *\n\t * @return /\n\t */\n\tpublic Object getSwitchLoginId() {\n\t\treturn SaHolder.getStorage().get(splicingKeySwitch());\n\t}\n\n\t/**\n\t * 在一个 lambda 代码段里，临时切换身份为指定账号id，lambda 结束后自动恢复\n\t *\n\t * @param loginId 指定账号id\n\t * @param function 要执行的方法\n\t */\n\tpublic void switchTo(Object loginId, SaFunction function) {\n\t\ttry {\n\t\t\tswitchTo(loginId);\n\t\t\tfunction.run();\n\t\t} finally {\n\t\t\tendSwitch();\n\t\t}\n\t}\n\n\n\t// ------------------- 二级认证 -------------------\n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param safeTime 维持时间 (单位: 秒)\n\t */\n\tpublic void openSafe(long safeTime) {\n\t\topenSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE, safeTime);\n\t}\n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param service 业务标识\n\t * @param safeTime 维持时间 (单位: 秒)\n\t */\n\tpublic void openSafe(String service, long safeTime) {\n\t\t// 1、开启二级认证前必须处于登录状态，否则抛出异常\n\t\tcheckLogin();\n\n\t\t// 2、写入指定的 可以 标记，打开二级认证\n\t\tString tokenValue = getTokenValueNotNull();\n\t\tgetSaTokenDao().set(splicingKeySafe(tokenValue, service), SaTokenConsts.SAFE_AUTH_SAVE_VALUE, safeTime);\n\n\t\t// 3、$$ 发布事件，某某 token 令牌开启了二级认证\n\t\tSaTokenEventCenter.doOpenSafe(loginType, tokenValue, service, safeTime);\n\t}\n\n\t/**\n\t * 判断：当前会话是否处于二级认证时间内\n\t *\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic boolean isSafe() {\n\t\treturn isSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE);\n\t}\n\n\t/**\n\t * 判断：当前会话 是否处于指定业务的二级认证时间内\n\t *\n\t * @param service 业务标识\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic boolean isSafe(String service) {\n\t\treturn isSafe(getTokenValue(), service);\n\t}\n\n\t/**\n\t * 判断：指定 token 是否处于二级认证时间内\n\t *\n\t * @param tokenValue Token 值\n\t * @param service 业务标识\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic boolean isSafe(String tokenValue, String service) {\n\t\t// 1、如果提供的 Token 为空，则直接视为未认证\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 2、如果此 token 不处于登录状态，也将其视为未认证\n\t\tObject loginId = getLoginIdNotHandle(tokenValue);\n\t\tif( ! isValidLoginId(loginId) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 3、如果缓存中可以查询出指定的键值，则代表已认证，否则视为未认证\n\t\tString value = getSaTokenDao().get(splicingKeySafe(tokenValue, service));\n\t\treturn !(SaFoxUtil.isEmpty(value));\n\t}\n\n\t/**\n\t * 校验：当前会话是否已通过二级认证，如未通过则抛出异常\n\t */\n\tpublic void checkSafe() {\n\t\tcheckSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE);\n\t}\n\n\t/**\n\t * 校验：检查当前会话是否已通过指定业务的二级认证，如未通过则抛出异常\n\t *\n\t * @param service 业务标识\n\t */\n\tpublic void checkSafe(String service) {\n\t\t// 1、必须先通过登录校验\n\t\tcheckLogin();\n\n\t\t// 2、再进行二级认证校验\n\t\t// \t\t如果缓存中可以查询出指定的键值，则代表已认证，否则视为未认证\n\t\tString tokenValue = getTokenValue();\n\t\tString value = getSaTokenDao().get(splicingKeySafe(tokenValue, service));\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\tthrow new NotSafeException(loginType, tokenValue, service).setCode(SaErrorCode.CODE_11071);\n\t\t}\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @return 剩余有效时间\n\t */\n\tpublic long getSafeTime() {\n\t\treturn getSafeTime(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE);\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @param service 业务标识\n\t * @return 剩余有效时间\n\t */\n\tpublic long getSafeTime(String service) {\n\t\t// 1、如果前端没有提交 Token，则直接视为未认证\n\t\tString tokenValue = getTokenValue();\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\n\t\t// 2、从缓存中查询这个 key 的剩余有效期\n\t\treturn getSaTokenDao().getTimeout(splicingKeySafe(tokenValue, service));\n\t}\n\n\t/**\n\t * 在当前会话 结束二级认证\n\t */\n\tpublic void closeSafe() {\n\t\tcloseSafe(SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE);\n\t}\n\n\t/**\n\t * 在当前会话 结束指定业务标识的二级认证\n\t *\n\t * @param service 业务标识\n\t */\n\tpublic void closeSafe(String service) {\n\t\t// 1、如果前端没有提交 Token，则无需任何操作\n\t\tString tokenValue = getTokenValue();\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 2、删除 key\n\t\tgetSaTokenDao().delete(splicingKeySafe(tokenValue, service));\n\n\t\t// 3、$$ 发布事件，某某 token 令牌关闭了二级认证\n\t\tSaTokenEventCenter.doCloseSafe(loginType, tokenValue, service);\n\t}\n\n\n\t// ------------------- 拼接相应key -------------------\n\n\t/**\n\t * 获取：客户端 tokenName\n\t *\n\t * @return key\n\t */\n\tpublic String splicingKeyTokenName() {\n\t\treturn getConfigOrGlobal().getTokenName();\n\t}\n\n\t/**\n\t * 拼接： 在保存 token - id 映射关系时，应该使用的key\n\t *\n\t * @param tokenValue token值\n\t * @return key\n\t */\n\tpublic String splicingKeyTokenValue(String tokenValue) {\n\t\treturn getConfigOrGlobal().getTokenName() + \":\" + loginType + \":token:\" + tokenValue;\n\t}\n\n\t/**\n\t * 拼接： 在保存 Account-Session 时，应该使用的 key\n\t *\n\t * @param loginId 账号id\n\t * @return key\n\t */\n\tpublic String splicingKeySession(Object loginId) {\n\t\treturn getConfigOrGlobal().getTokenName() + \":\" + loginType + \":session:\" + loginId;\n\t}\n\n\t/**\n\t * 拼接：在保存 Token-Session 时，应该使用的 key\n\t *\n\t * @param tokenValue token值\n\t * @return key\n\t */\n\tpublic String splicingKeyTokenSession(String tokenValue) {\n\t\treturn getConfigOrGlobal().getTokenName() + \":\" + loginType + \":token-session:\" + tokenValue;\n\t}\n\n\t/**\n\t * 拼接： 在保存 token 最后活跃时间时，应该使用的 key\n\t *\n\t * @param tokenValue token值\n\t * @return key\n\t */\n\tpublic String splicingKeyLastActiveTime(String tokenValue) {\n\t\treturn getConfigOrGlobal().getTokenName() + \":\" + loginType + \":last-active:\" + tokenValue;\n\t}\n\n\t/**\n\t * 拼接：在进行临时身份切换时，应该使用的 key\n\t *\n\t * @return key\n\t */\n\tpublic String splicingKeySwitch() {\n\t\treturn SaTokenConsts.SWITCH_TO_SAVE_KEY + loginType;\n\t}\n\n\t/**\n\t * 如果 token 为本次请求新创建的，则以此字符串为 key 存储在当前 request 中\n\t *\n\t * @return key\n\t */\n\tpublic String splicingKeyJustCreatedSave() {\n\t\t//\t\treturn SaTokenConsts.JUST_CREATED_SAVE_KEY + loginType;\n\t\treturn SaTokenConsts.JUST_CREATED;\n\t}\n\n\t/**\n\t * 拼接： 在保存服务封禁标记时，应该使用的 key\n\t *\n\t * @param loginId 账号id\n\t * @param service 具体封禁的服务\n\t * @return key\n\t */\n\tpublic String splicingKeyDisable(Object loginId, String service) {\n\t\treturn getConfigOrGlobal().getTokenName() + \":\" + loginType + \":disable:\" + service + \":\" + loginId;\n\t}\n\n\t/**\n\t * 拼接： 在保存业务二级认证标记时，应该使用的 key\n\t *\n\t * @param tokenValue 要认证的 Token\n\t * @param service 要认证的业务标识\n\t * @return key\n\t */\n\tpublic String splicingKeySafe(String tokenValue, String service) {\n\t\t// 格式：<Token名称>:<账号类型>:<safe>:<业务标识>:<Token值>\n\t\t// 形如：satoken:login:safe:important:gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__\n\t\treturn getConfigOrGlobal().getTokenName() + \":\" + loginType + \":safe:\" + service + \":\" + tokenValue;\n\t}\n\n\n\t// ------------------- Bean 对象、字段代理 -------------------\n\n\t/**\n\t * 返回当前 StpLogic 使用的持久化对象\n\t *\n\t * @return /\n\t */\n\tpublic SaTokenDao getSaTokenDao() {\n\t\treturn SaManager.getSaTokenDao();\n\t}\n\n\t/**\n\t * 返回当前 StpLogic 是否支持共享 token 策略\n\t *\n\t * @return /\n\t */\n\tpublic boolean isSupportShareToken() {\n\t\treturn getConfigOrGlobal().getIsShare();\n\t}\n\n\t/**\n\t * 返回全局配置是否开启了 Token 活跃度校验，返回 true 代表已打开，返回 false 代表不打开，此时永不冻结 token\n\t *\n\t * @return /\n\t */\n\tpublic boolean isOpenCheckActiveTimeout() {\n\t\tSaTokenConfig cfg = getConfigOrGlobal();\n\t\treturn cfg.getActiveTimeout() != SaTokenDao.NEVER_EXPIRE || cfg.getDynamicActiveTimeout();\n\t}\n\n\t/**\n\t * 返回全局配置的 Cookie 保存时长，单位：秒 （根据全局 timeout 计算）\n\t *\n\t * @return Cookie 应该保存的时长\n\t */\n\tpublic int getConfigOfCookieTimeout() {\n\t\tlong timeout = getConfigOrGlobal().getTimeout();\n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn Integer.MAX_VALUE;\n\t\t}\n\t\treturn (int) timeout;\n\t}\n\n\t/**\n\t * 返回全局配置的 maxTryTimes 值，在每次创建 token 时，对其唯一性测试的最高次数（-1=不测试）\n\t *\n\t * @param loginParameter /\n\t * @return /\n\t */\n\tpublic int getConfigOfMaxTryTimes(SaLoginParameter loginParameter) {\n\t\treturn loginParameter.getMaxTryTimes();\n\t}\n\n\t/**\n\t * 判断：集合中是否包含指定元素（模糊匹配）\n\t *\n\t * @param list 集合\n\t * @param element 元素\n\t * @return /\n\t */\n\tpublic boolean hasElement(List<String> list, String element) {\n\t\treturn SaStrategy.instance.hasElement.apply(list, element);\n\t}\n\n\t/**\n\t * 当前 StpLogic 对象是否支持 token 扩展参数\n\t *\n\t * @return /\n\t */\n\tpublic boolean isSupportExtra() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 根据当前配置对象创建一个 SaLoginParameter 对象\n\t *\n\t * @return /\n\t */\n\tpublic SaLoginParameter createSaLoginParameter() {\n\t\treturn new SaLoginParameter(getConfigOrGlobal());\n\t}\n\n\t/**\n\t * 根据当前配置对象创建一个 SaLogoutParameter 对象\n\t *\n\t * @return /\n\t */\n\tpublic SaLogoutParameter createSaLogoutParameter() {\n\t\treturn new SaLogoutParameter(getConfigOrGlobal());\n\t}\n\n\n\n\t// ------------------- 过期方法 -------------------\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceType </h2>\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic String getLoginDevice() {\n\t\treturn getLoginDeviceType();\n\t}\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceTypeByToken </h2>\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic String getLoginDeviceByToken(String tokenValue) {\n\t\treturn getLoginDeviceTypeByToken(tokenValue);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaTwoParamFunction;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\n\nimport java.util.List;\n\n/**\n * Sa-Token 权限认证工具类\n *\n * @author click33\n * @since 1.0.0\n */\npublic class StpUtil {\n\t\n\tprivate StpUtil() {}\n\t\n\t/**\n\t * 多账号体系下的类型标识\n\t */\n\tpublic static final String TYPE = \"login\";\n\t\n\t/**\n\t * 底层使用的 StpLogic 对象\n\t */\n\tpublic static StpLogic stpLogic = new StpLogic(TYPE);\n\n\t/**\n\t * 获取当前 StpLogic 的账号类型\n\t *\n\t * @return /\n\t */\n\tpublic static String getLoginType(){\n\t\treturn stpLogic.getLoginType();\n\t}\n\n\t/**\n\t * 安全的重置 StpLogic 对象\n\t *\n\t * <br> 1、更改此账户的 StpLogic 对象 \n\t * <br> 2、put 到全局 StpLogic 集合中 \n\t * <br> 3、发送日志 \n\t * \n\t * @param newStpLogic / \n\t */\n\tpublic static void setStpLogic(StpLogic newStpLogic) {\n\t\t// 1、重置此账户的 StpLogic 对象\n\t\tstpLogic = newStpLogic;\n\t\t\n\t\t// 2、添加到全局 StpLogic 集合中\n\t\t//    以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic\n\t\tSaManager.putStpLogic(newStpLogic);\n\t\t\n\t\t// 3、$$ 发布事件：更新了 stpLogic 对象\n\t\tSaTokenEventCenter.doSetStpLogic(stpLogic);\n\t}\n\n\t/**\n\t * 获取 StpLogic 对象\n\t *\n\t * @return / \n\t */\n\tpublic static StpLogic getStpLogic() {\n\t\treturn stpLogic;\n\t}\n\t\n\t\n\t// ------------------- 获取 token 相关 -------------------\n\n\t/**\n\t * 返回 token 名称，此名称在以下地方体现：Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀\n\t *\n\t * @return /\n\t */\n\tpublic static String getTokenName() {\n \t\treturn stpLogic.getTokenName();\n \t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t */\n\tpublic static void setTokenValue(String tokenValue){\n\t\tstpLogic.setTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param cookieTimeout Cookie存活时间(秒)\n\t */\n\tpublic static void setTokenValue(String tokenValue, int cookieTimeout){\n\t\tstpLogic.setTokenValue(tokenValue, cookieTimeout);\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param loginParameter 登录参数\n\t */\n\tpublic static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){\n\t\tstpLogic.setTokenValue(tokenValue, loginParameter);\n\t}\n\n\t/**\n\t * 将 token 写入到当前请求的 Storage 存储器里\n\t *\n\t * @param tokenValue 要保存的 token 值\n\t */\n\tpublic static void setTokenValueToStorage(String tokenValue){\n\t\tstpLogic.setTokenValueToStorage(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值\n\t *\n\t * @return 当前tokenValue\n\t */\n\tpublic static String getTokenValue() {\n\t\treturn stpLogic.getTokenValue();\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值 （不裁剪前缀）\n\t *\n\t * @return / \n\t */\n\tpublic static String getTokenValueNotCut(){\n\t\treturn stpLogic.getTokenValueNotCut();\n\t}\n\n\t/**\n\t * 获取当前会话的 token 参数信息\n\t *\n\t * @return token 参数信息\n\t */\n\tpublic static SaTokenInfo getTokenInfo() {\n\t\treturn stpLogic.getTokenInfo();\n\t}\n\n\t\n\t// ------------------- 登录相关操作 -------------------\n\n\t// --- 登录 \n\n\t/**\n\t * 会话登录\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t */\n\tpublic static void login(Object id) {\n\t\tstpLogic.login(id);\n\t}\n\n\t/**\n\t * 会话登录，并指定登录设备类型\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param deviceType 设备类型\n\t */\n\tpublic static void login(Object id, String deviceType) {\n\t\tstpLogic.login(id, deviceType);\n\t}\n\n\t/**\n\t * 会话登录，并指定是否 [记住我]\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param isLastingCookie 是否为持久Cookie，值为 true 时记住我，值为 false 时关闭浏览器需要重新登录\n\t */\n\tpublic static void login(Object id, boolean isLastingCookie) {\n\t\tstpLogic.login(id, isLastingCookie);\n\t}\n\n\t/**\n\t * 会话登录，并指定此次登录 token 的有效期, 单位:秒\n\t *\n\t * @param id      账号id，建议的类型：（long | int | String）\n\t * @param timeout 此次登录 token 的有效期, 单位:秒\n\t */\n\tpublic static void login(Object id, long timeout) {\n\t\tstpLogic.login(id, timeout);\n\t}\n\n\t/**\n\t * 会话登录，并指定所有登录参数 Model\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model\n\t */\n\tpublic static void login(Object id, SaLoginParameter loginParameter) {\n\t\tstpLogic.login(id, loginParameter);\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic static String createLoginSession(Object id) {\n\t\treturn stpLogic.createLoginSession(id);\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model \n\t * @return 返回会话令牌\n\t */\n\tpublic static String createLoginSession(Object id, SaLoginParameter loginParameter) {\n\t\treturn stpLogic.createLoginSession(id, loginParameter);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的登录会话数据，如果获取不到则创建并返回\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic static String getOrCreateLoginSession(Object id) {\n\t\treturn stpLogic.getOrCreateLoginSession(id);\n\t}\n\n\t// --- 注销 (根据 token)\n\n\t/**\n\t * 在当前客户端会话注销\n\t */\n\tpublic static void logout() {\n\t\tstpLogic.logout();\n\t}\n\n\t/**\n\t * 在当前客户端会话注销，根据注销参数\n\t */\n\tpublic static void logout(SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logout(logoutParameter);\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void logoutByTokenValue(String tokenValue) {\n\t\tstpLogic.logoutByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token、注销参数\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logoutByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void kickoutByTokenValue(String tokenValue) {\n\t\tstpLogic.kickoutByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.kickoutByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void replacedByTokenValue(String tokenValue) {\n\t\tstpLogic.replacedByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.replacedByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t// --- 注销 (根据 loginId)\n\n\t/**\n\t * 会话注销，根据账号id\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void logout(Object loginId) {\n\t\tstpLogic.logout(loginId);\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 设备类型\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型)\n\t */\n\tpublic static void logout(Object loginId, String deviceType) {\n\t\tstpLogic.logout(loginId, deviceType);\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 注销参数\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void logout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logout(loginId, logoutParameter);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void kickout(Object loginId) {\n\t\tstpLogic.kickout(loginId);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型)\n\t */\n\tpublic static void kickout(Object loginId, String deviceType) {\n\t\tstpLogic.kickout(loginId, deviceType);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void kickout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.kickout(loginId, logoutParameter);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void replaced(Object loginId) {\n\t\tstpLogic.replaced(loginId);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 （填 null 代表顶替该账号的所有设备类型）\n\t */\n\tpublic static void replaced(Object loginId, String deviceType) {\n\t\tstpLogic.replaced(loginId, deviceType);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void replaced(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.replaced(loginId, logoutParameter);\n\t}\n\n\t// --- 注销 (会话管理辅助方法)\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (注销下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByLogout(session, terminal);\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByKickout(session, terminal);\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByReplaced(session, terminal);\n\t}\n\n\n\t// 会话查询\n\n\t/**\n\t * 判断当前会话是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic static boolean isLogin() {\n\t\treturn stpLogic.isLogin();\n\t}\n\n\t/**\n\t * 判断指定账号是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic static boolean isLogin(Object loginId) {\n\t\treturn stpLogic.isLogin(loginId);\n\t}\n\n\t/**\n\t * 检验当前会话是否已经登录，如未登录，则抛出异常\n\t */\n \tpublic static void checkLogin() {\n \t\tstpLogic.checkLogin();\n \t}\n\n\t/**\n\t * 获取当前会话账号id，如果未登录，则抛出异常\n\t *\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginId() {\n\t\treturn stpLogic.getLoginId();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回默认值\n\t *\n\t * @param <T> 返回类型 \n\t * @param defaultValue 默认值\n\t * @return 登录id\n\t */\n\tpublic static <T> T getLoginId(T defaultValue) {\n\t\treturn stpLogic.getLoginId(defaultValue);\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回null\n\t *\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginIdDefaultNull() {\n\t\treturn stpLogic.getLoginIdDefaultNull();\n \t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 String 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static String getLoginIdAsString() {\n\t\treturn stpLogic.getLoginIdAsString();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 int 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static int getLoginIdAsInt() {\n\t\treturn stpLogic.getLoginIdAsInt();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 long 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static long getLoginIdAsLong() {\n\t\treturn stpLogic.getLoginIdAsLong();\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶、被冻结等状态，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n \tpublic static Object getLoginIdByToken(String tokenValue) {\n \t\treturn stpLogic.getLoginIdByToken(tokenValue);\n \t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结)，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginIdByTokenNotThinkFreeze(String tokenValue) {\n\t\treturn stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param key 键值 \n\t * @return 对应的扩展数据\n\t */\n\tpublic static Object getExtra(String key) {\n\t\treturn stpLogic.getExtra(key);\n\t}\n\n\t/**\n\t * 获取指定 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param tokenValue 指定的 Token 值\n\t * @param key 键值\n\t * @return 对应的扩展数据\n\t */\n\tpublic static Object getExtra(String tokenValue, String key) {\n\t\treturn stpLogic.getExtra(tokenValue, key);\n\t}\n \t\n \t\n\t// ------------------- Account-Session 相关 -------------------\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @param isCreate 是否新建\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionByLoginId(Object loginId, boolean isCreate) {\n\t\treturn stpLogic.getSessionByLoginId(loginId, isCreate);\n\t}\n\n\t/**\n\t * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建，则返回 null\n\t *\n\t * @param sessionId SessionId\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSessionBySessionId(String sessionId) {\n\t\treturn stpLogic.getSessionBySessionId(sessionId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionByLoginId(Object loginId) {\n\t\treturn stpLogic.getSessionByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param isCreate 是否新建 \n\t * @return Session对象\n\t */\n\tpublic static SaSession getSession(boolean isCreate) {\n\t\treturn stpLogic.getSession(isCreate);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSession() {\n\t\treturn stpLogic.getSession();\n\t}\n\n\t\n\t// ------------------- Token-Session 相关 -------------------  \n\n\t/**\n\t * 获取指定 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param tokenValue Token值\n\t * @return Session对象\n\t */\n\tpublic static SaSession getTokenSessionByToken(String tokenValue) {\n\t\treturn stpLogic.getTokenSessionByToken(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic static SaSession getTokenSession() {\n\t\treturn stpLogic.getTokenSession();\n\t}\n\n\t/**\n\t * 获取当前匿名 Token-Session （可在未登录情况下使用的Token-Session）\n\t *\n\t * @return Token-Session 对象\n\t */\n\tpublic static SaSession getAnonTokenSession() {\n\t\treturn stpLogic.getAnonTokenSession();\n\t}\n\t\n\n\t// ------------------- Active-Timeout token 最低活跃度 验证相关 -------------------\n\n\t/**\n\t * 续签当前 token：(将 [最后操作时间] 更新为当前时间戳)\n\t * <h2>\n\t * \t\t请注意: 即使 token 已被冻结 也可续签成功，\n\t * \t\t如果此场景下需要提示续签失败，可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可\n\t * </h2>\n\t */\n\tpublic static void updateLastActiveToNow() {\n\t\tstpLogic.updateLastActiveToNow();\n\t}\n\n\t/**\n\t * 检查当前 token 是否已被冻结，如果是则抛出异常\n\t */\n \tpublic static void checkActiveTimeout() {\n \t\tstpLogic.checkActiveTimeout();\n \t}\n\n\t/**\n\t * 获取当前 token 的最后活跃时间（13位时间戳），如果不存在则返回 -2\n\t *\n\t * @return /\n\t */\n\tpublic static long getTokenLastActiveTime() {\n\t\treturn stpLogic.getTokenLastActiveTime();\n\t}\n\n\n\t// ------------------- 过期时间相关 -------------------  \n\n\t/**\n\t * 获取当前会话 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n \tpublic static long getTokenTimeout() {\n \t\treturn stpLogic.getTokenTimeout();\n \t}\n\n\t/**\n\t * 获取指定 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @param token 指定token\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getTokenTimeout(String token) {\n\t\treturn stpLogic.getTokenTimeout(token);\n\t}\n\n\t/**\n\t * 获取当前登录账号的 Account-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n \tpublic static long getSessionTimeout() {\n \t\treturn stpLogic.getSessionTimeout();\n \t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n \tpublic static long getTokenSessionTimeout() {\n \t\treturn stpLogic.getTokenSessionTimeout();\n \t}\n\n\t/**\n\t * 获取当前 token 剩余活跃有效期：当前 token 距离被冻结还剩多少时间（单位: 秒，返回 -1 代表永不冻结，-2 代表没有这个值或 token 已被冻结了）\n\t *\n\t * @return /\n\t */\n \tpublic static long getTokenActiveTimeout() {\n \t\treturn stpLogic.getTokenActiveTimeout();\n \t}\n\n\t/**\n\t * 对当前 token 的 timeout 值进行续期\n\t *\n\t * @param timeout 要修改成为的有效时间 (单位: 秒)\n\t */\n \tpublic static void renewTimeout(long timeout) {\n \t\tstpLogic.renewTimeout(timeout);\n \t}\n\n\t/**\n\t * 对指定 token 的 timeout 值进行续期\n\t *\n\t * @param tokenValue 指定 token\n\t * @param timeout 要修改成为的有效时间 (单位: 秒，填 -1 代表要续为永久有效)\n\t */\n \tpublic static void renewTimeout(String tokenValue, long timeout) {\n \t\tstpLogic.renewTimeout(tokenValue, timeout);\n \t}\n \t\n \t\n\t// ------------------- 角色认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的角色集合\n\t *\n\t * @return /\n\t */\n\tpublic static List<String> getRoleList() {\n\t\treturn stpLogic.getRoleList();\n\t}\n\n\t/**\n\t * 获取：指定账号的角色集合\n\t *\n\t * @param loginId 指定账号id \n\t * @return /\n\t */\n\tpublic static List<String> getRoleList(Object loginId) {\n\t\treturn stpLogic.getRoleList(loginId);\n\t}\n\n\t/**\n\t * 判断：当前账号是否拥有指定角色, 返回 true 或 false\n\t *\n\t * @param role 角色\n\t * @return /\n\t */\n \tpublic static boolean hasRole(String role) {\n \t\treturn stpLogic.hasRole(role);\n \t}\n\n\t/**\n\t * 判断：指定账号是否含有指定角色标识, 返回 true 或 false\n\t *\n\t * @param loginId 账号id\n\t * @param role 角色标识\n\t * @return 是否含有指定角色标识\n\t */\n \tpublic static boolean hasRole(Object loginId, String role) {\n \t\treturn stpLogic.hasRole(loginId, role);\n \t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n \tpublic static boolean hasRoleAnd(String... roleArray){\n \t\treturn stpLogic.hasRoleAnd(roleArray);\n \t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n \tpublic static boolean hasRoleOr(String... roleArray){\n \t\treturn stpLogic.hasRoleOr(roleArray);\n \t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识, 如果验证未通过，则抛出异常: NotRoleException\n\t *\n\t * @param role 角色标识\n\t */\n \tpublic static void checkRole(String role) {\n \t\tstpLogic.checkRole(role);\n \t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n \tpublic static void checkRoleAnd(String... roleArray){\n \t\tstpLogic.checkRoleAnd(roleArray);\n \t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n \tpublic static void checkRoleOr(String... roleArray){\n \t\tstpLogic.checkRoleOr(roleArray);\n \t}\n\n\t\n\t// ------------------- 权限认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的权限码集合\n\t *\n\t * @return / \n\t */\n\tpublic static List<String> getPermissionList() {\n\t\treturn stpLogic.getPermissionList();\n\t}\n\n\t/**\n\t * 获取：指定账号的权限码集合\n\t *\n\t * @param loginId 指定账号id\n\t * @return / \n\t */\n\tpublic static List<String> getPermissionList(Object loginId) {\n\t\treturn stpLogic.getPermissionList(loginId);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic static boolean hasPermission(String permission) {\n\t\treturn stpLogic.hasPermission(permission);\n\t}\n\n\t/**\n\t * 判断：指定账号 id 是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param loginId 账号 id\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic static boolean hasPermission(Object loginId, String permission) {\n\t\treturn stpLogic.hasPermission(loginId, permission);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，必须全部具有 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n \tpublic static boolean hasPermissionAnd(String... permissionArray){\n \t\treturn stpLogic.hasPermissionAnd(permissionArray);\n \t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n \tpublic static boolean hasPermissionOr(String... permissionArray){\n \t\treturn stpLogic.hasPermissionOr(permissionArray);\n \t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限, 如果验证未通过，则抛出异常: NotPermissionException\n\t *\n\t * @param permission 权限码\n\t */\n\tpublic static void checkPermission(String permission) {\n\t\tstpLogic.checkPermission(permission);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic static void checkPermissionAnd(String... permissionArray) {\n\t\tstpLogic.checkPermissionAnd(permissionArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic static void checkPermissionOr(String... permissionArray) {\n\t\tstpLogic.checkPermissionOr(permissionArray);\n\t}\n\n\n\t// ------------------- id 反查 token 相关操作 -------------------\n\n\t/**\n\t * 获取指定账号 id 的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @return token值\n\t */\n\tpublic static String getTokenValueByLoginId(Object loginId) {\n\t\treturn stpLogic.getTokenValueByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return token值\n\t */\n\tpublic static String getTokenValueByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTokenValueByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有相关 token\n\t */\n\tpublic static List<String> getTokenValueListByLoginId(Object loginId) {\n\t\treturn stpLogic.getTokenValueListByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic static List<String> getTokenValueListByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTokenValueListByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic static List<SaTerminalInfo> getTerminalListByLoginId(Object loginId) {\n\t\treturn stpLogic.getTerminalListByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return /\n\t */\n\tpublic static List<SaTerminalInfo> getTerminalListByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTerminalListByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合，执行特定函数\n\t *\n\t * @param loginId 账号id\n\t * @param function 需要执行的函数\n\t */\n\tpublic static void forEachTerminalList(Object loginId, SaTwoParamFunction<SaSession, SaTerminalInfo> function) {\n\t\tstpLogic.forEachTerminalList(loginId, function);\n\t}\n\n\t/**\n\t * 返回当前 token 指向的 SaTerminalInfo 设备信息，如果 token 无效则返回 null\n\t *\n\t * @return /\n\t */\n\tpublic static SaTerminalInfo getTerminalInfo() {\n\t\treturn stpLogic.getTerminalInfo();\n\t}\n\n\t/**\n\t * 返回指定 token 指向的 SaTerminalInfo 设备信息，如果 Token 无效则返回 null\n\t *\n\t * @param tokenValue 指定 token\n\t * @return /\n\t */\n\tpublic static SaTerminalInfo getTerminalInfoByToken(String tokenValue) {\n\t\treturn stpLogic.getTerminalInfoByToken(tokenValue);\n\t}\n\n\t/**\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic static String getLoginDeviceType() {\n\t\treturn stpLogic.getLoginDeviceType();\n\t}\n\n\t/**\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic static String getLoginDeviceTypeByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginDeviceTypeByToken(tokenValue);\n\t}\n\n\t/**\n\t * 返回当前会话的登录设备 ID\n\t *\n\t * @return /\n\t */\n\tpublic static String getLoginDeviceId() {\n\t\treturn stpLogic.getLoginDeviceId();\n\t}\n\n\t/**\n\t * 返回指定 token 会话的登录设备 ID\n\t *\n\t * @param tokenValue 指定token\n\t * @return /\n\t */\n\tpublic static String getLoginDeviceIdByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginDeviceIdByToken(tokenValue);\n\t}\n\n\t/**\n\t * 判断对于指定 loginId 来讲，指定设备 id 是否为可信任设备\n\t * @param deviceId /\n\t * @return /\n\t */\n\tpublic static boolean isTrustDeviceId(Object userId, String deviceId) {\n\t\treturn stpLogic.isTrustDeviceId(userId, deviceId);\n\t}\n\n\n\n\t// ------------------- 会话管理 -------------------  \n\n\t/**\n\t * 根据条件查询缓存中所有的 token\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return token集合\n\t */\n\tpublic static List<String> searchTokenValue(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchTokenValue(keyword, start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 SessionId\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量  (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic static List<String> searchSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchSessionId(keyword, start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 Token-Session-Id\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic static List<String> searchTokenSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchTokenSessionId(keyword, start, size, sortType);\n\t}\n\n\t\n\t// ------------------- 账号封禁 -------------------  \n\n\t/**\n\t * 封禁：指定账号\n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id \n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disable(Object loginId, long time) {\n\t\tstpLogic.disable(loginId, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁 (true=已被封禁, false=未被封禁) \n\t *\n\t * @param loginId 账号id\n\t * @return / \n\t */\n\tpublic static boolean isDisable(Object loginId) {\n\t\treturn stpLogic.isDisable(loginId);\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void checkDisable(Object loginId) {\n\t\tstpLogic.checkDisable(loginId);\n\t}\n\n\t/**\n\t * 获取：指定账号剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @return / \n\t */\n\tpublic static long getDisableTime(Object loginId) {\n\t\treturn stpLogic.getDisableTime(loginId);\n\t}\n\n\t/**\n\t * 解封：指定账号\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void untieDisable(Object loginId) {\n\t\tstpLogic.untieDisable(loginId);\n\t}\n\n\t\n\t// ------------------- 分类封禁 -------------------  \n\n\t/**\n\t * 封禁：指定账号的指定服务 \n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定服务 \n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disable(Object loginId, String service, long time) {\n\t\tstpLogic.disable(loginId, service, time);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务 是否已被封禁（true=已被封禁, false=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务 \n\t * @return / \n\t */\n\tpublic static boolean isDisable(Object loginId, String service) {\n\t\treturn stpLogic.isDisable(loginId, service);\n\t}\n\n\t/**\n\t * 校验：指定账号 指定服务 是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个 \n\t */\n\tpublic static void checkDisable(Object loginId, String... services) {\n\t\tstpLogic.checkDisable(loginId, services);\n\t}\n\n\t/**\n\t * 获取：指定账号 指定服务 剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务 \n\t * @return see note \n\t */\n\tpublic static long getDisableTime(Object loginId, String service) {\n\t\treturn stpLogic.getDisableTime(loginId, service);\n\t}\n\n\t/**\n\t * 解封：指定账号、指定服务\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个 \n\t */\n\tpublic static void untieDisable(Object loginId, String... services) {\n\t\tstpLogic.untieDisable(loginId, services);\n\t}\n\n\n\t// ------------------- 阶梯封禁 -------------------  \n\n\t/**\n\t * 封禁：指定账号，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id \n\t * @param level 指定封禁等级 \n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disableLevel(Object loginId, int level, long time) {\n\t\tstpLogic.disableLevel(loginId, level, time);\n\t}\n\n\t/**\n\t * 封禁：指定账号的指定服务，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id \n\t * @param service 指定封禁服务 \n\t * @param level 指定封禁等级 \n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disableLevel(Object loginId, String service, int level, long time) {\n\t\tstpLogic.disableLevel(loginId, service, level, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁到指定等级\n\t *\n\t * @param loginId 指定账号id \n\t * @param level 指定封禁等级 \n\t * @return / \n\t */\n\tpublic static boolean isDisableLevel(Object loginId, int level) {\n\t\treturn stpLogic.isDisableLevel(loginId, level);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务，是否已被封禁到指定等级 \n\t *\n\t * @param loginId 指定账号id \n\t * @param service 指定封禁服务 \n\t * @param level 指定封禁等级 \n\t * @return / \n\t */\n\tpublic static boolean isDisableLevel(Object loginId, String service, int level) {\n\t\treturn stpLogic.isDisableLevel(loginId, service, level);\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id \n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic static void checkDisableLevel(Object loginId, int level) {\n\t\tstpLogic.checkDisableLevel(loginId, level);\n\t}\n\n\t/**\n\t * 校验：指定账号的指定服务，是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id \n\t * @param service 指定封禁服务 \n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic static void checkDisableLevel(Object loginId, String service, int level) {\n\t\tstpLogic.checkDisableLevel(loginId, service, level);\n\t}\n\n\t/**\n\t * 获取：指定账号被封禁的等级，如果未被封禁则返回-2 \n\t *\n\t * @param loginId 指定账号id \n\t * @return / \n\t */\n\tpublic static int getDisableLevel(Object loginId) {\n\t\treturn stpLogic.getDisableLevel(loginId);\n\t}\n\n\t/**\n\t * 获取：指定账号的 指定服务 被封禁的等级，如果未被封禁则返回-2 \n\t *\n\t * @param loginId 指定账号id \n\t * @param service 指定封禁服务 \n\t * @return / \n\t */\n\tpublic static int getDisableLevel(Object loginId, String service) {\n\t\treturn stpLogic.getDisableLevel(loginId, service);\n\t}\n\t\n\t\n\t// ------------------- 临时身份切换 -------------------\n\n\t/**\n\t * 临时切换身份为指定账号id\n\t *\n\t * @param loginId 指定loginId \n\t */\n\tpublic static void switchTo(Object loginId) {\n\t\tstpLogic.switchTo(loginId);\n\t}\n\n\t/**\n\t * 结束临时切换身份\n\t */\n\tpublic static void endSwitch() {\n\t\tstpLogic.endSwitch();\n\t}\n\n\t/**\n\t * 判断当前请求是否正处于 [ 身份临时切换 ] 中\n\t *\n\t * @return /\n\t */\n\tpublic static boolean isSwitch() {\n\t\treturn stpLogic.isSwitch();\n\t}\n\n\t/**\n\t * 在一个 lambda 代码段里，临时切换身份为指定账号id，lambda 结束后自动恢复\n\t *\n\t * @param loginId 指定账号id \n\t * @param function 要执行的方法 \n\t */\n\tpublic static void switchTo(Object loginId, SaFunction function) {\n\t\tstpLogic.switchTo(loginId, function);\n\t}\n\t\n\n\t// ------------------- 二级认证 -------------------  \n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param safeTime 维持时间 (单位: 秒) \n\t */\n\tpublic static void openSafe(long safeTime) {\n\t\tstpLogic.openSafe(safeTime);\n\t}\n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param service 业务标识  \n\t * @param safeTime 维持时间 (单位: 秒) \n\t */\n\tpublic static void openSafe(String service, long safeTime) {\n\t\tstpLogic.openSafe(service, safeTime);\n\t}\n\n\t/**\n\t * 判断：当前会话是否处于二级认证时间内\n\t *\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 \n\t */\n\tpublic static boolean isSafe() {\n\t\treturn stpLogic.isSafe();\n\t}\n\n\t/**\n\t * 判断：当前会话 是否处于指定业务的二级认证时间内\n\t *\n\t * @param service 业务标识  \n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 \n\t */\n\tpublic static boolean isSafe(String service) {\n\t\treturn stpLogic.isSafe(service);\n\t}\n\n\t/**\n\t * 判断：指定 token 是否处于二级认证时间内\n\t *\n\t * @param tokenValue Token 值  \n\t * @param service 业务标识  \n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 \n\t */\n\tpublic static boolean isSafe(String tokenValue, String service) {\n\t\treturn stpLogic.isSafe(tokenValue, service);\n\t}\n\n\t/**\n\t * 校验：当前会话是否已通过二级认证，如未通过则抛出异常\n\t */\n\tpublic static void checkSafe() {\n\t\tstpLogic.checkSafe();\n\t}\n\n\t/**\n\t * 校验：检查当前会话是否已通过指定业务的二级认证，如未通过则抛出异常\n\t *\n\t * @param service 业务标识  \n\t */\n\tpublic static void checkSafe(String service) {\n\t\tstpLogic.checkSafe(service);\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @return 剩余有效时间\n\t */\n\tpublic static long getSafeTime() {\n\t\treturn stpLogic.getSafeTime();\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @param service 业务标识  \n\t * @return 剩余有效时间\n\t */\n\tpublic static long getSafeTime(String service) {\n\t\treturn stpLogic.getSafeTime(service);\n\t}\n\n\t/**\n\t * 在当前会话 结束二级认证 \n\t */\n\tpublic static void closeSafe() {\n\t\tstpLogic.closeSafe();\n\t}\n\n\t/**\n\t * 在当前会话 结束指定业务标识的二级认证\n\t *\n\t * @param service 业务标识  \n\t */\n\tpublic static void closeSafe(String service) {\n\t\tstpLogic.closeSafe(service);\n\t}\n\n\n\t// ------------------- Bean 对象、字段代理 -------------------\n\n\t/**\n\t * 根据当前配置对象创建一个 SaLoginParameter 对象\n\t *\n\t * @return /\n\t */\n\tpublic static SaLoginParameter createSaLoginParameter() {\n\t\treturn stpLogic.createSaLoginParameter();\n\t}\n\n\n\t// ------------------- 过期方法 -------------------\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceType </h2>\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic static String getLoginDevice() {\n\t\treturn stpLogic.getLoginDevice();\n\t}\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceTypeByToken </h2>\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic static String getLoginDeviceByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginDeviceByToken(tokenValue);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLoginParameter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp.parameter;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaCookieConfig;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.stp.parameter.enums.SaLogoutMode;\nimport cn.dev33.satoken.stp.parameter.enums.SaReplacedLoginExitMode;\nimport cn.dev33.satoken.stp.parameter.enums.SaReplacedRange;\nimport cn.dev33.satoken.util.SaTokenConsts;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 在调用 `StpUtil.login()` 时的 配置参数对象，决定登录的一些细节行为 <br>\n *\n * <pre>\n *     \t// 例如：在登录时指定 token 有效期为七天，代码如下：\n *     \tStpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));\n * </pre>\n *\n * @author click33\n * @since 1.13.2\n */\npublic class SaLoginParameter {\n\n\t// --------- 单独参数\n\n\t/**\n\t * 此次登录的客户端设备类型 \n\t */\n\tprivate String deviceType;\n\n\t/**\n\t * 此次登录的客户端设备id\n\t */\n\tprivate String deviceId;\n\n\t/**\n\t * 扩展信息（只在 jwt 模式下生效）\n\t */\n\tprivate Map<String, Object> extraData;\n\n\t/**\n\t * 预定Token（预定本次登录生成的Token值）\n\t */\n\tprivate String token;\n\n\t/**\n\t * 本次登录挂载到 SaTerminalInfo 的自定义扩展数据\n\t */\n\tprivate Map<String, Object> terminalExtraData;\n\n\n\t// --------- 覆盖性参数\n\n\t/**\n\t * 指定此次登录 token 有效期，单位：秒 （如未指定，自动取全局配置的 timeout 值）\n\t */\n\tprivate long timeout;\n\n\t/**\n\t * 指定此次登录 token 最低活跃频率，单位：秒（如未指定，则使用全局配置的 activeTimeout 值）\n\t */\n\tprivate Long activeTimeout;\n\n\t/**\n\t * 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\t */\n\tprivate Boolean isConcurrent;\n\n\t/**\n\t * 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n\t */\n\tprivate Boolean isShare;\n\n\t/**\n\t * 同一账号最大登录数量，-1代表不限 （只有在 isConcurrent=true, isShare=false 时此配置项才有意义）\n\t */\n\tprivate int maxLoginCount;\n\n\t/**\n\t * 在每次创建 token 时的最高循环次数，用于保证 token 唯一性（-1=不循环尝试，直接使用）\n\t */\n\tprivate int maxTryTimes;\n\n\t/**\n\t * 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t */\n\tprivate Boolean isLastingCookie;\n\n\t/**\n\t * 是否在登录后将 Token 写入到响应头\n\t */\n\tprivate Boolean isWriteHeader;\n\n\t/**\n\t * 在 isConcurrent=false 时，决定新旧设备谁将放弃会话\n\t */\n\tprivate SaReplacedLoginExitMode replacedLoginExitMode;\n\n\t/**\n\t * 当 isConcurrent=false 时，顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端)\n\t */\n\tprivate SaReplacedRange replacedRange;\n\n\t/**\n\t * 溢出 maxLoginCount 的客户端，将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线)\n\t */\n\tprivate SaLogoutMode overflowLogoutMode;\n\n\t/**\n\t * 在登录时，是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\n\t */\n\tprivate Boolean rightNowCreateTokenSession;\n\n\t/**\n\t * Cookie 配置对象\n\t */\n\tpublic SaCookieConfig cookie = new SaCookieConfig();\n\n\n\t// ------ 附加方法\n\n\tpublic SaLoginParameter() {\n\t\tthis(SaManager.getConfig());\n\t}\n\tpublic SaLoginParameter(SaTokenConfig config) {\n\t\tsetDefaultValues(config);\n\t}\n\n\t/**\n\t * 根据 SaTokenConfig 对象初始化默认值\n\t *\n\t * @param config 使用的配置对象\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setDefaultValues(SaTokenConfig config) {\n\t\tthis.deviceType = SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE;\n\t\tthis.timeout = config.getTimeout();\n\t\tthis.isConcurrent = config.getIsConcurrent();\n\t\tthis.isShare = config.getIsShare();\n\t\tthis.maxLoginCount = config.getMaxLoginCount();\n\t\tthis.maxTryTimes = config.getMaxTryTimes();\n\t\tthis.isLastingCookie = config.getIsLastingCookie();\n\t\tthis.isWriteHeader = config.getIsWriteHeader();\n\t\tthis.replacedRange = config.getReplacedRange();\n\t\tthis.overflowLogoutMode = config.getOverflowLogoutMode();\n\t\tthis.rightNowCreateTokenSession = config.getRightNowCreateTokenSession();\n\t\tthis.replacedLoginExitMode = config.getReplacedLoginExitMode();\n\n\t\tthis.setupCookieConfig(cookie -> {\n\t\t\tSaCookieConfig gCookie = config.getCookie();\n\t\t\tcookie.setDomain(gCookie.getDomain());\n\t\t\tcookie.setPath(gCookie.getPath());\n\t\t\tcookie.setSecure(gCookie.getSecure());\n\t\t\tcookie.setHttpOnly(gCookie.getHttpOnly());\n\t\t\tcookie.setSameSite(gCookie.getSameSite());\n\t\t\tcookie.setExtraAttrs(new LinkedHashMap<>(gCookie.getExtraAttrs()));\n\t\t});\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入扩展数据（只在jwt模式下生效）\n\t * @param key 键\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setExtra(String key, Object value) {\n\t\tif(this.extraData == null) {\n\t\t\tthis.extraData = new LinkedHashMap<>();\n\t\t}\n\t\tthis.extraData.put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取扩展数据（只在jwt模式下生效）\n\t * @param key 键\n\t * @return 扩展数据的值\n\t */\n\tpublic Object getExtra(String key) {\n\t\tif(this.extraData == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.extraData.get(key);\n\t}\n\n\t/**\n\t * 判断是否设置了扩展数据（只在jwt模式下生效）\n\t * @return /\n\t */\n\tpublic boolean haveExtraData() {\n\t\treturn extraData != null && !extraData.isEmpty();\n\t}\n\n\t/**\n\t * 写入本次登录挂载到 SaTerminalInfo 的自定义扩展数据\n\t * @param key 键\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tpublic SaLoginParameter setTerminalExtra(String key, Object value) {\n\t\tif(this.terminalExtraData == null) {\n\t\t\tthis.terminalExtraData = new LinkedHashMap<>();\n\t\t}\n\t\tthis.terminalExtraData.put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取本次登录挂载到 SaTerminalInfo 的自定义扩展数据\n\t * @param key 键\n\t * @return 扩展数据的值 \n\t */\n\tpublic Object getTerminalExtra(String key) {\n\t\tif(this.terminalExtraData == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.terminalExtraData.get(key);\n\t}\n\n\t/**\n\t * 判断是否设置了本次登录挂载到 SaTerminalInfo 的自定义扩展数据\n\t * @return / \n\t */\n\tpublic boolean haveTerminalExtraData() {\n\t\treturn terminalExtraData != null && !terminalExtraData.isEmpty();\n\t}\n\n\t/**\n\t * 计算 Cookie 时长\n\t * @return /\n\t */\n\tpublic int getCookieTimeout() {\n\t\tif( ! getIsLastingCookie()) {\n\t\t\treturn -1;\n\t\t}\n\t\tlong _timeout = getTimeout();\n\t\tif(_timeout == SaTokenDao.NEVER_EXPIRE || _timeout > Integer.MAX_VALUE) {\n\t\t\treturn Integer.MAX_VALUE;\n\t\t}\n\t\treturn (int)_timeout;\n\t}\n\n\t/**\n\t * 静态方法获取一个 SaLoginParameter 对象\n\t * @return SaLoginParameter 对象\n\t */\n\tpublic static SaLoginParameter create() {\n\t\treturn new SaLoginParameter(SaManager.getConfig());\n\t}\n\n\t/**\n\t * 设置 Cookie 配置项\n\t * @param fun /\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setupCookieConfig(SaParamFunction<SaCookieConfig> fun) {\n\t\tfun.run(this.cookie);\n\t\treturn this;\n\t}\n\n\n\n\t// ---------------- get set\n\n\t/**\n\t * @return 此次登录的客户端设备类型\n\t */\n\tpublic String getDeviceType() {\n\t\treturn deviceType;\n\t}\n\n\t/**\n\t * @param deviceType 此次登录的客户端设备类型\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setDeviceType(String deviceType) {\n\t\tthis.deviceType = deviceType;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 此次登录的客户端设备id\n\t *\n\t * @return deviceId 此次登录的客户端设备id\n\t */\n\tpublic String getDeviceId() {\n\t\treturn this.deviceId;\n\t}\n\n\t/**\n\t * 设置 此次登录的客户端设备id\n\t *\n\t * @param deviceId 此次登录的客户端设备id\n\t */\n\tpublic SaLoginParameter setDeviceId(String deviceId) {\n\t\tthis.deviceId = deviceId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 当 isConcurrent=false 时，顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端)\n\t *\n\t * @return replacedMode 顶人下线的范围\n\t */\n\tpublic SaReplacedRange getReplacedRange() {\n\t\treturn this.replacedRange;\n\t}\n\n\t/**\n\t * 当 isConcurrent=false 时，顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端)\n\t *\n\t * @param replacedRange /\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setReplacedRange(SaReplacedRange replacedRange) {\n\t\tthis.replacedRange = replacedRange;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 溢出 maxLoginCount 的客户端，将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线)\n\t *\n\t * @return overflowLogoutMode /\n\t */\n\tpublic SaLogoutMode getOverflowLogoutMode() {\n\t\treturn this.overflowLogoutMode;\n\t}\n\n\t/**\n\t * 设置 溢出 maxLoginCount 的客户端，将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线)\n\t *\n\t * @param overflowLogoutMode /\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setOverflowLogoutMode(SaLogoutMode overflowLogoutMode) {\n\t\tthis.overflowLogoutMode = overflowLogoutMode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t */\n\tpublic Boolean getIsLastingCookie() {\n\t\treturn isLastingCookie;\n\t}\n\n\t/**\n\t * @param isLastingCookie 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setIsLastingCookie(Boolean isLastingCookie) {\n\t\tthis.isLastingCookie = isLastingCookie;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 指定此次登录 token 有效期，单位：秒\n\t */\n\tpublic long getTimeout() {\n\t\treturn timeout;\n\t}\n\n\t/**\n\t * @param timeout 指定此次登录 token 有效期，单位：秒 （如未指定，自动取全局配置的 timeout 值）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setTimeout(long timeout) {\n\t\tthis.timeout = timeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 此次登录 token 最低活跃频率，单位：秒（如未指定，则使用全局配置的 activeTimeout 值）\n\t */\n\tpublic Long getActiveTimeout() {\n\t\treturn activeTimeout;\n\t}\n\n\t/**\n\t * @param activeTimeout 指定此次登录 token 最低活跃频率，单位：秒（如未指定，则使用全局配置的 activeTimeout 值）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setActiveTimeout(long activeTimeout) {\n\t\tthis.activeTimeout = activeTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\t */\n\tpublic Boolean getIsConcurrent() {\n\t\treturn isConcurrent;\n\t}\n\n\t/**\n\t * @param isConcurrent 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setIsConcurrent(Boolean isConcurrent) {\n\t\tthis.isConcurrent = isConcurrent;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token）\n\t */\n\tpublic Boolean getIsShare() {\n\t\treturn isShare;\n\t}\n\n\t/**\n\t * @param isShare 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setIsShare(Boolean isShare) {\n\t\tthis.isShare = isShare;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 同一账号最大登录数量，-1代表不限 （只有在 isConcurrent=true, isShare=false 时此配置项才有意义）\n\t */\n\tpublic int getMaxLoginCount() {\n\t\treturn maxLoginCount;\n\t}\n\n\t/**\n\t * @param maxLoginCount 同一账号最大登录数量，-1代表不限 （只有在 isConcurrent=true, isShare=false 时此配置项才有意义）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setMaxLoginCount(int maxLoginCount) {\n\t\tthis.maxLoginCount = maxLoginCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 在每次创建 token 时的最高循环次数，用于保证 token 唯一性（-1=不循环尝试，直接使用）\n\t */\n\tpublic int getMaxTryTimes() {\n\t\treturn maxTryTimes;\n\t}\n\n\t/**\n\t * @param maxTryTimes 在每次创建 token 时的最高循环次数，用于保证 token 唯一性（-1=不循环尝试，直接使用）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setMaxTryTimes(int maxTryTimes) {\n\t\tthis.maxTryTimes = maxTryTimes;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 扩展信息（只在jwt模式下生效）\n\t */\n\tpublic Map<String, Object> getExtraData() {\n\t\treturn extraData;\n\t}\n\n\t/**\n\t * @param extraData 扩展信息（只在jwt模式下生效）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setExtraData(Map<String, Object> extraData) {\n\t\tthis.extraData = extraData;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 预定Token（预定本次登录生成的Token值）\n\t */\n\tpublic String getToken() {\n\t\treturn token;\n\t}\n\n\t/**\n\t * @param token 预定Token（预定本次登录生成的Token值）\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setToken(String token) {\n\t\tthis.token = token;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 是否在登录后将 Token 写入到响应头\n\t */\n\tpublic Boolean getIsWriteHeader() {\n\t\treturn isWriteHeader;\n\t}\n\n\t/**\n\t * @param isWriteHeader 是否在登录后将 Token 写入到响应头\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setIsWriteHeader(Boolean isWriteHeader) {\n\t\tthis.isWriteHeader = isWriteHeader;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 本次登录挂载到 SaTerminalInfo 的自定义扩展数据\n\t *\n\t * @return /\n\t */\n\tpublic Map<String, Object> getTerminalExtraData() {\n\t\treturn this.terminalExtraData;\n\t}\n\n\t/**\n\t * 设置 本次登录挂载到 SaTerminalInfo 的自定义扩展数据\n\t *\n\t * @param terminalExtraData /\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setTerminalExtraData(Map<String, Object> terminalExtraData) {\n\t\tthis.terminalExtraData = terminalExtraData;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 在登录时，是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\n\t *\n\t * @return /\n\t */\n\tpublic Boolean getRightNowCreateTokenSession() {\n\t\treturn this.rightNowCreateTokenSession;\n\t}\n\n\t/**\n\t * 设置 在登录时，是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\n\t *\n\t * @param rightNowCreateTokenSession /\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setRightNowCreateTokenSession(Boolean rightNowCreateTokenSession) {\n\t\tthis.rightNowCreateTokenSession = rightNowCreateTokenSession;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return Cookie 配置对象\n\t */\n\tpublic SaCookieConfig getCookie() {\n\t\treturn cookie;\n\t}\n\n\t/**\n\t * @param cookie Cookie 配置对象\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setCookie(SaCookieConfig cookie) {\n\t\tthis.cookie = cookie;\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 获取：在 isConcurrent=false 时，决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线，新设备登录成功, NEW_DEVICE=新设备登录失败，旧设备维持在线)\n\t * @return /\n\t */\n\tpublic SaReplacedLoginExitMode getReplacedLoginExitMode() {\n\t\treturn replacedLoginExitMode;\n\t}\n\n\t/**\n\t * 设置：在 isConcurrent=false 时，决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线，新设备登录成功, NEW_DEVICE=新设备登录失败，旧设备维持在线)\n\t * @param replacedLoginExitMode /\n\t * @return 对象自身\n\t */\n\tpublic SaLoginParameter setReplacedLoginExitMode(SaReplacedLoginExitMode replacedLoginExitMode) {\n\t\tthis.replacedLoginExitMode = replacedLoginExitMode;\n\t\treturn this;\n\t}\n\n\t/*\n\t * toString\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaLoginParameter [\"\n\t\t\t\t+ \"deviceType=\" + deviceType\n\t\t\t\t+ \", deviceId=\" + deviceId\n\t\t\t\t+ \", replacedRange=\" + replacedRange\n\t\t\t\t+ \", replacedLoginExitMode=\" + replacedLoginExitMode\n\t\t\t\t+ \", overflowLogoutMode=\" + overflowLogoutMode\n\t\t\t\t+ \", isLastingCookie=\" + isLastingCookie\n\t\t\t\t+ \", timeout=\" + timeout\n\t\t\t\t+ \", activeTimeout=\" + activeTimeout\n\t\t\t\t+ \", isConcurrent=\" + isConcurrent\n\t\t\t\t+ \", isShare=\" + isShare\n\t\t\t\t+ \", maxLoginCount=\" + maxLoginCount\n\t\t\t\t+ \", maxTryTimes=\" + maxTryTimes\n\t\t\t\t+ \", extraData=\" + extraData\n\t\t\t\t+ \", token=\" + token\n\t\t\t\t+ \", isWriteHeader=\" + isWriteHeader\n\t\t\t\t+ \", terminalTag=\" + terminalExtraData\n\t\t\t\t+ \", rightNowCreateTokenSession=\" + rightNowCreateTokenSession\n\t\t\t\t+ \", cookie=\" + cookie\n\t\t\t\t+ \"]\";\n\t}\n\n\n\n\n\t/**\n\t * <h2> 请更换为 getDeviceType </h2>\n\t * @return 此次登录的客户端设备类型\n\t */\n\t@Deprecated\n\tpublic String getDevice() {\n\t\treturn deviceType;\n\t}\n\n\t/**\n\t * <h2> 请更换为 setDeviceType </h2>\n\t * @param device 此次登录的客户端设备类型\n\t * @return 对象自身\n\t */\n\t@Deprecated\n\tpublic SaLoginParameter setDevice(String device) {\n\t\tthis.deviceType = device;\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/SaLogoutParameter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp.parameter;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.stp.parameter.enums.SaLogoutMode;\nimport cn.dev33.satoken.stp.parameter.enums.SaLogoutRange;\n\n/**\n * 在会话注销时的 配置参数对象，决定注销时的一些细节行为 <br>\n *\n * <pre>\n *     \t// 例如：\n *     \tStpUtil.logout(10001, new SaLogoutParameter());\n * </pre>\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaLogoutParameter {\n\n\t// --------- 单独参数\n\n\t/**\n\t * 需要注销的设备类型 (为 null 代表不限制，为具体值代表只注销此设备类型的会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(id, parame) 时有效)\n\t */\n\tprivate String deviceType;\n\n\t/**\n\t * 需要注销的设备ID (为 null 代表不限制，为具体值代表只注销此设备ID的会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(id, param) 时有效)\n\t */\n\tprivate String deviceId;\n\n\t/**\n\t * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线，REPLACED=顶人下线)\n\t */\n\tprivate SaLogoutMode mode = SaLogoutMode.LOGOUT;\n\n\n\t// --------- 覆盖性参数\n\n\t/**\n\t * 注销范围 (TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(param) 时有效)\n\t */\n\tprivate SaLogoutRange range;\n\n\t/**\n\t * 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)\n\t * <br/> (此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue(\"token\", param) 时有效)\n\t */\n\tprivate Boolean isKeepFreezeOps;\n\n\t/**\n\t * 在注销 token 后，是否保留其对应的 Token-Session\n\t */\n\tprivate Boolean isKeepTokenSession;\n\n\n\t// ------ 附加方法\n\n\tpublic SaLogoutParameter() {\n\t\tthis(SaManager.getConfig());\n\t}\n\tpublic SaLogoutParameter(SaTokenConfig config) {\n\t\tsetDefaultValues(config);\n\t}\n\n\t/**\n\t * 根据 SaTokenConfig 对象初始化默认值\n\t *\n\t * @param config 使用的配置对象\n\t * @return 对象自身\n\t */\n\tpublic SaLogoutParameter setDefaultValues(SaTokenConfig config) {\n\t\tthis.range = config.getLogoutRange();\n\t\tthis.isKeepFreezeOps = config.getIsLogoutKeepFreezeOps();\n\t\tthis.isKeepTokenSession = config.getIsLogoutKeepTokenSession();\n\t\treturn this;\n\t}\n\n\t/**\n\t * 静态方法获取一个 SaLoginParameter 对象\n\t * @return SaLoginParameter 对象\n\t */\n\tpublic static SaLogoutParameter create() {\n\t\treturn new SaLogoutParameter(SaManager.getConfig());\n\t}\n\n\n\n\t// ---------------- get set\n\n\t/**\n\t * @return 在注销 token 后，是否保留其对应的 Token-Session\n\t */\n\tpublic Boolean getIsKeepTokenSession() {\n\t\treturn isKeepTokenSession;\n\t}\n\n\t/**\n\t * @param isKeepTokenSession 在注销 token 后，是否保留其对应的 Token-Session\n\t *\n\t * @return 对象自身\n\t */\n\tpublic SaLogoutParameter setIsKeepTokenSession(Boolean isKeepTokenSession) {\n\t\tthis.isKeepTokenSession = isKeepTokenSession;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)\n\t * <br/> (此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue(\"token\", param) 时有效)\n\t *\n\t * @return /\n\t */\n\tpublic Boolean getIsKeepFreezeOps() {\n\t\treturn this.isKeepFreezeOps;\n\t}\n\n\t/**\n\t * 设置 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)\n\t * <br/> (此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue(\"token\", param) 时有效)\n\t *\n\t * @param isKeepFreezeOps /\n\t * @return 对象自身\n\t */\n\tpublic SaLogoutParameter setIsKeepFreezeOps(Boolean isKeepFreezeOps) {\n\t\tthis.isKeepFreezeOps = isKeepFreezeOps;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 需要注销的设备类型 (为 null 代表不限制，为具体值代表只注销此设备类型的会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(id, parame) 时有效)\n\t *\n\t * @return deviceType /\n\t */\n\tpublic String getDeviceType() {\n\t\treturn this.deviceType;\n\t}\n\n\t/**\n\t * 需要注销的设备类型 (为 null 代表不限制，为具体值代表只注销此设备类型的会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(id, parame) 时有效)\n\t *\n\t * @param deviceType /\n\t * @return /\n\t */\n\tpublic SaLogoutParameter setDeviceType(String deviceType) {\n\t\tthis.deviceType = deviceType;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 需要注销的设备ID (为 null 代表不限制，为具体值代表只注销此设备 ID 的会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(id, parame) 时有效)\n\t *\n\t * @return /\n\t */\n\tpublic String getDeviceId() {\n\t\treturn this.deviceId;\n\t}\n\n\t/**\n\t * 需要注销的设备类型 (为 null 代表不限制，为具体值代表只注销此设备 ID 的会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(id, parame) 时有效)\n\t *\n\t * @param deviceId /\n\t * @return /\n\t */\n\tpublic SaLogoutParameter setDeviceId(String deviceId) {\n\t\tthis.deviceId = deviceId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线，REPLACED=顶人下线)\n\t *\n\t * @return logoutMode 注销类型\n\t */\n\tpublic SaLogoutMode getMode() {\n\t\treturn this.mode;\n\t}\n\n\t/**\n\t * 注销类型 (LOGOUT=注销下线、KICKOUT=踢人下线，REPLACED=顶人下线)\n\t *\n\t * @param mode 注销类型\n\t * @return /\n\t */\n\tpublic SaLogoutParameter setMode(SaLogoutMode mode) {\n\t\tthis.mode = mode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 注销范围 (TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(param) 时有效)\n\t *\n\t * @return /\n\t */\n\tpublic SaLogoutRange getRange() {\n\t\treturn this.range;\n\t}\n\n\t/**\n\t * 注销范围 (TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话)\n\t * <br/> (此参数只在调用 StpUtil.logout(param) 时有效)\n\t *\n\t * @param range /\n\t * @return /\n\t */\n\tpublic SaLogoutParameter setRange(SaLogoutRange range) {\n\t\tthis.range = range;\n\t\treturn this;\n\t}\n\n\t/*\n\t * toString\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaLoginParameter [\"\n\t\t\t\t+ \"deviceType=\" + deviceType\n\t\t\t\t+ \", deviceId=\" + deviceId\n\t\t\t\t+ \", isKeepTokenSession=\" + isKeepTokenSession\n\t\t\t\t+ \", isKeepFreezeOps=\" + isKeepFreezeOps\n\t\t\t\t+ \", mode=\" + mode\n\t\t\t\t+ \", range=\" + range\n\t\t\t\t+ \"]\";\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutMode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp.parameter.enums;\n\n/**\n * SaLogoutMode: 注销模式\n *\n * @author click33\n * @since 1.41.0\n */\npublic enum SaLogoutMode {\n\n\t/**\n\t * 注销下线\n\t */\n\tLOGOUT,\n\n\t/**\n\t * 踢人下线\n\t */\n\tKICKOUT,\n\n\t/**\n\t * 顶人下线\n\t */\n\tREPLACED;\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaLogoutRange.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp.parameter.enums;\n\n/**\n * SaLogoutMode: 注销范围\n *\n * @author click33\n * @since 1.41.0\n */\npublic enum SaLogoutRange {\n\n\t/**\n\t * token 范围：只注销提供的 token 指向的会话\n\t */\n\tTOKEN,\n\n\t/**\n\t * 账号范围：注销 token 指向的 loginId 会话\n\t */\n\tACCOUNT\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedLoginExitMode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp.parameter.enums;\n\n/**\n * 在 isConcurrent=false 时，决定新旧设备谁将放弃会话\n * @author 石泽旭\n * @since 1.44.0\n */\npublic enum SaReplacedLoginExitMode {\n\n    /**\n     * 旧设备下线，新设备登录成功\n     */\n    OLD_DEVICE,\n\n    /**\n     * 新设备登录失败，旧设备维持在线\n     */\n    NEW_DEVICE\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/stp/parameter/enums/SaReplacedRange.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.stp.parameter.enums;\n\n/**\n * 顶人下线的范围\n *\n * @author click33\n * @since 1.41.0\n */\npublic enum SaReplacedRange {\n\n\t/**\n\t * 当前指定的设备类型端\n\t */\n\tCURR_DEVICE_TYPE,\n\n\t/**\n\t * 所有设备类型端\n\t */\n\tALL_DEVICE_TYPE\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaAnnotationStrategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy;\n\nimport cn.dev33.satoken.annotation.*;\nimport cn.dev33.satoken.annotation.handler.*;\nimport cn.dev33.satoken.fun.strategy.*;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.router.SaRouter;\n\nimport java.lang.annotation.Annotation;\nimport java.util.*;\n\n/**\n * Sa-Token 注解鉴权相关策略\n *\n * @author click33\n * @since 1.39.0\n */\npublic final class SaAnnotationStrategy {\n\n\tprivate SaAnnotationStrategy() {\n\t\tregisterDefaultAnnotationHandler();\n\t}\n\n\t/**\n\t * 全局单例引用\n\t */\n\tpublic static final SaAnnotationStrategy instance = new SaAnnotationStrategy();\n\n\n\t// ----------------------- 所有策略\n\n\t/**\n\t * 注解处理器集合\n\t */\n\tpublic Map<Class<?>, SaAnnotationHandlerInterface<?>> annotationHandlerMap = new LinkedHashMap<>();\n\n\t/**\n\t * 注册所有默认的注解处理器\n\t */\n\tpublic void registerDefaultAnnotationHandler() {\n\t\tannotationHandlerMap.put(SaCheckLogin.class, new SaCheckLoginHandler());\n\t\tannotationHandlerMap.put(SaCheckRole.class, new SaCheckRoleHandler());\n\t\tannotationHandlerMap.put(SaCheckPermission.class, new SaCheckPermissionHandler());\n\t\tannotationHandlerMap.put(SaCheckSafe.class, new SaCheckSafeHandler());\n\t\tannotationHandlerMap.put(SaCheckDisable.class, new SaCheckDisableHandler());\n\t\tannotationHandlerMap.put(SaCheckHttpBasic.class, new SaCheckHttpBasicHandler());\n\t\tannotationHandlerMap.put(SaCheckHttpDigest.class, new SaCheckHttpDigestHandler());\n\t\tannotationHandlerMap.put(SaCheckOr.class, new SaCheckOrHandler());\n\t}\n\n\t/**\n\t * 注册一个注解处理器\n\t */\n\tpublic void registerAnnotationHandler(SaAnnotationHandlerInterface<?> handler) {\n\t\tannotationHandlerMap.put(handler.getHandlerAnnotationClass(), handler);\n\t\tSaTokenEventCenter.doRegisterAnnotationHandler(handler);\n\t}\n\n\t/**\n\t * 注册一个注解处理器，到首位\n\t */\n\tpublic void registerAnnotationHandlerToFirst(SaAnnotationHandlerInterface<?> handler) {\n\t\tMap<Class<?>, SaAnnotationHandlerInterface<?>> newMap = new LinkedHashMap<>();\n\t\tnewMap.put(handler.getHandlerAnnotationClass(), handler);\n\t\tnewMap.putAll(annotationHandlerMap);\n\t\tthis.annotationHandlerMap = newMap;\n\t\tSaTokenEventCenter.doRegisterAnnotationHandler(handler);\n\t}\n\n\t/**\n\t * 移除一个注解处理器\n\t */\n\tpublic void removeAnnotationHandler(Class<?> cls) {\n\t\tannotationHandlerMap.remove(cls);\n\t}\n\n\t/**\n\t * 对一个 [Method] 对象进行注解校验 （注解鉴权内部实现）\n\t */\n\tpublic SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> {\n\n\t\t// 如果 Method 或其所属 Class 上有 @SaIgnore 注解，则直接跳过整个校验过程\n\t\tif(instance.isAnnotationPresent.apply(method, SaIgnore.class)) {\n\t\t\tSaRouter.stop();\n\t\t}\n\n\t\t// 先校验 Method 所属 Class 上的注解\n\t\tinstance.checkElementAnnotation.accept(method.getDeclaringClass());\n\n\t\t// 再校验 Method 上的注解\n\t\tinstance.checkElementAnnotation.accept(method);\n\t};\n\n\t/**\n\t * 对一个 [Element] 对象进行注解校验 （注解鉴权内部实现）\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> {\n\t\t// 如果此元素上标注了 @SaCheckOr，则必须在后续判断中忽略掉其指定的 append() 类型注解判断\n\t\tList<Class<? extends Annotation>> ignoreClassList = new ArrayList<>();\n\t\tSaCheckOr checkOr = (SaCheckOr)instance.getAnnotation.apply(element, SaCheckOr.class);\n\t\tif(checkOr != null) {\n\t\t\tignoreClassList = Arrays.asList(checkOr.append());\n\t\t}\n\n\t\t// 遍历所有的注解处理器，检查此 element 是否具有这些指定的注解\n\t\tfor (Map.Entry<Class<?>, SaAnnotationHandlerInterface<?>> entry: annotationHandlerMap.entrySet()) {\n\t\t\t// 忽略掉在 @SaCheckOr 中 append 字段指定的注解\n\t\t\tClass<Annotation> atClass = (Class<Annotation>)entry.getKey();\n\t\t\tif(ignoreClassList.contains(atClass)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tAnnotation annotation = instance.getAnnotation.apply(element, atClass);\n\t\t\tif(annotation != null) {\n\t\t\t\tentry.getValue().check(annotation, element);\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * 从元素上获取注解\n\t */\n\tpublic SaGetAnnotationFunction getAnnotation = (element, annotationClass)->{\n\t\treturn element.getAnnotation(annotationClass);\n\t};\n\n\t/**\n\t * 判断一个 Method 或其所属 Class 是否包含指定注解\n\t */\n\tpublic SaIsAnnotationPresentFunction isAnnotationPresent = (method, annotationClass) -> {\n\t\treturn instance.getAnnotation.apply(method, annotationClass) != null ||\n\t\t\t\tinstance.getAnnotation.apply(method.getDeclaringClass(), annotationClass) != null;\n\t};\n\n\t/**\n\t * SaCheckELRootMap 扩展函数\n\t */\n\tpublic SaCheckELRootMapExtendFunction checkELRootMapExtendFunction = rootMap -> {\n\t\t// 默认不做任何处理\n\t};\n\n\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaFirewallStrategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.strategy.SaFirewallCheckFailHandleFunction;\nimport cn.dev33.satoken.fun.strategy.SaFirewallCheckFunction;\nimport cn.dev33.satoken.strategy.hooks.*;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Sa-Token 防火墙策略\n *\n * @author click33\n * @since 1.40.0\n */\npublic final class SaFirewallStrategy {\n\n\t/**\n\t * 全局单例引用\n\t */\n\tpublic static final SaFirewallStrategy instance = new SaFirewallStrategy();\n\n\t/**\n\t * 防火墙校验钩子函数集合\n\t */\n\tpublic List<SaFirewallCheckHook> checkHooks = new ArrayList<>();\n\n\tprivate SaFirewallStrategy() {\n\t\t// 初始化默认的防火墙校验钩子函数集合\n\t\tcheckHooks.add(SaFirewallCheckHookForWhitePath.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForBlackPath.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForPathDangerCharacter.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForPathBannedCharacter.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForDirectoryTraversal.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForHost.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForHttpMethod.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForHeader.instance);\n\t\tcheckHooks.add(SaFirewallCheckHookForParameter.instance);\n\t}\n\n\t/**\n\t * 注册一个防火墙校验 hook\n\t * @param checkHook /\n\t */\n\tpublic void registerHook(SaFirewallCheckHook checkHook) {\n\t\tSaManager.getLog().info(\"防火墙校验 hook 注册成功: \" + checkHook.getClass());\n\t\tcheckHooks.add(checkHook);\n\t}\n\n\t/**\n\t * 注册一个防火墙校验 hook 到第一位，\n\t * <b>请注意将 hook 注册到第一位将会优先于白名单的判断，如果您依然希望白名单 hook 保持最高优先级，请调用 registerHookToSecond </b>\n\t * @param checkHook /\n\t */\n\tpublic void registerHookToFirst(SaFirewallCheckHook checkHook) {\n\t\tSaManager.getLog().info(\"防火墙校验 hook 注册成功: \" + checkHook.getClass());\n\t\tcheckHooks.add(0, checkHook);\n\t}\n\n\t/**\n\t * 注册一个防火墙校验 hook 到第二位\n\t * @param checkHook /\n\t */\n\tpublic void registerHookToSecond(SaFirewallCheckHook checkHook) {\n\t\tSaManager.getLog().info(\"防火墙校验 hook 注册成功: \" + checkHook.getClass());\n\t\tcheckHooks.add(1, checkHook);\n\t}\n\n\t/**\n\t * 移除指定类型的防火墙校验 hook\n\t * @param hookClass /\n\t */\n\tpublic void removeHook(Class<? extends SaFirewallCheckHook> hookClass) {\n\t\tfor (SaFirewallCheckHook hook : checkHooks) {\n\t\t\tif (hook.getClass().equals(hookClass)) {\n\t\t\t\tcheckHooks.remove(hook);\n\t\t\t\tSaManager.getLog().info(\"防火墙校验 hook 移除成功: \" + hookClass);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 防火墙校验函数\n\t */\n\tpublic SaFirewallCheckFunction check = (req, res, extArg) -> {\n\t\tfor (SaFirewallCheckHook checkHook : checkHooks) {\n\t\t\tcheckHook.execute(req, res, extArg);\n\t\t}\n\t};\n\n\t/**\n\t * 当请求 path 校验不通过时地处理方案，自定义示例：\n\t * <pre>\n\t * \t\tSaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {\n\t * \t\t\t// 自定义处理逻辑 ...\n\t *      };\n\t * </pre>\n\t */\n\tpublic SaFirewallCheckFailHandleFunction checkFailHandle = null;\n\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.NotImplException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.fun.strategy.*;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\n\nimport java.util.UUID;\n\n/**\n * Sa-Token 策略对象\n * <p>\n * 此类统一定义框架内的一些关键性逻辑算法，方便开发者进行按需重写，例：\n * </p>\n * <pre>\n // SaStrategy全局单例，所有方法都用以下形式重写\n SaStrategy.instance.setCreateToken((loginId, loginType) -》 {\n // 自定义Token生成的算法\n return \"xxxx\";\n });\n * </pre>\n *\n * @author click33\n * @since 1.27.0\n */\npublic final class SaStrategy {\n\n\tprivate SaStrategy() {\n\t}\n\n\t/**\n\t * 获取 SaStrategy 对象的单例引用\n\t */\n\tpublic static final SaStrategy instance = new SaStrategy();\n\n\n\t// ----------------------- 所有策略\n\n\t/**\n\t * 创建 Token 的策略\n\t */\n\tpublic SaCreateTokenFunction createToken = (loginId, loginType) -> {\n\t\t// 根据配置的tokenStyle生成不同风格的token\n\t\tString tokenStyle = SaManager.getStpLogic(loginType).getConfigOrGlobal().getTokenStyle();\n\n\t\tswitch (tokenStyle) {\n\t\t\t// uuid\n\t\t\tcase SaTokenConsts.TOKEN_STYLE_UUID:\n\t\t\t\treturn UUID.randomUUID().toString();\n\n\t\t\t// 简单uuid (不带下划线)\n\t\t\tcase SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID:\n\t\t\t\treturn UUID.randomUUID().toString().replaceAll(\"-\", \"\");\n\n\t\t\t// 32位随机字符串\n\t\t\tcase SaTokenConsts.TOKEN_STYLE_RANDOM_32:\n\t\t\t\treturn SaFoxUtil.getRandomString(32);\n\n\t\t\t// 64位随机字符串\n\t\t\tcase SaTokenConsts.TOKEN_STYLE_RANDOM_64:\n\t\t\t\treturn SaFoxUtil.getRandomString(64);\n\n\t\t\t// 128位随机字符串\n\t\t\tcase SaTokenConsts.TOKEN_STYLE_RANDOM_128:\n\t\t\t\treturn SaFoxUtil.getRandomString(128);\n\n\t\t\t// tik风格 (2_14_16)\n\t\t\tcase SaTokenConsts.TOKEN_STYLE_TIK:\n\t\t\t\treturn SaFoxUtil.getRandomString(2) + \"_\" + SaFoxUtil.getRandomString(14) + \"_\" + SaFoxUtil.getRandomString(16) + \"__\";\n\n\t\t\t// 默认，还是uuid\n\t\t\tdefault:\n\t\t\t\tSaManager.getLog().warn(\"配置的 tokenStyle 值无效：{}，仅允许以下取值: \" +\n\t\t\t\t\t\t\"uuid、simple-uuid、random-32、random-64、random-128、tik\", tokenStyle);\n\t\t\t\treturn UUID.randomUUID().toString();\n\t\t}\n\t};\n\n\t/**\n\t * 创建 Session 的策略\n\t */\n\tpublic SaCreateSessionFunction createSession = (sessionId) -> {\n\t\treturn new SaSession(sessionId);\n\t};\n\n\t/**\n\t * 反序列化 SaSession 时默认指定的类型\n\t */\n\tpublic volatile Class<? extends SaSession> sessionClassType = SaSession.class;\n\n\t/**\n\t * 判断：集合中是否包含指定元素（模糊匹配）\n\t */\n\tpublic SaHasElementFunction hasElement = (list, element) -> {\n\n\t\t// 空集合直接返回false\n\t\tif(list == null || list.size() == 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 先尝试一下简单匹配，如果可以匹配成功则无需继续模糊匹配\n\t\tif (list.contains(element)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// 开始模糊匹配\n\t\tfor (String patt : list) {\n\t\t\tif(SaFoxUtil.vagueMatch(patt, element)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 走出for循环说明没有一个元素可以匹配成功\n\t\treturn false;\n\t};\n\n\t/**\n\t * 生成唯一式 token 的算法\n\t */\n\tpublic SaGenerateUniqueTokenFunction generateUniqueToken = (elementName, maxTryTimes, createTokenFunction, checkTokenFunction) -> {\n\n\t\t// 为方便叙述，以下代码注释均假设在处理生成 token 的场景，但实际上本方法也可能被用于生成 code、ticket 等\n\n\t\t// 循环生成\n\t\tfor (int i = 1; ; i++) {\n\t\t\t// 生成 token\n\t\t\tString token = createTokenFunction.get();\n\n\t\t\t// 如果 maxTryTimes == -1，表示不做唯一性验证，直接返回\n\t\t\tif (maxTryTimes == -1) {\n\t\t\t\treturn token;\n\t\t\t}\n\n\t\t\t// 如果 token 在DB库查询不到数据，说明是个可用的全新 token，直接返回\n\t\t\tif (checkTokenFunction.apply(token)) {\n\t\t\t\treturn token;\n\t\t\t}\n\n\t\t\t// 如果已经循环了 maxTryTimes 次，仍然没有创建出可用的 token，那么抛出异常\n\t\t\tif (i >= maxTryTimes) {\n\t\t\t\tthrow new SaTokenException(elementName + \" 生成失败，已尝试\" + i + \"次，生成算法过于简单或资源池已耗尽\");\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n     * 是否自动续期 active-timeout\n     */\n    public SaAutoRenewFunction autoRenew = (stpLogic) -> {\n        return stpLogic.getConfigOrGlobal().getAutoRenew();\n    };\n\n\t/**\n\t * 创建 StpLogic 的算法\n\t */\n\tpublic SaCreateStpLogicFunction createStpLogic = (loginType) -> {\n\t\treturn new StpLogic(loginType);\n\t};\n\n\t/**\n\t * 路由匹配策略\n\t */\n\tpublic SaRouteMatchFunction routeMatcher = (pattern, path) -> {\n\t\tthrow new NotImplException(\"未实现具体路由匹配策略\").setCode(SaErrorCode.CODE_12401);\n\t};\n\n\t/**\n\t * CORS 策略处理函数\n\t */\n\tpublic SaCorsHandleFunction corsHandle = (req, res, sto) -> {\n\n\t};\n\n\n\t// ----------------------- 重写策略 set连缀风格\n\n\t/**\n\t * 重写创建 Token 的策略\n\t *\n\t * @param createToken /\n\t * @return /\n\t */\n\tpublic SaStrategy setCreateToken(SaCreateTokenFunction createToken) {\n\t\tthis.createToken = createToken;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 重写创建 Session 的策略\n\t *\n\t * @param createSession /\n\t * @return /\n\t */\n\tpublic SaStrategy setCreateSession(SaCreateSessionFunction createSession) {\n\t\tthis.createSession = createSession;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 判断：集合中是否包含指定元素（模糊匹配）\n\t *\n\t * @param hasElement /\n\t * @return /\n\t */\n\tpublic SaStrategy setHasElement(SaHasElementFunction hasElement) {\n\t\tthis.hasElement = hasElement;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 生成唯一式 token 的算法\n\t *\n\t * @param generateUniqueToken /\n\t * @return /\n\t */\n\tpublic SaStrategy setGenerateUniqueToken(SaGenerateUniqueTokenFunction generateUniqueToken) {\n\t\tthis.generateUniqueToken = generateUniqueToken;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 创建 StpLogic 的算法\n\t *\n\t * @param createStpLogic /\n\t * @return /\n\t */\n\tpublic SaStrategy setCreateStpLogic(SaCreateStpLogicFunction createStpLogic) {\n\t\tthis.createStpLogic = createStpLogic;\n\t\treturn this;\n\t}\n\n\t/**\n     * 是否自动续期\n     *\n     * @param autoRenew /\n     * @return /\n     */\n    public SaStrategy setAutoRenew(SaAutoRenewFunction autoRenew) {\n        this.autoRenew = autoRenew;\n        return this;\n    }\n\n\t//\n\n\t/**\n\t * 请更换为 instance\n\t */\n\t@Deprecated\n\tpublic static final SaStrategy me = instance;\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHook.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\n\n/**\n * 防火墙策略校验钩子函数 - 接口\n *\n * @author click33\n * @since 1.41.0\n */\n@FunctionalInterface\npublic interface SaFirewallCheckHook {\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    void execute(SaRequest req, SaResponse res, Object extArg);\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForBlackPath.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.RequestPathInvalidException;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 防火墙策略校验钩子函数：请求 path 黑名单校验\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForBlackPath implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForBlackPath instance = new SaFirewallCheckHookForBlackPath();\n\n    /**\n     * 请求 path 黑名单\n     */\n    public List<String> blackPaths = new ArrayList<>();\n\n    /**\n     * 重载配置\n     * @param paths 黑名单 path 列表\n     */\n    public void resetConfig(String... paths) {\n        this.blackPaths.clear();\n        this.blackPaths.addAll(Arrays.asList(paths));\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 扩展预留参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        String requestPath = req.getRequestPath();\n        for (String item : blackPaths) {\n            if (requestPath.equals(item)) {\n                throw new RequestPathInvalidException(\"非法请求：\" + requestPath, requestPath);\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForDirectoryTraversal.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.RequestPathInvalidException;\n\n/**\n * 防火墙策略校验钩子函数：请求 path 目录遍历符检测\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForDirectoryTraversal implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForDirectoryTraversal instance = new SaFirewallCheckHookForDirectoryTraversal();\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        String requestPath = req.getRequestPath();\n        if(!isPathValid(requestPath)) {\n            throw new RequestPathInvalidException(\"非法请求：\" + requestPath, requestPath);\n        }\n    }\n\n    /**\n     * 检查路径是否有效\n     * @param path /\n     * @return /\n     */\n    public static boolean isPathValid(String path) {\n        if (path == null || path.isEmpty()) {\n            return false;\n        }\n\n        // 必须以 '/' 开头\n        if (path.charAt(0) != '/') {\n            return false;\n        }\n\n        // 特殊处理根路径 \"/\"\n        if (path.equals(\"/\")) {\n            return true;\n        }\n\n        String[] components = path.split(\"/\");\n        for (int i = 0; i < components.length; i++) {\n            String component = components[i];\n\n            // 处理空组件\n            if (component.isEmpty()) {\n                if (i == 0) {\n                    // 允许路径以 \"/\" 开头（第一个组件为空）\n                    continue;\n                } else {\n                    // 其他位置的空组件（如中间或末尾的 \"//\"）非法\n                    return false;\n                }\n            }\n\n            // 检查是否包含 \".\" 或 \"..\" 组件\n            if (component.equals(\".\") || component.equals(\"..\")) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    // 测试\n//    public static void main(String[] args) {\n//        test(\"/user/info\", true);      // 合法\n//        test(\"/user/info/.\", false);   // 末尾包含 /.\n//        test(\"/user/info/..\", false);  // 末尾包含 /..\n//        test(\"/user/info/./get\", false); // 中间包含 /./\n//        test(\"/user/info/../get\", false); // 中间包含 /../\n//        test(\"/user/info/.js\", true);  // 合法后缀\n//        test(\"/.abcdef\", true);         // 合法隐藏文件\n//        test(\"//user\", false);          // 多余斜杠\n//        test(\"/user//info\", false);     // 中间多余斜杠\n//        test(\"/\", true);               // 根目录合法\n//        test(\"user/../info\", false);    // 不以 / 开头\n//        test(\"a/b/c/..\", false);       // 不以 / 开头\n//        test(\"test/.\", false);          // 不以 / 开头\n//        test(\"\", true);                // 空路径非法\n//    }\n//\n//    private static void test(String path, boolean expected) {\n//        boolean result = isPathValid(path);\n//        System.out.printf(\"Path: %-20s Expected: %-5s Actual: %-5s %s%n\",\n//                path, expected, result, (result == expected) ? \"✓\" : \"✗\");\n//    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHeader.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.FirewallCheckException;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 防火墙策略校验钩子函数：请求头检测\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForHeader implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForHeader instance = new SaFirewallCheckHookForHeader();\n\n    /**\n     * 不允许的请求头列表\n     */\n    public List<String> notAllowHeaderNames = new ArrayList<>();\n\n    public SaFirewallCheckHookForHeader() {\n    }\n\n    /**\n     * 配置\n     * @param notAllowHeaderNames 不允许的请求头列表 (先清空原来的，再添加上新的)\n     */\n    public void resetConfig(String... notAllowHeaderNames) {\n        this.notAllowHeaderNames.clear();\n        this.notAllowHeaderNames.addAll(Arrays.asList(notAllowHeaderNames));\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        for (String headerName : notAllowHeaderNames) {\n            if(req.getHeader(headerName) != null) {\n                throw new FirewallCheckException(\"非法请求头：\" + headerName);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHost.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.FirewallCheckException;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 防火墙策略校验钩子函数：Host 检测\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForHost implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForHost instance = new SaFirewallCheckHookForHost();\n\n    /**\n     * 是否校验 host，默认关闭\n     */\n    public boolean isCheckHost = false;\n\n    /**\n     * 允许的 host 列表，允许通配符\n     */\n    public List<String> allowHosts = new ArrayList<>();\n\n    /**\n     * 重载配置\n     * @param isCheckHost 是否校验 host\n     * @param allowHosts 允许的 host 列表 (先清空原来的，再添加上新的)\n     */\n    public void resetConfig(boolean isCheckHost, String... allowHosts) {\n        this.isCheckHost = isCheckHost;\n        this.allowHosts.clear();\n        this.allowHosts.addAll(Arrays.asList(allowHosts));\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        if(isCheckHost) {\n            String host = req.getHost();\n            if( ! SaStrategy.instance.hasElement.apply(allowHosts, host) ) {\n                throw new FirewallCheckException(\"非法请求 host：\" + host);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForHttpMethod.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.FirewallCheckException;\nimport cn.dev33.satoken.router.SaHttpMethod;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 防火墙策略校验钩子函数：请求 Method 检测\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForHttpMethod implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForHttpMethod instance = new SaFirewallCheckHookForHttpMethod();\n\n    /**\n     * 是否校验 请求Method，默认开启\n     */\n    public boolean isCheckMethod = true;\n\n    /**\n     * 允许的 HttpMethod 列表\n     */\n    public List<String> allowMethods = new ArrayList<>();\n\n    public SaFirewallCheckHookForHttpMethod() {\n        // 默认允许的 HttpMethod 列表\n        allowMethods.add(SaHttpMethod.GET.name());\n        allowMethods.add(SaHttpMethod.POST.name());\n        allowMethods.add(SaHttpMethod.PUT.name());\n        allowMethods.add(SaHttpMethod.DELETE.name());\n        allowMethods.add(SaHttpMethod.HEAD.name());\n        allowMethods.add(SaHttpMethod.OPTIONS.name());\n        allowMethods.add(SaHttpMethod.PATCH.name());\n        allowMethods.add(SaHttpMethod.TRACE.name());\n        allowMethods.add(SaHttpMethod.CONNECT.name());\n    }\n\n    /**\n     * 配置\n     * @param isCheckMethod 是否校验 Method\n     * @param methods 允许的 HttpMethod 列表 (先清空原来的，再添加上新的)\n     */\n    public void resetConfig(boolean isCheckMethod, String... methods) {\n        this.isCheckMethod = isCheckMethod;\n        this.allowMethods.clear();\n        this.allowMethods.addAll(Arrays.asList(methods));\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        if(isCheckMethod) {\n            String method = req.getMethod();\n            if( ! allowMethods.contains(method) ) {\n                throw new FirewallCheckException(\"非法请求 Method：\" + method);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForParameter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.FirewallCheckException;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 防火墙策略校验钩子函数：请求参数检测\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForParameter implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForParameter instance = new SaFirewallCheckHookForParameter();\n\n    /**\n     * 不允许的请求参数列表\n     */\n    public List<String> notAllowParameterNames = new ArrayList<>();\n\n    public SaFirewallCheckHookForParameter() {\n    }\n\n    /**\n     * 配置\n     * @param notAllowParameterNames 不允许的请求参数列表 (先清空原来的，再添加上新的)\n     */\n    public void resetConfig(String... notAllowParameterNames) {\n        this.notAllowParameterNames.clear();\n        this.notAllowParameterNames.addAll(Arrays.asList(notAllowParameterNames));\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        for (String parameterName : notAllowParameterNames) {\n            if(req.getParam(parameterName) != null) {\n                throw new FirewallCheckException(\"非法请求参数：\" + parameterName);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathBannedCharacter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.RequestPathInvalidException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 防火墙策略校验钩子函数：请求 path 禁止字符校验\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForPathBannedCharacter implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForPathBannedCharacter instance = new SaFirewallCheckHookForPathBannedCharacter();\n\n    /**\n     * 是否严格禁止出现百分号字符 % （默认：否）\n     */\n    public boolean bannedPercentage = false;\n\n    /**\n     * 重载配置\n     * @param bannedPercentage 是否严格禁止出现百分号字符 % （默认：否）\n     */\n    public void resetConfig(boolean bannedPercentage) {\n        this.bannedPercentage = bannedPercentage;\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        // 非可打印 ASCII 字符检查\n        String requestPath = req.getRequestPath();\n        if(SaFoxUtil.hasNonPrintableASCII(requestPath)) {\n            throw new RequestPathInvalidException(\"请求 path 包含禁止字符：\" + requestPath, requestPath);\n        }\n        if(bannedPercentage && requestPath.contains(\"%\")) {\n            throw new RequestPathInvalidException(\"请求 path 包含禁止字符 %：\" + requestPath, requestPath);\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForPathDangerCharacter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.RequestPathInvalidException;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 防火墙策略校验钩子函数：请求 path 危险字符校验\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForPathDangerCharacter implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForPathDangerCharacter instance = new SaFirewallCheckHookForPathDangerCharacter();\n\n    /**\n     * 请求 path 不允许出现的危险字符\n     */\n    public List<String> dangerCharacter = new ArrayList<>(Arrays.asList(\n            \"//\",           // //\n            \"\\\\\",\t\t\t// \\\n            \"%2e\", \"%2E\",\t// .\n            \"%2f\", \"%2F\",\t// /\n            \"%5c\", \"%5C\",\t// \\\n            \";\", \"%3b\", \"%3B\",\t// ;    // 参考资料：https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA\n            \"%25\",\t\t\t// 空格\n            \"\\0\", \"%00\",\t// 空字符\n            \"\\n\", \"%0a\", \"%0A\",\t// 换行符\n            \"\\r\", \"%0d\", \"%0D\",\t// 回车符\n            \"\\u2028\",     // 行分隔符\n            \"\\u2029\"    // 段分隔符\n    ));\n\n    /**\n     * 重载配置\n     * @param character 危险字符列表\n     */\n    public void resetConfig(String... character) {\n        this.dangerCharacter = Arrays.asList(character);\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        String requestPath = req.getRequestPath();\n        for (String item : dangerCharacter) {\n            if (requestPath.contains(item)) {\n                throw new RequestPathInvalidException(\"非法请求：\" + requestPath, requestPath);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/strategy/hooks/SaFirewallCheckHookForWhitePath.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.strategy.hooks;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.StopMatchException;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 防火墙策略校验钩子函数：请求 path 白名单放行\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaFirewallCheckHookForWhitePath implements SaFirewallCheckHook {\n\n    /**\n     * 默认实例\n     */\n    public static SaFirewallCheckHookForWhitePath instance = new SaFirewallCheckHookForWhitePath();\n\n    /**\n     * 请求 path 白名单\n     */\n    public List<String> whitePaths = new ArrayList<>();\n\n    /**\n     * 重载配置\n     * @param paths 白名单 path 列表\n     */\n    public void resetConfig(String... paths) {\n        this.whitePaths.clear();\n        this.whitePaths.addAll(Arrays.asList(paths));\n    }\n\n    /**\n     * 执行的方法\n     *\n     * @param req 请求对象\n     * @param res 响应对象\n     * @param extArg 预留扩展参数\n     */\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        String requestPath = req.getRequestPath();\n        for (String item : whitePaths) {\n            if (requestPath.equals(item)) {\n                throw new StopMatchException();\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.temp;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.raw.SaRawSessionDelegator;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaTtlMethods;\n\nimport java.util.*;\n\n/**\n * Sa-Token 临时 token 验证模块\n *\n * <p>\n *     有效期很短的一种token，一般用于一次性接口防盗用、短时间资源访问等业务场景\n * </p>\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTempTemplate implements SaTtlMethods {\n\n\t/**\n\t *默认命名空间\n\t */\n\tpublic static final String DEFAULT_NAMESPACE = \"temp-token\";\n\n\t/**\n\t * 命名空间\n\t */\n\tpublic String namespace;\n\n\t/**\n\t * Raw Session 读写委托\n\t */\n\tpublic SaRawSessionDelegator rawSessionDelegator;\n\n\t/**\n\t * 在 raw-session 中的保存索引列表使用的 key\n\t */\n\tpublic static final String TEMP_TOKEN_MAP = \"__HD_TEMP_TOKEN_MAP\";\n\n\tpublic SaTempTemplate(){\n\t\tthis(DEFAULT_NAMESPACE);\n\t}\n\n\t/**\n\t * 实例化\n\t * @param namespace 命名空间，用于多实例隔离\n\t */\n\tpublic SaTempTemplate(String namespace){\n\t\tif(SaFoxUtil.isEmpty(namespace)) {\n\t\t\tthrow new SaTokenException(\"namespace 不能为空\");\n\t\t}\n\t\tthis.namespace = namespace;\n\t\tthis.rawSessionDelegator = new SaRawSessionDelegator(namespace);\n\t}\n\n\n\t// -------- 创建\n\n\t/**\n\t * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token，请自行在 value 拼接不同前缀)\n\t *\n\t * @param value 指定值\n\t * @param timeout 有效时间，单位：秒，-1 代表永久有效\n\t * @return 生成的 token\n\t */\n\tpublic String createToken(Object value, long timeout) {\n\t\treturn createToken(value, timeout, false);\n\t}\n\n\t/**\n\t * 为指定 业务标识、指定 value 创建一个 Token\n\t * @param value 指定值\n\t * @param timeout 有效期，单位：秒，-1 代表永久有效\n\t * @param isRecordIndex 是否记录索引，以便后续使用 value 反查 token\n\t * @return 生成的token\n\t */\n\tpublic String createToken(Object value, long timeout, boolean isRecordIndex) {\n\n\t\t// 生成 temp-token\n\t\tString tempToken = createTempTokenValue(value);\n\n\t\t// 持久化映射关系\n\t\tsaveToken(tempToken, value, timeout);\n\n\t\t// 记录索引\n\t\tif(isRecordIndex) {\n\t\t\tSaSession session = rawSessionDelegator.getSessionById(value);\n\t\t\taddTempTokenIndex(session, tempToken, timeout);\n\t\t\tadjustIndex(value, session);\n\t\t}\n\n\t\t// 返回\n\t\treturn tempToken;\n\t}\n\n\t/**\n\t * 保存 token\n\t * @param token /\n\t * @param value /\n\t * @param timeout /\n\t */\n\tpublic void saveToken(String token, Object value, long timeout) {\n\t\tString key = splicingTempTokenSaveKey(token);\n\t\tSaManager.getSaTokenDao().setObject(key, value, timeout);\n\t}\n\n\t/**\n\t * 创建一个 temp-token 值\n\t *\n\t * @return /\n\t */\n\tpublic String createTempTokenValue(Object value) {\n\t\treturn SaStrategy.instance.generateUniqueToken.execute(\n\t\t\t\t\"Temp Token\",\n\t\t\t\tSaManager.getConfig().getMaxTryTimes(),\n\t\t\t\t() -> randomTempToken(value),\n\t\t\t\t_apiKey -> _getValue(_apiKey) == null\n\t\t);\n\t}\n\n\t/**\n\t * 随机一个 temp-token\n\t *\n\t * @return /\n\t */\n\tpublic String randomTempToken(Object value) {\n\t\treturn UUID.randomUUID().toString().replace(\"-\", \"\");\n\t}\n\n\n\t// -------- 解析\n\n\t/**\n\t * 解析 Token 获取 value\n\t * @param token 指定 Token\n\t * @return /\n\t */\n\tpublic Object parseToken(String token) {\n\t\treturn _getValue(token);\n\t}\n\n\t/**\n\t * 解析 Token 获取 value，并转换为指定类型\n\t *\n\t * @param token 指定 Token\n\t * @param cs 指定类型\n\t * @param <T> 默认值的类型\n\t * @return /\n\t */\n\tpublic<T> T parseToken(String token, Class<T> cs) {\n\t\treturn parseToken(token, null, cs);\n\t}\n\n\t/**\n\t * 解析 token 获取 value，并裁剪指定前缀，然后转换为指定类型\n\t * <h2>\n\t *     请注意此方法在旧版本（<= v1.41.0） 时的三个参数为：service, token, class <br/>\n\t *     新版本三个参数为：token, cutPrefix, class <br/>\n\t *     请注意其中的逻辑变化\n\t * </h2>\n\t *\n\t * @param token 指定 Token\n\t * @param cs 指定类型\n\t * @param <T> 默认值的类型\n\t * @return /\n\t */\n\tpublic<T> T parseToken(String token, String cutPrefix, Class<T> cs) {\n\t\t// 解析值\n\t\tObject value = parseToken(token);\n\n\t\t// 如果未指定裁剪前缀，则直接返回\n\t\tif(SaFoxUtil.isEmpty(cutPrefix)) {\n\t\t\treturn SaFoxUtil.getValueByType(value, cs);\n\t\t}\n\n\t\t// 如果符合前缀则裁剪并返回，如果不符合前缀则返回 null\n\t\tcheckCutPrefixLength(cutPrefix);\n\t\tString str = SaFoxUtil.valueToString(value);\n\t\tif(str.startsWith(cutPrefix)) {\n\t\t\treturn SaFoxUtil.getValueByType(str.substring(cutPrefix.length()), cs);\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定指定 Token 的剩余有效期，单位：秒\n\t * <p> 返回值 -1 代表永久，-2 代表 token 无效\n\t *\n\t * @param token /\n\t * @return /\n\t */\n\tpublic long getTimeout(String token) {\n\t\treturn _getTimeout(token);\n\t}\n\n\n\t// -------- 删除\n\n\t/**\n\t * 删除一个 token\n\t * @param token 指定 Token\n\t */\n\tpublic void deleteToken(String token) {\n\t\t// 如果无此数据，则直接返回\n\t\tObject value = parseToken(token);\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 删除 token 本身\n\t\t_deleteToken(token);\n\n\t\t// 调整索引\n\t\tSaSession session = rawSessionDelegator.getSessionById(value, false);\n\t\tif(session != null) {\n\t\t\tdeleteTempTokenIndex(session, token);\n\t\t\tadjustIndex(value, null);\n\t\t}\n\t}\n\n\n\n\t// ------------------- 索引操作\n\n\t/**\n\t * 调整索引\n\t *\n\t * @param value 值\n\t * @param session 可填写 null，代表使用 value 现场查询\n\t * @return 调整后的索引列表\n\t */\n\tpublic Map<String, Long> adjustIndex(Object value, SaSession session) {\n\n\t\t// 未提供则现场查询\n\t\tif(session == null) {\n\t\t\tsession = rawSessionDelegator.getSessionById(value, false);\n\t\t\tif(session == null) {\n\t\t\t\treturn newTokenIndexMap();\n\t\t\t}\n\t\t}\n\n\t\t// 重新整理索引列表\n\t\tMap<String, Long>  tempTokenNewList = newTokenIndexMap();\n\t\tArrayList<Long> tempTokenTtlList = new ArrayList<>();\n\t\tMap<String, Long> tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap);\n\t\tfor (Map.Entry<String, Long> entry : tempTokenMap.entrySet()) {\n\t\t\tlong ttl = expireTimeToTtl(entry.getValue());\n\t\t\tif(ttl != SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\t\ttempTokenNewList.put(entry.getKey(), entry.getValue());\n\t\t\t\ttempTokenTtlList.add(ttl);\n\t\t\t}\n\t\t}\n\n\t\t// 有则保存，无则删除\n\t\tif( ! tempTokenNewList.isEmpty()) {\n\t\t\tsession.set(TEMP_TOKEN_MAP, tempTokenNewList);\n\t\t} else {\n\t\t\trawSessionDelegator.deleteSessionById(value);\n\t\t\treturn tempTokenNewList;\n\t\t}\n\n\t\t// 调整 SaSession TTL\n\t\tlong maxTtl = getMaxTtl(tempTokenTtlList);\n\t\tif(maxTtl != 0) {\n\t\t\tsession.updateTimeout(maxTtl);\n\t\t}\n\t\treturn tempTokenNewList;\n\t}\n\n\t/**\n\t * 获取指定 value 的 temp-token 列表记录\n\t * @param value /\n\t * @return /\n\t */\n\tpublic List<String> getTempTokenList(Object value) {\n\t\t// 先调增索引再获取，否则有可能获取到的不是最新有效数据\n\t\tMap<String, Long> tempTokenMap = adjustIndex(value, null);\n        return new ArrayList<>(tempTokenMap.keySet());\n\t}\n\n\t/**\n\t * 在 SaSession 上添加临时 temp-token 索引\n\t * @param session /\n\t * @param token /\n\t * @param timeout /\n\t */\n\tprotected void addTempTokenIndex(SaSession session, String token, long timeout) {\n\t\tMap<String, Long> tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap);\n\t\tif(! tempTokenMap.containsKey(token)) {\n\t\t\ttempTokenMap.put(token, ttlToExpireTime(timeout));\n\t\t\tsession.set(TEMP_TOKEN_MAP, tempTokenMap);\n\t\t}\n\t}\n\n\t/**\n\t * 在 SaSession 上删除临时 temp-token 索引\n\t * @param session /\n\t * @param token /\n\t */\n\tprotected void deleteTempTokenIndex(SaSession session, String token) {\n\t\tMap<String, Long> tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTokenIndexMap);\n\t\tif(tempTokenMap.containsKey(token)) {\n\t\t\ttempTokenMap.remove(token);\n\t\t\tsession.set(TEMP_TOKEN_MAP, tempTokenMap);\n\t\t}\n\t}\n\n\n\t// -------- 元操作\n\n\tprotected Object _getValue(String token) {\n\t\tString key = splicingTempTokenSaveKey(token);\n\t\treturn SaManager.getSaTokenDao().getObject(key);\n\t}\n\tprotected void _deleteToken(String token) {\n\t\tString key = splicingTempTokenSaveKey(token);\n\t\tSaManager.getSaTokenDao().deleteObject(key);\n\t}\n\tprotected long _getTimeout(String token) {\n\t\tString key = splicingTempTokenSaveKey(token);\n\t\treturn SaManager.getSaTokenDao().getObjectTimeout(key);\n\t}\n\n\n\n\t// -------- 其它\n\n\t/**\n\t * 检查裁剪前缀长度\n\t * @param cutPrefix /\n\t */\n\tprotected static void checkCutPrefixLength(String cutPrefix) {\n\t\tif(cutPrefix.length() >= 32) {\n\t\t\tthrow new SaTokenException(\"裁剪前缀长度必须小于 32 位\");\n\t\t}\n\t}\n\n\t/**\n\t * 获取：在存储临时 token 数据时，应该使用的 key\n\t * @param token token值\n\t * @return key\n\t */\n\tpublic String splicingTempTokenSaveKey(String token) {\n\t\treturn SaManager.getConfig().getTokenName() + \":\" + namespace + \":\" + token;\n\t}\n\n\t/**\n\t * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效)\n\t */\n\tpublic String getJwtSecretKey() {\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.temp;\n\nimport cn.dev33.satoken.SaManager;\n\nimport java.util.List;\n\n/**\n * Sa-Token 临时 token 验证模块 - 工具类\n *\n * <p>\n *     有效期很短的一种token，一般用于一次性接口防盗用、短时间资源访问等业务场景\n * </p>\n *\n * @author click33\n * @since 1.20.0\n */\npublic class SaTempUtil {\n\n\tprivate SaTempUtil() {\n\t}\n\n\t// -------- 创建\n\n\t/**\n\t * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token，请自行在 value 拼接不同前缀)\n\t *\n\t * @param value 指定值\n\t * @param timeout 有效时间，单位：秒，-1 代表永久有效\n\t * @return 生成的 token\n\t */\n\tpublic static String createToken(Object value, long timeout) {\n\t\treturn SaManager.getSaTempTemplate().createToken(value, timeout);\n\t}\n\n\t/**\n\t * 为指定 业务标识、指定 value 创建一个 Token\n\t * @param value 指定值\n\t * @param timeout 有效期，单位：秒，-1 代表永久有效\n\t * @param isRecordIndex 是否记录索引，以便后续使用 value 反查 token\n\t * @return 生成的token\n\t */\n\tpublic static String createToken(Object value, long timeout, boolean isRecordIndex) {\n\t\treturn SaManager.getSaTempTemplate().createToken(value, timeout, isRecordIndex);\n\t}\n\n\t/**\n\t * 保存 token\n\t * @param token /\n\t * @param value /\n\t * @param timeout /\n\t */\n\tpublic static void saveToken(String token, Object value, long timeout) {\n\t\tSaManager.getSaTempTemplate().saveToken(token, value, timeout);\n\t}\n\n\t// -------- 解析\n\n\t/**\n\t * 解析 Token 获取 value\n\t * @param token 指定 Token\n\t * @return /\n\t */\n\tpublic static Object parseToken(String token) {\n\t\treturn SaManager.getSaTempTemplate().parseToken(token);\n\t}\n\n\t/**\n\t * 解析 Token 获取 value，并转换为指定类型\n\t *\n\t * @param token 指定 Token\n\t * @param cs 指定类型\n\t * @param <T> 默认值的类型\n\t * @return /\n\t */\n\tpublic static<T> T parseToken(String token, Class<T> cs) {\n\t\treturn SaManager.getSaTempTemplate().parseToken(token, cs);\n\t}\n\n\t/**\n\t * 解析 token 获取 value，并裁剪指定前缀，然后转换为指定类型\n\t * <h2>\n\t *     请注意此方法在旧版本（<= v1.41.0） 时的三个参数为：service, token, class <br/>\n\t *     新版本三个参数为：token, cutPrefix, class <br/>\n\t *     请注意其中的逻辑变化\n\t * </h2>\n\t *\n\t * @param token 指定 Token\n\t * @param cs 指定类型\n\t * @param <T> 默认值的类型\n\t * @return /\n\t */\n\tpublic static<T> T parseToken(String token, String cutPrefix, Class<T> cs) {\n\t\treturn SaManager.getSaTempTemplate().parseToken(token, cutPrefix, cs);\n\t}\n\n\t/**\n\t * 获取指定指定 Token 的剩余有效期，单位：秒\n\t * <p> 返回值 -1 代表永久，-2 代表 token 无效\n\t *\n\t * @param token /\n\t * @return /\n\t */\n\tpublic static long getTimeout(String token) {\n\t\treturn SaManager.getSaTempTemplate().getTimeout(token);\n\t}\n\n\n\t// -------- 删除\n\n\t/**\n\t * 删除一个 token\n\t * @param token 指定 Token\n\t */\n\tpublic static void deleteToken(String token) {\n\t\tSaManager.getSaTempTemplate().deleteToken(token);\n\t}\n\n\n\t// ------------------- 索引操作\n\n\t/**\n\t * 获取指定 value 的 temp-token 列表记录\n\t * @param value /\n\t * @return /\n\t */\n\tpublic static List<String> getTempTokenList(Object value) {\n\t\treturn SaManager.getSaTempTemplate().getTempTokenList(value);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\n\nimport java.io.Console;\nimport java.io.UnsupportedEncodingException;\nimport java.lang.reflect.Field;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.text.SimpleDateFormat;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * Sa-Token 内部工具类\n *\n * @author click33\n * @since 1.18.0\n */\npublic class SaFoxUtil {\n\n\tprivate SaFoxUtil() {\n\t}\n\n\t/**\n\t * 打印 Sa-Token 版本字符画\n\t */\n\tpublic static void printSaToken() {\n\t\tString str = \"\"\n\t\t\t\t+ \"____ ____    ___ ____ _  _ ____ _  _ \\r\\n\"\n\t\t\t\t+ \"[__  |__| __  |  |  | |_/  |___ |\\\\ | \\r\\n\"\n\t\t\t\t+ \"___] |  |     |  |__| | \\\\_ |___ | \\\\| \"\n//\t\t\t\t+ SaTokenConsts.VERSION_NO\n//\t\t\t\t+ \"sa-token：\"\n//\t\t\t\t+ \"\\r\\n\" + \"DevDoc：\" + SaTokenConsts.DEV_DOC_URL // + \"\\r\\n\";\n\t\t\t\t+ \"\\r\\n\" + SaTokenConsts.DEV_DOC_URL // + \"\\r\\n\";\n\t\t\t\t+ \" (\" + SaTokenConsts.VERSION_NO + \")\"\n//\t\t\t\t+ \"\\r\\n\" + \"GitHub：\" + SaTokenConsts.GITHUB_URL // + \"\\r\\n\";\n\t\t\t\t;\n\t\tSystem.out.println(str);\n\t}\n\n\t/**\n\t * 生成指定长度的随机字符串\n\t *\n\t * @param length 字符串的长度\n\t * @return 一个随机字符串\n\t */\n\tpublic static String getRandomString(int length) {\n\t\tString str = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tint number = ThreadLocalRandom.current().nextInt(62);\n\t\t\tsb.append(str.charAt(number));\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 生成指定区间的 int 值\n\t *\n\t * @param min 最小值（包括）\n\t * @param max 最大值（包括）\n\t * @return /\n\t */\n\tpublic static int getRandomNumber(int min, int max) {\n\t\treturn ThreadLocalRandom.current().nextInt(min, max + 1);\n\t}\n\n\t/**\n\t * 指定元素是否为null或者空字符串\n\t * @param str 指定元素\n\t * @return 是否为null或者空字符串\n\t */\n\tpublic static boolean isEmpty(Object str) {\n\t\treturn str == null || \"\".equals(str);\n\t}\n\n\t/**\n\t * 指定元素是否不为 (null或者空字符串)\n\t * @param str 指定元素\n\t * @return 是否为null或者空字符串\n\t */\n\tpublic static boolean isNotEmpty(Object str) {\n\t\treturn ! isEmpty(str);\n\t}\n\n\t/**\n\t * 指定数组是否为null或者空数组\n\t * <h3> 该方法已过时，建议使用 isEmptyArray 方法 </h3>\n\t * @param <T> /\n\t * @param array /\n\t * @return /\n\t */\n\t@Deprecated\n\tpublic static <T> boolean isEmpty(T[] array) {\n\t\treturn isEmptyArray(array);\n\t}\n\n\t/**\n\t * 指定数组是否为null或者空数组\n\t * @param <T> /\n\t * @param array /\n\t * @return /\n\t */\n\tpublic static <T> boolean isEmptyArray(T[] array) {\n\t\treturn array == null || array.length == 0;\n\t}\n\n\t/**\n\t * 指定集合是否为null或者空数组\n\t * @param list /\n\t * @return /\n\t */\n\tpublic static boolean isEmptyList(List<?> list) {\n\t\treturn list == null || list.isEmpty();\n\t}\n\n\t/**\n\t * 比较两个对象是否相等\n\t * @param a 第一个对象\n\t * @param b 第二个对象\n\t * @return 两个对象是否相等\n\t */\n\tpublic static boolean equals(Object a, Object b) {\n        return (a == b) || (a != null && a.equals(b));\n    }\n\t\n\t/**\n\t * 比较两个对象是否不相等 \n\t * @param a 第一个对象 \n\t * @param b 第二个对象 \n\t * @return 两个对象是否不相等 \n\t */\n\tpublic static boolean notEquals(Object a, Object b) {\n        return !equals(a, b);\n    }\n\t\t/**\n\t * 以当前时间戳和随机int数字拼接一个随机字符串\n\t *\n\t * @return 随机字符串\n\t */\n\tpublic static String getMarking28() {\n\t\treturn System.currentTimeMillis() + \"\" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);\n\t}\n\n\t/**\n\t * 将日期格式化 （yyyy-MM-dd HH:mm:ss）\n\t * @param date 日期\n\t * @return 格式化后的时间\n\t */\n\tpublic static String formatDate(Date date){\n\t\treturn new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(date);\n\t}\n\n\t/**\n\t * 将日期格式化 （yyyy-MM-dd HH:mm:ss）\n\t * @param zonedDateTime 日期\n\t * @return 格式化后的时间\n\t */\n\tpublic static String formatDate(ZonedDateTime zonedDateTime) {\n\t\treturn zonedDateTime.format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\"));\n\t}\n\n\t/**\n\t * 指定毫秒后的时间（格式化 ：yyyy-MM-dd HH:mm:ss）\n\t * @param ms 指定毫秒后\n\t * @return 格式化后的时间\n\t */\n\tpublic static String formatAfterDate(long ms) {\n\t\tInstant instant = Instant.ofEpochMilli(System.currentTimeMillis() + ms);\n\t\tZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());\n\t\treturn formatDate(zonedDateTime);\n\t}\n\n\t/**\n\t * 从集合里查询数据\n\t *\n\t * @param dataList 数据集合\n\t * @param prefix   前缀\n\t * @param keyword  关键字\n\t * @param start    起始位置 (-1代表查询所有)\n\t * @param size     获取条数\n\t * @param sortType     排序类型（true=正序，false=反序）\n\t *\n\t * @return 符合条件的新数据集合\n\t */\n\tpublic static List<String> searchList(Collection<String> dataList, String prefix, String keyword, int start, int size, boolean sortType) {\n\t\tif (prefix == null) {\n\t\t\tprefix = \"\";\n\t\t}\n\t\tif (keyword == null) {\n\t\t\tkeyword = \"\";\n\t\t}\n\t\t// 挑选出所有符合条件的\n\t\tList<String> list = new ArrayList<>();\n\t\tfor (String key : dataList) {\n\t\t\tif (key.startsWith(prefix) && key.contains(keyword)) {\n\t\t\t\tlist.add(key);\n\t\t\t}\n\t\t}\n\t\t// 取指定段数据\n\t\treturn searchList(list, start, size, sortType);\n\t}\n\n\t/**\n\t * 从集合里查询数据\n\t *\n\t * @param list  数据集合\n\t * @param start 起始位置\n\t * @param size  获取条数 (-1代表从start处一直取到末尾)\n\t * @param sortType     排序类型（true=正序，false=反序）\n\t *\n\t * @return 符合条件的新数据集合\n\t */\n\tpublic static List<String> searchList(List<String> list, int start, int size, boolean sortType) {\n\t\t// 如果是反序的话\n\t\tif( ! sortType) {\n\t\t\tCollections.reverse(list);\n\t\t}\n\t\t// start 至少为0\n\t\tif (start < 0) {\n\t\t\tstart = 0;\n\t\t}\n\t\t// size为-1时，代表一直取到末尾，否则取到 start + size\n\t\tint end;\n\t\tif(size == -1) {\n\t\t\tend = list.size();\n\t\t} else {\n\t\t\tend = start + size;\n\t\t}\n\t\t// 取出的数据放到新集合中\n\t\tList<String> list2 = new ArrayList<>();\n\t\tfor (int i = start; i < end; i++) {\n\t\t\t// 如果已经取到list的末尾，则直接退出\n\t\t\tif (i >= list.size()) {\n\t\t\t\treturn list2;\n\t\t\t}\n\t\t\tlist2.add(list.get(i));\n\t\t}\n\t\treturn list2;\n\t}\n\n\t/**\n\t * 字符串模糊匹配\n\t * <p>example:\n\t * <p> user* user-add   --  true\n\t * <p> user* art-add    --  false\n\t * @param patt 表达式\n\t * @param str 待匹配的字符串\n\t * @return 是否可以匹配\n\t */\n\tpublic static boolean vagueMatch(String patt, String str) {\n\t\t// 两者均为 null 时，直接返回 true\n\t\tif(patt == null && str == null) {\n\t\t\treturn true;\n\t\t}\n\t\t// 两者其一为 null 时，直接返回 false\n\t\tif(patt == null || str == null) {\n\t\t\treturn false;\n\t\t}\n\t\t// 如果表达式不带有*号，则只需简单equals即可 (这样可以使速度提升200倍左右)\n\t\tif( ! patt.contains(\"*\")) {\n\t\t\treturn patt.equals(str);\n\t\t}\n\t\t// 深入匹配\n\t\treturn vagueMatchMethod(patt, str);\n\t}\n\n\t/**\n\t * 字符串模糊匹配\n\t *\n\t * @param pattern /\n\t * @param str    /\n\t * @return /\n\t */\n\tprivate static boolean vagueMatchMethod( String pattern, String str) {\n\t\tint m = str.length();\n\t\tint n = pattern.length();\n\t\tboolean[][] dp = new boolean[m + 1][n + 1];\n\t\tdp[0][0] = true;\n\t\tfor (int i = 1; i <= n; ++i) {\n\t\t\tif (pattern.charAt(i - 1) == '*') {\n\t\t\t\tdp[0][i] = true;\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tfor (int i = 1; i <= m; ++i) {\n\t\t\tfor (int j = 1; j <= n; ++j) {\n\t\t\t\tif (pattern.charAt(j - 1) == '*') {\n\t\t\t\t\tdp[i][j] = dp[i][j - 1] || dp[i - 1][j];\n\t\t\t\t} else if (str.charAt(i - 1) == pattern.charAt(j - 1)) {\n\t\t\t\t\tdp[i][j] = dp[i - 1][j - 1];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn dp[m][n];\n\t}\n\n\t/**\n\t * 判断类型是否为8大包装类型\n\t * @param cs /\n\t * @return /\n\t */\n\tpublic static boolean isWrapperType(Class<?> cs) {\n\t\treturn cs == Integer.class || cs == Short.class ||  cs == Long.class ||  cs == Byte.class\n\t\t\t|| cs == Float.class || cs == Double.class ||  cs == Boolean.class ||  cs == Character.class;\n\t}\n\n\t/**\n\t * 判断类型是否为基础类型：8大基本数据类型、8大包装类、String\n\t * @param cs /\n\t * @return /\n\t */\n\tpublic static boolean isBasicType(Class<?> cs) {\n\t\treturn cs.isPrimitive() || isWrapperType(cs) || cs == String.class;\n\t}\n\n\t/**\n\t * 将指定值转化为指定类型\n\t * @param <T> 泛型\n\t * @param obj 值\n\t * @param cs 类型\n\t * @return 转换后的值\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getValueByType(Object obj, Class<T> cs) {\n\t\t// 如果 obj 为 null 或者本来就是 cs 类型\n\t\tif(obj == null || obj.getClass().equals(cs)) {\n\t\t\treturn (T)obj;\n\t\t}\n\t\t// 开始转换\n\t\tString obj2 = String.valueOf(obj);\n\t\tObject obj3;\n\t\tif (cs.equals(String.class)) {\n\t\t\tobj3 = obj2;\n\t\t} else if (cs.equals(int.class) || cs.equals(Integer.class)) {\n\t\t\tobj3 = Integer.valueOf(obj2);\n\t\t} else if (cs.equals(long.class) || cs.equals(Long.class)) {\n\t\t\tobj3 = Long.valueOf(obj2);\n\t\t} else if (cs.equals(short.class) || cs.equals(Short.class)) {\n\t\t\tobj3 = Short.valueOf(obj2);\n\t\t} else if (cs.equals(byte.class) || cs.equals(Byte.class)) {\n\t\t\tobj3 = Byte.valueOf(obj2);\n\t\t} else if (cs.equals(float.class) || cs.equals(Float.class)) {\n\t\t\tobj3 = Float.valueOf(obj2);\n\t\t} else if (cs.equals(double.class) || cs.equals(Double.class)) {\n\t\t\tobj3 = Double.valueOf(obj2);\n\t\t} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {\n\t\t\tobj3 = Boolean.valueOf(obj2);\n\t\t} else if (cs.equals(char.class) || cs.equals(Character.class)) {\n\t\t\tobj3 = obj2.charAt(0);\n\t\t} else {\n\t\t\tobj3 = obj;\n\t\t}\n\t\treturn (T)obj3;\n\t}\n\n\t/**\n\t * 将 Map 转化为 Object\n\t * @param map /\n\t * @param clazz /\n\t * @return /\n\t * @param <T> /\n\t */\n\tpublic static <T> T mapToObject(Map<String, Object> map, Class<T> clazz) {\n\t\tif(map == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif(clazz == Map.class) {\n\t\t\treturn (T) map;\n\t\t}\n\t\ttry {\n\t\t\tT obj = clazz.getDeclaredConstructor().newInstance();\n\t\t\tfor (Field field : clazz.getDeclaredFields()) {\n\t\t\t\tString fieldName = field.getName();\n\t\t\t\tif (map.containsKey(fieldName)) {\n\t\t\t\t\tfield.setAccessible(true);\n\t\t\t\t\tfield.set(obj, map.get(fieldName));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn obj;\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(\"转换失败: \" + e.getMessage(), e);\n\t\t}\n\t}\n\n\n\t/**\n\t * 在url上拼接上kv参数并返回\n\t * @param url url\n\t * @param paramStr 参数, 例如 id=1001\n\t * @return 拼接后的url字符串\n\t */\n\tpublic static String joinParam(String url, String paramStr) {\n\t\t// 如果参数为空, 直接返回\n\t\tif(paramStr == null || paramStr.length() == 0) {\n\t\t\treturn url;\n\t\t}\n\t\tif(url == null) {\n\t\t\turl = \"\";\n\t\t}\n\t\tint index = url.lastIndexOf('?');\n\t\t// ? 不存在\n\t\tif(index == -1) {\n\t\t\treturn url + '?' + paramStr;\n\t\t}\n\t\t// ? 是最后一位\n\t\tif(index == url.length() - 1) {\n\t\t\treturn url + paramStr;\n\t\t}\n\t\t// ? 是其中一位\n\t\tif(index < url.length() - 1) {\n\t\t\tString separatorChar = \"&\";\n\t\t\t// 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 &\n\t\t\tif(url.lastIndexOf(separatorChar) != url.length() - 1 && paramStr.indexOf(separatorChar) != 0) {\n\t\t\t\treturn url + separatorChar + paramStr;\n\t\t\t} else {\n\t\t\t\treturn url + paramStr;\n\t\t\t}\n\t\t}\n\t\t// 正常情况下, 代码不可能执行到此\n\t\treturn url;\n\t}\n\n\t/**\n\t * 在url上拼接上kv参数并返回\n\t * @param url url\n\t * @param key 参数名称\n\t * @param value 参数值\n\t * @return 拼接后的url字符串\n\t */\n\tpublic static String joinParam(String url, String key, Object value) {\n\t\t// 如果url或者key为空, 直接返回\n\t\tif(isEmpty(url) || isEmpty(key)) {\n\t\t\treturn url;\n\t\t}\n\t\treturn joinParam(url, key + \"=\" + value);\n\t}\n\n\t/**\n\t * 在url上拼接锚参数\n\t * @param url url\n\t * @param paramStr 参数, 例如 id=1001\n\t * @return 拼接后的url字符串\n\t */\n\tpublic static String joinSharpParam(String url, String paramStr) {\n\t\t// 如果参数为空, 直接返回\n\t\tif(paramStr == null || paramStr.length() == 0) {\n\t\t\treturn url;\n\t\t}\n\t\tif(url == null) {\n\t\t\turl = \"\";\n\t\t}\n\t\tint index = url.lastIndexOf('#');\n\t\t// # 不存在\n\t\tif(index == -1) {\n\t\t\treturn url + '#' + paramStr;\n\t\t}\n\t\t// # 是最后一位\n\t\tif(index == url.length() - 1) {\n\t\t\treturn url + paramStr;\n\t\t}\n\t\t// # 是其中一位\n\t\tif(index < url.length() - 1) {\n\t\t\tString separatorChar = \"&\";\n\t\t\t// 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 &\n\t\t\tif(url.lastIndexOf(separatorChar) != url.length() - 1 && paramStr.indexOf(separatorChar) != 0) {\n\t\t\t\treturn url + separatorChar + paramStr;\n\t\t\t} else {\n\t\t\t\treturn url + paramStr;\n\t\t\t}\n\t\t}\n\t\t// 正常情况下, 代码不可能执行到此\n\t\treturn url;\n\t}\n\n\t/**\n\t * 在url上拼接锚参数\n\t * @param url url\n\t * @param key 参数名称\n\t * @param value 参数值\n\t * @return 拼接后的url字符串\n\t */\n\tpublic static String joinSharpParam(String url, String key, Object value) {\n\t\t// 如果url或者key为空, 直接返回\n\t\tif(isEmpty(url) || isEmpty(key)) {\n\t\t\treturn url;\n\t\t}\n\t\treturn joinSharpParam(url, key + \"=\" + value);\n\t}\n\n\t/**\n\t * 拼接两个url\n\t * <p> 例如：url1=http://domain.cn，url2=/sso/auth，则返回：http://domain.cn/sso/auth </p>\n\t *\n\t * @param url1 第一个url\n\t * @param url2 第二个url\n\t * @return 拼接完成的url\n\t */\n\tpublic static String spliceTwoUrl(String url1, String url2) {\n\t\t// q1、任意一个为空，则直接返回另一个\n\t\tif(url1 == null) {\n\t\t\treturn url2;\n\t\t}\n\t\tif(url2 == null) {\n\t\t\treturn url1;\n\t\t}\n\n\t\t// q2、如果 url2 以 http 开头，将其视为一个完整地址\n\t\tif(url2.startsWith(\"http\")) {\n\t\t\treturn url2;\n\t\t}\n\n\t\t// q3、将两个地址拼接在一起\n\t\treturn url1 + url2;\n\t}\n\n\t/**\n\t * 将数组的所有元素使用逗号拼接在一起\n\t * @param arr 数组\n\t * @return 字符串，例: a,b,c\n\t */\n\tpublic static String arrayJoin(String[] arr) {\n\t\tif(arr == null) {\n\t\t\treturn \"\";\n\t\t}\n\t\tStringBuilder str = new StringBuilder();\n\t\tfor (int i = 0; i < arr.length; i++) {\n\t\t\tstr.append(arr[i]);\n\t\t\tif(i != arr.length - 1) {\n\t\t\t\tstr.append(\",\");\n\t\t\t}\n\t\t}\n\t\treturn str.toString();\n\t}\n\n\t/**\n\t * 验证URL的正则表达式\n\t */\n\tpublic static String URL_REGEX = \"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\";\n\n\t/**\n\t * 使用正则表达式判断一个字符串是否为URL\n\t * @param str 字符串\n\t * @return 拼接后的url字符串\n\t */\n\tpublic static boolean isUrl(String str) {\n\t\tif(isEmpty(str)) {\n\t\t\treturn false;\n\t\t}\n        return str.toLowerCase().matches(URL_REGEX);\n\t}\n\n\t/**\n\t * URL编码\n\t * @param url see note\n\t * @return see note\n\t */\n\tpublic static String encodeUrl(String url) {\n\t\ttry {\n\t\t\treturn URLEncoder.encode(url, \"UTF-8\");\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12103);\n\t\t}\n\t}\n\n\t/**\n\t * URL解码\n\t * @param url see note\n\t * @return see note\n\t */\n\tpublic static String decoderUrl(String url) {\n\t\ttry {\n\t\t\treturn URLDecoder.decode(url, \"UTF-8\");\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaErrorCode.CODE_12104);\n\t\t}\n\t}\n\n\t/**\n\t * 将指定字符串按照逗号分隔符转化为字符串集合\n\t * @param str 字符串\n\t * @return 分割后的字符串集合\n\t */\n\tpublic static List<String> convertStringToList(String str) {\n\t\tList<String> list = new ArrayList<>();\n\t\tif(isEmpty(str)) {\n\t\t\treturn list;\n\t\t}\n\t\tString[] arr = str.split(\",\");\n\t\tfor (String s : arr) {\n\t\t\ts = s.trim();\n\t\t\tif(!isEmpty(s)) {\n\t\t\t\tlist.add(s);\n\t\t\t}\n\t\t}\n\t\treturn list;\n\t}\n\n\t/**\n\t * 将指定集合按照逗号连接成一个字符串\n\t * @param list 集合\n\t * @return 字符串\n\t */\n\tpublic static String convertListToString(List<?> list) {\n\t\tif(list == null || list.isEmpty()) {\n\t\t\treturn \"\";\n\t\t}\n\t\tStringBuilder str = new StringBuilder();\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tstr.append(list.get(i));\n\t\t\tif(i != list.size() - 1) {\n\t\t\t\tstr.append(\",\");\n\t\t\t}\n\t\t}\n\t\treturn str.toString();\n\t}\n\n    /**\n     * String 转 Array，按照逗号切割\n     * @param str 字符串\n     * @return 数组\n     */\n    public static String[] convertStringToArray(String str) {\n    \tList<String> list = convertStringToList(str);\n    \treturn list.toArray(new String[0]);\n    }\n\n    /**\n     * Array 转 String，按照逗号连接\n     * @param arr 数组\n     * @return 字符串\n     */\n    public static String convertArrayToString(String[] arr) {\n    \tif(arr == null || arr.length == 0) {\n    \t\treturn \"\";\n    \t}\n    \treturn String.join(\",\", arr);\n    }\n\n    /**\n     * 返回一个空集合\n     * @param <T> 集合类型\n     * @return 空集合\n     */\n    public static <T>List<T> emptyList() {\n    \treturn new ArrayList<>();\n    }\n\n    /**\n     * String数组转集合\n     * @param str String数组\n     * @return 集合\n     */\n    public static List<String> toList(String... str) {\n\t\treturn new ArrayList<>(Arrays.asList(str));\n    }\n\n\t/**\n\t * String 集合转数组\n\t * @param list 集合\n\t * @return 数组\n\t */\n\tpublic static String[] toArray(List<String> list) {\n\t\treturn list.toArray(new String[0]);\n\t}\n\n    public static List<String> logLevelList = Arrays.asList(\"\", \"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\");\n\n    /**\n     * 将日志等级从 String 格式转化为 int 格式\n     * @param level /\n     * @return /\n     */\n    public static int translateLogLevelToInt(String level) {\n    \tint levelInt = logLevelList.indexOf(level);\n    \tif(levelInt <= 0 || levelInt >= logLevelList.size()) {\n    \t\tlevelInt = 1;\n    \t}\n    \treturn levelInt;\n    }\n\n    /**\n     * 将日志等级从 String 格式转化为 int 格式\n     * @param level /\n     * @return /\n     */\n    public static String translateLogLevelToString(int level) {\n    \tif(level <= 0 || level >= logLevelList.size()) {\n    \t\tlevel = 1;\n    \t}\n    \treturn logLevelList.get(level);\n    }\n\n\t/**\n\t * 判断当前系统是否可以打印彩色日志，判断准确率并非100%，但基本可以满足大部分场景\n\t * @return /\n\t */\n\t@SuppressWarnings(\"all\")\n\tpublic static boolean isCanColorLog() {\n\n\t\t// 获取当前环境相关信息\n\t\tConsole console = System.console();\n\t\tString term = System.getenv().get(\"TERM\");\n\n\t\t// 两者均为 null，一般是在 eclipse、idea 等 IDE 环境下运行的，此时可以打印彩色日志\n\t\tif(console == null && term == null) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// 两者均不为 null，一般是在 linux 环境下控制台运行的，此时可以打印彩色日志\n\t\tif(console != null && term != null) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// console 有值，term 为 null，一般是在 windows 的 cmd 控制台运行的，此时不可以打印彩色日志\n\t\tif(console != null && term == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// console 为 null，term 有值，一般是在 linux 的 nohup 命令运行的，此时不可以打印彩色日志\n\t\t// 此时也有可能是在 windows 的 git bash 环境下运行的，此时可以打印彩色日志，但此场景无法和上述场景区分，所以统一不打印彩色日志\n\t\tif(console == null && term != null) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 正常情况下，代码不会走到这里，但是方法又必须要有返回值，所以随便返回一个\n\t\treturn false;\n\t}\n\n\t/**\n\t * list1 是否完全包含 list2 中所有元素\n\t * @param list1 集合1\n\t * @param list2 集合2\n\t * @return /\n\t */\n\tpublic static boolean list1ContainList2AllElement(List<String> list1, List<String> list2){\n\t\tif(list2 == null || list2.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\t\tif(list1 == null || list1.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (String str : list2) {\n\t\t\tif(!list1.contains(str)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * list1 是否包含 list2 中任意一个元素\n\t * @param list1 集合1\n\t * @param list2 集合2\n\t * @return /\n\t */\n\tpublic static boolean list1ContainList2AnyElement(List<String> list1, List<String> list2){\n\t\tif(list1 == null || list1.isEmpty() || list2 == null || list2.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (String str : list2) {\n\t\t\tif(list1.contains(str)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 从 list1 中剔除 list2 所包含的元素 （克隆副本操作，不影响 list1）\n\t * @param list1 集合1\n\t * @param list2 集合2\n\t * @return /\n\t */\n\tpublic static List<String> list1RemoveByList2(List<String> list1, List<String> list2){\n\t\tif(list1 == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif(list1.isEmpty() || list2 == null || list2.isEmpty()) {\n\t\t\treturn new ArrayList<>(list1);\n\t\t}\n\t\tList<String> listX = new ArrayList<>(list1);\n\t\tfor (String str : list2) {\n\t\t\tlistX.remove(str);\n\t\t}\n\t\treturn listX;\n\t}\n\n\t/**\n\t * 检查字符串是否包含非可打印 ASCII 字符\n\t * @param str /\n\t * @return /\n\t */\n\tpublic static boolean hasNonPrintableASCII(String str) {\n\t\tif (str == null) {\n\t\t\treturn false;\n\t\t}\n\t\tfor (int i = 0; i < str.length(); i++) {\n\t\t\tchar c = str.charAt(i);\n\t\t\t// ASCII 范围检查：0-31 或 127\n\t\t\tif ((c <= 31) || (c == 127)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * 将 value 转化为 String，如果 value 为 null，则返回空字符串\n\t * @param value /\n\t * @return /\n\t */\n\tpublic static String valueToString(Object value) {\n\t\tif (value == null) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn value.toString();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/SaHexUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\n/**\n * 十六进制工具类\n *\n * @author deepseek\n * @since 2025/2/24\n */\npublic class SaHexUtil {\n\n    // 十六进制字符表（大写）\n    private static final char[] HEX_ARRAY = \"0123456789ABCDEF\".toCharArray();\n\n    /**\n     * 将字节数组转换为十六进制字符串（JDK8兼容）\n     * @param bytes 要转换的字节数组\n     * @return 十六进制字符串（大写）\n     */\n    public static String bytesToHex(byte[] bytes) {\n        if (bytes == null) return null;\n        char[] hexChars = new char[bytes.length * 2];\n        for (int i = 0; i < bytes.length; i++) {\n            int v = bytes[i] & 0xFF;\n            hexChars[i * 2] = HEX_ARRAY[v >>> 4];\n            hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F];\n        }\n        return new String(hexChars);\n    }\n\n    /**\n     * 将十六进制字符串转换为字节数组（JDK8兼容）\n     * @param hexString 有效的十六进制字符串（不区分大小写）\n     * @return 对应的字节数组\n     * @throws IllegalArgumentException 输入字符串格式错误时抛出异常\n     */\n    public static byte[] hexToBytes(String hexString) {\n        if (hexString == null) return null;\n        int len = hexString.length();\n        if (len % 2 != 0) {\n            throw new IllegalArgumentException(\"Hex string must have even length\");\n        }\n\n        byte[] data = new byte[len / 2];\n        for (int i = 0; i < len; i += 2) {\n            int high = Character.digit(hexString.charAt(i), 16);\n            int low = Character.digit(hexString.charAt(i+1), 16);\n\n            if (high == -1 || low == -1) {\n                throw new IllegalArgumentException(\n                        \"Invalid hex character at position \" + i + \" or \" + (i+1)\n                );\n            }\n\n            data[i/2] = (byte) ((high << 4) + low);\n        }\n        return data;\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/SaResult.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\nimport cn.dev33.satoken.SaManager;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 对请求接口返回 Json 格式数据的简易封装。\n *\n * <p>\n *     所有预留字段：<br>\n * \t\tcode = 状态码 <br>\n * \t\tmsg  = 描述信息 <br>\n * \t\tdata = 携带对象 <br>\n * </p>\n *\n * @author click33\n * @since 1.22.0\n */\npublic class SaResult extends LinkedHashMap<String, Object> implements Serializable{\n\n\t// 序列化版本号\n\tprivate static final long serialVersionUID = 1L;\n\n\t// 预定的状态码\n\tpublic static final int CODE_SUCCESS = 200;\t\t\n\tpublic static final int CODE_ERROR = 500;\n\tpublic static final int CODE_NOT_PERMISSION = 403;\n\tpublic static final int CODE_NOT_LOGIN = 401;\n\n\t/**\n\t * 构建 \n\t */\n\tpublic SaResult() {\n\t}\n\n\t/**\n\t * 构建 \n\t * @param code 状态码\n\t * @param msg 信息\n\t * @param data 数据 \n\t */\n\tpublic SaResult(int code, String msg, Object data) {\n\t\tthis.setCode(code);\n\t\tthis.setMsg(msg);\n\t\tthis.setData(data);\n\t}\n\n\t/**\n\t * 根据 Map 快速构建 \n\t * @param map / \n\t */\n\tpublic SaResult(Map<String, ?> map) {\n\t\tthis.setMap(map);\n\t}\n\t\n\t/**\n\t * 获取code \n\t * @return code\n\t */\n\tpublic Integer getCode() {\n\t\treturn (Integer)this.get(\"code\");\n\t}\n\t/**\n\t * 获取msg\n\t * @return msg\n\t */\n\tpublic String getMsg() {\n\t\treturn (String)this.get(\"msg\");\n\t}\n\t/**\n\t * 获取data\n\t * @return data \n\t */\n\tpublic Object getData() {\n\t\treturn this.get(\"data\");\n\t}\n\t\n\t/**\n\t * 给code赋值，连缀风格\n\t * @param code code\n\t * @return 对象自身\n\t */\n\tpublic SaResult setCode(int code) {\n\t\tthis.put(\"code\", code);\n\t\treturn this;\n\t}\n\t/**\n\t * 给msg赋值，连缀风格\n\t * @param msg msg\n\t * @return 对象自身\n\t */\n\tpublic SaResult setMsg(String msg) {\n\t\tthis.put(\"msg\", msg);\n\t\treturn this;\n\t}\n\t/**\n\t * 给data赋值，连缀风格\n\t * @param data data\n\t * @return 对象自身\n\t */\n\tpublic SaResult setData(Object data) {\n\t\tthis.put(\"data\", data);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入一个值 自定义key, 连缀风格\n\t * @param key key\n\t * @param data data\n\t * @return 对象自身 \n\t */\n\tpublic SaResult set(String key, Object data) {\n\t\tthis.put(key, data);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取一个值 根据自定义key \n\t * @param <T> 要转换为的类型 \n\t * @param key key\n\t * @param cs 要转换为的类型 \n\t * @return 值 \n\t */\n\tpublic <T> T get(String key, Class<T> cs) {\n\t\treturn SaFoxUtil.getValueByType(get(key), cs);\n\t}\n\n\t/**\n\t * 写入一个Map, 连缀风格\n\t * @param map map \n\t * @return 对象自身 \n\t */\n\tpublic SaResult setMap(Map<String, ?> map) {\n\t\tif(map != null) {\n\t\t\tfor (String key : map.keySet()) {\n\t\t\t\tthis.put(key, map.get(key));\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入一个 json 字符串, 连缀风格\n\t * @param jsonString json 字符串\n\t * @return 对象自身\n\t */\n\tpublic SaResult setJsonString(String jsonString) {\n\t\tMap<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(jsonString);\n\t\treturn setMap(map);\n\t}\n\n\t/**\n\t * 移除默认属性（code、msg、data）, 连缀风格\n\t * @return 对象自身\n\t */\n\tpublic SaResult removeDefaultFields() {\n\t\tthis.remove(\"code\");\n\t\tthis.remove(\"msg\");\n\t\tthis.remove(\"data\");\n\t\treturn this;\n\t}\n\n\t/**\n\t * 移除非默认属性（code、msg、data）, 连缀风格\n\t * @return 对象自身\n\t */\n\tpublic SaResult removeNonDefaultFields() {\n\t\tfor (String key : this.keySet()) {\n\t\t\tif(\"code\".equals(key) || \"msg\".equals(key) || \"data\".equals(key)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.remove(key);\n\t\t}\n\t\treturn this;\n\t}\n\n\t\n\t// ============================  静态方法快速构建  ==================================\n\t\n\t// 构建成功\n\tpublic static SaResult ok() {\n\t\treturn new SaResult(CODE_SUCCESS, \"ok\", null);\n\t}\n\tpublic static SaResult ok(String msg) {\n\t\treturn new SaResult(CODE_SUCCESS, msg, null);\n\t}\n\tpublic static SaResult code(int code) {\n\t\treturn new SaResult(code, null, null);\n\t}\n\tpublic static SaResult data(Object data) {\n\t\treturn new SaResult(CODE_SUCCESS, \"ok\", data);\n\t}\n\t\n\t// 构建失败\n\tpublic static SaResult error() {\n\t\treturn new SaResult(CODE_ERROR, \"error\", null);\n\t}\n\tpublic static SaResult error(String msg) {\n\t\treturn new SaResult(CODE_ERROR, msg, null);\n\t}\n\n\t// 构建未登录\n\tpublic static SaResult notLogin() {\n\t\treturn new SaResult(CODE_NOT_LOGIN, \"not login\", null);\n\t}\n\n\t// 构建无权限\n\tpublic static SaResult notPermission() {\n\t\treturn new SaResult(CODE_NOT_PERMISSION, \"not permission\", null);\n\t}\n\n\n\n\t// 构建指定状态码 \n\tpublic static SaResult get(int code, String msg, Object data) {\n\t\treturn new SaResult(code, msg, data);\n\t}\n\n\t// 构建一个空的\n\tpublic static SaResult empty() {\n\t\treturn new SaResult();\n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@Override\n\tpublic String toString() {\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \" + transValue(this.getMsg()) \n\t\t\t\t+ \", \\\"data\\\": \" + transValue(this.getData()) \n\t\t\t\t+ \"}\";\n\t}\n\n\t/**\n\t * 转换 value 值：\n\t * \t如果 value 值属于 String 类型，则在前后补上引号\n\t * \t如果 value 值属于其它类型，则原样返回\n\t *\n\t * @param value 具体要操作的值\n\t * @return 转换后的值\n\t */\n\tprivate String transValue(Object value) {\n\t\tif(value == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif(value instanceof String) {\n\t\t\treturn \"\\\"\" + value + \"\\\"\";\n\t\t}\n\t\treturn String.valueOf(value);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/SaSugar.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\nimport cn.dev33.satoken.fun.SaFunction;\n\nimport java.util.function.Supplier;\n\n/**\n * 代码语法糖封装\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSugar {\n\n\t/**\n\t * 执行一个 Lambda 表达式，返回这个 Lambda 表达式的结果值，\n\t * <br> 方便组织代码，例如: \n\t * <pre> \n\t \tint value = Sugar.get(() -> {\n\t\t\tint a = 1;\n\t\t\tint b = 2;\n\t\t\treturn a + b;\n\t\t});\n\t\t</pre> \n\t * @param <R> 返回值类型 \n\t * @param lambda lambda 表达式\n\t * @return lambda 的执行结果\n\t */\n\tpublic static <R> R get(Supplier<R> lambda) {\n\t\treturn lambda.get();\n\t}\n\n\t/**\n\t * 执行一个 Lambda 表达式 \n\t * <br> 方便组织代码，例如: \n\t * <pre> \n\t \tSugar.exe(() -> {\n\t\t\tint a = 1;\n\t\t\tint b = 2;\n\t\t\treturn a + b;\n\t\t});\n\t\t</pre> \n\t * @param lambda lambda 表达式\n\t */\n\tpublic static void exe(SaFunction lambda) {\n\t\tlambda.run();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\n/**\n * Sa-Token 常量类\n *\n * <p>\n *     一般的常量采用就近原则，定义在各自相应的模块中。\n *     但有一些常量没有明确的归属模块，会在很多模块中使用到，比如版本号、开源地址等，属于全局性的础性常量，这些常量统一定义在此类中。\n * </p>\n *\n * @author click33\n * @since 1.8.0\n */\npublic class SaTokenConsts {\n\n\tprivate SaTokenConsts() {\n\t}\n\t\n\t// ------------------ Sa-Token 版本信息\n\t\n\t/**\n\t * Sa-Token 当前版本号 \n\t */\n\tpublic static final String VERSION_NO = \"v1.45.0\";\n\n\t/**\n\t * Sa-Token 开源地址 Gitee \n\t */\n\tpublic static final String GITEE_URL = \"https://gitee.com/dromara/sa-token\";\n\n\t/**\n\t * Sa-Token 开源地址 GitHub  \n\t */\n\tpublic static final String GITHUB_URL = \"https://github.com/dromara/sa-token\";\n\n\t/**\n\t * Sa-Token 开发文档地址 \n\t */\n\tpublic static final String DEV_DOC_URL = \"https://sa-token.cc\";\n\t\n\t\n\t// ------------------ 常量 key 标记\n\t\n\t/**\n\t * 常量 key 标记: 如果 token 为本次请求新创建的，则以此字符串为 key 存储在当前请求 str 中\n\t */\n\tpublic static final String JUST_CREATED = \"JUST_CREATED_\"; \t\n\n\t/**\n\t * 常量 key 标记: 如果 token 为本次请求新创建的，则以此字符串为 key 存储在当前 request 中（不拼接前缀，纯Token）\n\t */\n\tpublic static final String JUST_CREATED_NOT_PREFIX = \"JUST_CREATED_NOT_PREFIX_\"; \t\n\n\t/**\n\t * 常量 key 标记: 如果本次请求已经验证过 activeTimeout, 则以此 key 在 storage 中做一个标记\n\t */\n\tpublic static final String TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY = \"TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY_\";\n\n\t/**\n\t * 常量 key 标记: 在登录时，默认使用的设备类型 \n\t */\n\tpublic static final String DEFAULT_LOGIN_DEVICE_TYPE = \"DEF\";\n\n\t/**\n\t * 常量 key 标记: 在封禁账号时，默认封禁的服务类型 \n\t */\n\tpublic static final String DEFAULT_DISABLE_SERVICE = \"login\"; \n\n\t/**\n\t * 常量 key 标记: 在封禁账号时，默认封禁的等级 \n\t */\n\tpublic static final int DEFAULT_DISABLE_LEVEL = 1; \n\n\t/**\n\t * 常量 key 标记: 在封禁账号时，可使用的最小封禁级别 \n\t */\n\tpublic static final int MIN_DISABLE_LEVEL = 1; \n\n\t/**\n\t * 常量 key 标记: 账号封禁级别，表示未被封禁 \n\t */\n\tpublic static final int NOT_DISABLE_LEVEL = -2; \n\t\n\t/**\n\t * 常量 key 标记: 在进行临时身份切换时使用的 key\n\t */\n\tpublic static final String SWITCH_TO_SAVE_KEY = \"SWITCH_TO_SAVE_KEY_\"; \n\n\t/**\n\t * 常量 key 标记: 在进行 Token 二级验证时，使用的 key\n\t */\n\t@Deprecated\n\tpublic static final String SAFE_AUTH_SAVE_KEY = \"SAFE_AUTH_SAVE_KEY_\"; \n\n\t/**\n\t * 常量 key 标记: 在进行 Token 二级认证时，写入的 value 值\n\t */\n\tpublic static final String SAFE_AUTH_SAVE_VALUE = \"SAFE_AUTH_SAVE_VALUE\"; \n\n\t/**\n\t * 常量 key 标记: 在进行 Token 二级验证时，默认的业务类型 \n\t */\n\tpublic static final String DEFAULT_SAFE_AUTH_SERVICE = \"important\"; \n\n\t/**\n\t * 常量 key 标记: 临时 Token 认证模块，默认的业务类型 \n\t */\n\t@Deprecated\n\tpublic static final String DEFAULT_TEMP_TOKEN_SERVICE = \"record\"; \n\n\n\t// ------------------ token-style 相关\n\t\n\t/**\n\t * Token风格: uuid \n\t */\n\tpublic static final String TOKEN_STYLE_UUID = \"uuid\"; \n\t\n\t/**\n\t * Token风格: 简单uuid (不带下划线) \n\t */\n\tpublic static final String TOKEN_STYLE_SIMPLE_UUID = \"simple-uuid\"; \n\t\n\t/**\n\t * Token风格: 32位随机字符串 \n\t */\n\tpublic static final String TOKEN_STYLE_RANDOM_32 = \"random-32\"; \n\t\n\t/**\n\t * Token风格: 64位随机字符串 \n\t */\n\tpublic static final String TOKEN_STYLE_RANDOM_64 = \"random-64\"; \n\t\n\t/**\n\t * Token风格: 128位随机字符串 \n\t */\n\tpublic static final String TOKEN_STYLE_RANDOM_128 = \"random-128\"; \n\t\n\t/**\n\t * Token风格: tik风格 (2_14_16) \n\t */\n\tpublic static final String TOKEN_STYLE_TIK = \"tik\";\n\n\n\t// ------------------ SaSession 的类型\n\n\t/**\n\t * SaSession 的类型: Account-Session\n\t */\n\tpublic static final String SESSION_TYPE__ACCOUNT = \"Account-Session\";\n\n\t/**\n\t * SaSession 的类型: Token-Session\n\t */\n\tpublic static final String SESSION_TYPE__TOKEN = \"Token-Session\";\n\n\t/**\n\t * SaSession 的类型: Anon-Token-Session\n\t */\n\tpublic static final String SESSION_TYPE__ANON = \"Anon-Token-Session\";\n\n\t/**\n\t * SaSession 的类型: Custom-Session\n\t */\n\tpublic static final String SESSION_TYPE__CUSTOM = \"Custom-Session\";\n\n\n\t// ------------------ 其它\n\n\t/**\n\t * 连接 Token 前缀和 Token 值的字符\n\t */\n\tpublic static final String TOKEN_CONNECTOR_CHAT  = \" \"; \n\t\n\t/**\n\t * 切面、拦截器、过滤器等各种组件的注册优先级顺序\n\t */\n\tpublic static final int ASSEMBLY_ORDER = -100;\n\n\t/**\n\t * 防火墙校验过滤器的注册顺序\n\t */\n\tpublic static final int FIREWALL_CHECK_FILTER_ORDER = -102;\n\n\t/**\n\t * 跨域处理过滤器的注册顺序\n\t */\n\tpublic static final int CORS_FILTER_ORDER = -103;\n\n\t/**\n\t * 上下文过滤器的注册顺序\n\t */\n\tpublic static final int SA_TOKEN_CONTEXT_FILTER_ORDER = -104;\n\n\t/**\n\t * RPC 框架权限过滤器的注册顺序\n\t */\n\tpublic static final int RPC_PERMISSION_FILTER_ORDER = -30000;\n\n\t/**\n\t * RPC 框架上下文过滤器的注册顺序\n\t */\n\tpublic static final int RPC_CONTEXT_FILTER_ORDER = -30005;\n\n\t/**\n\t * Content-Type  key\n\t */\n\tpublic static final String CONTENT_TYPE_KEY = \"Content-Type\";\n\n\t/**\n\t * Content-Type  text/plain; charset=utf-8\n\t */\n\tpublic static final String CONTENT_TYPE_TEXT_PLAIN = \"text/plain; charset=utf-8\";\n\n\t/**\n\t * Content-Type  application/json;charset=UTF-8\n\t */\n\tpublic static final String CONTENT_TYPE_APPLICATION_JSON = \"application/json;charset=UTF-8\";\n\n\n\n\t\n\t// =================== 废弃 ===================  \n\n\t/**\n\t * 请更换为 JUST_CREATED  \n\t */\n\t@Deprecated\n\tpublic static final String JUST_CREATED_SAVE_KEY = JUST_CREATED;\n\n\t/**\n\t * 请更换为 TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY\n\t */\n\t@Deprecated\n\tpublic static final String TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY = TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY;\n\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/SaTtlMethods.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\n\nimport java.util.*;\n\n/**\n * TTL 操作工具方法\n *\n * @author click33\n * @since 1.43.0\n */\npublic interface SaTtlMethods {\n\n\t/**\n\t * 获取一个新的 Token 集合\n\t * @return /\n\t */\n\tdefault List<String> newTokenValueList() {\n\t\treturn new ArrayList<>();\n\t}\n\n\t/**\n\t * 获取一个新的 TokenIndexMap 集合\n\t * @return /\n\t */\n\tdefault Map<String, Long> newTokenIndexMap() {\n\t\treturn new LinkedHashMap<>();\n\t}\n\n\t/**\n\t * 获取最大 ttl 值\n\t * @param ttlList /\n\t * @return /\n\t */\n\tdefault long getMaxTtl(ArrayList<Long> ttlList) {\n\t\tlong maxTtl = 0;\n\t\tfor (long ttl : ttlList) {\n\t\t\tif(ttl == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t\tmaxTtl = SaTokenDao.NEVER_EXPIRE;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(ttl > maxTtl) {\n\t\t\t\tmaxTtl = ttl;\n\t\t\t}\n\t\t}\n\t\treturn maxTtl;\n\t}\n\n\t/**\n\t * 获取最大 ttl 值：过期时间 (13位时间戳) 转 ttl (秒)\n\t * @param expireTimeList /\n\t * @return /\n\t */\n\tdefault long getMaxTtlByExpireTime(Collection<Long> expireTimeList) {\n\t\tlong maxTtl = 0;\n\t\tfor (long expireTime : expireTimeList) {\n\t\t\tlong ttl = expireTimeToTtl(expireTime);\n\t\t\tif(ttl == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t\tmaxTtl = SaTokenDao.NEVER_EXPIRE;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(ttl > maxTtl) {\n\t\t\t\tmaxTtl = ttl;\n\t\t\t}\n\t\t}\n\t\treturn maxTtl;\n\t}\n\n\t/**\n\t * 过期时间 (13位时间戳) 转 (13位时间戳) ttl (秒)\n\t * @param expireTime /\n\t * @return /\n\t */\n\tdefault long expireTimeToTtl(long expireTime) {\n\t\tif(expireTime == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn SaTokenDao.NEVER_EXPIRE;\n\t\t}\n\t\tif(expireTime == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\t\tlong currentTime = System.currentTimeMillis();\n\t\tif(expireTime < currentTime) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\t\treturn (expireTime - currentTime) / 1000;\n\t}\n\n\t/**\n\t * ttl (秒) 转 过期时间 (13位时间戳)\n\t * @param ttl /\n\t * @return /\n\t */\n\tdefault long ttlToExpireTime(long ttl) {\n\t\tif(ttl == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn SaTokenDao.NEVER_EXPIRE;\n\t\t}\n\t\tif(ttl < 0) {\n\t\t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n\t\t}\n\t\treturn ttl * 1000 + System.currentTimeMillis();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/SaValue2Box.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\n\n/**\n * 封装两个值的容器，方便取值、写值等操作，value1 和 value2 用逗号隔开，形如：123,abc\n *\n * @author click33\n * @since 1.35.0\n */\npublic class SaValue2Box {\n\n    /**\n     * 第一个值\n     */\n    private Object value1;\n\n    /**\n     * 第二个值\n     */\n    private Object value2;\n\n    /**\n     * 直接提供两个值构建\n     * @param value1 第一个值\n     * @param value2 第二个值\n     */\n    public SaValue2Box(Object value1, Object value2) {\n        this.value1 = value1;\n        this.value2 = value2;\n    }\n\n    /**\n     * 根据字符串构建，字符串形如：123,abc\n     * @param valueString 形如：123,abc\n     */\n    public SaValue2Box(String valueString) {\n        if(valueString == null){\n            return;\n        }\n        String[] split = valueString.split(\",\");\n        if(split.length == 0){\n            // do nothing\n        }\n        else if(split.length == 1){\n            this.value1 = split[0];\n        }\n        else {\n            this.value1 = split[0];\n            this.value2 = split[1];\n        }\n    }\n\n    /**\n     * 获取第一个值\n     * @return 第一个值\n     */\n    public Object getValue1() {\n        return value1;\n    }\n\n    /**\n     * 获取第二个值\n     * @return 第二个值\n     */\n    public Object getValue2() {\n        return value2;\n    }\n\n    /**\n     * 设置第一个值\n     * @param value1 第一个值\n     */\n    public void setValue1(Object value1) {\n        this.value1 = value1;\n    }\n\n    /**\n     * 设置第二个值\n     * @param value2 第二个值\n     */\n    public void setValue2(Object value2) {\n        this.value2 = value2;\n    }\n\n    /**\n     * 判断第一个值是否为 null 或者空字符串\n     * @return /\n     */\n    public boolean value1IsEmpty() {\n        return SaFoxUtil.isEmpty(value1);\n    }\n\n    /**\n     * 判断第二个值是否为 null 或者空字符串\n     * @return /\n     */\n    public boolean value2IsEmpty() {\n        return SaFoxUtil.isEmpty(value2);\n    }\n\n    /**\n     * 获取第一个值，并转化为 String 类型\n     * @return /\n     */\n    public String getValue1AsString() {\n        return value1 == null ? null : value1.toString();\n    }\n\n    /**\n     * 获取第二个值，并转化为 String 类型\n     * @return /\n     */\n    public String getValue2AsString() {\n        return value2 == null ? null : value2.toString();\n    }\n\n    /**\n     * 获取第一个值，并转化为 long 类型\n     * @return /\n     */\n    public long getValue1AsLong() {\n        return Long.parseLong(value1.toString());\n    }\n\n    /**\n     * 获取第二个值，并转化为 long 类型\n     * @return /\n     */\n    public long getValue2AsLong() {\n        return Long.parseLong(value2.toString());\n    }\n\n    /**\n     * 获取第一个值，并转化为 long 类型，值不存在则返回默认值\n     * @return /\n     */\n    public Long getValue1AsLong(Long defaultValue) {\n        // 这里如果改成三元表达式，会导致自动拆箱造成空指针异常，所以只能用 if-else\n        if(value1 == null){\n            return defaultValue;\n        }\n        return Long.parseLong(value1.toString());\n    }\n\n    /**\n     * 获取第二个值，并转化为 long 类型，值不存在则返回默认值\n     * @return /\n     */\n    public Long getValue2AsLong(Long defaultValue) {\n        // 这里如果改成三元表达式，会导致自动拆箱造成空指针异常，所以只能用 if-else\n        if(value2 == null){\n            return defaultValue;\n        }\n        return Long.parseLong(value2.toString());\n    }\n\n    /**\n     * 该容器是否为无值状态，即：value1 无值、value2 无值\n     * @return /\n     */\n    public boolean isNotValueState() {\n        return value1IsEmpty() && value2IsEmpty();\n    }\n\n    /**\n     * 该容器是否为单值状态，即：value1 有值、value2 == 无值\n     * @return /\n     */\n    public boolean isSingleValueState() {\n        return ! value1IsEmpty() && value2IsEmpty();\n    }\n\n    /**\n     * 该容器是否为双值状态，即：value2 有值 （在 value2 有值的情况下，即使 value1 无值，也视为双值状态）\n     * @return /\n     */\n    public boolean isDoubleValueState() {\n        return ! value2IsEmpty();\n    }\n\n    /**\n     * 获取两个值的字符串形式，形如：123,abc\n     *\n     * <br><br>\n     * <pre>\n     *     System.out.println(new SaValue2Box(1, 2));     // 1,2\n     *     System.out.println(new SaValue2Box(null, null));   // null\n     *     System.out.println(new SaValue2Box(1, null));   // 1\n     *     System.out.println(new SaValue2Box(null, 2));  // ,2\n     * </pre>\n     * @return /\n     */\n    @Override\n    public String toString() {\n        if(value1 == null && value2 == null) {\n            return null;\n        }\n        if(value1 != null && value2 == null) {\n            return value1.toString();\n        }\n        return (value1 == null ? \"\" : value1.toString()) + \",\" + (value2 == null ? \"\" : value2.toString());\n    }\n\n}"
  },
  {
    "path": "sa-token-core/src/main/java/cn/dev33/satoken/util/StrFormatter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\n/**\n * 字符串格式化工具，将字符串中的 {} 按序替换为参数\n * <p>\n * \t本工具类 copy 自 Hutool：\n * \t\thttps://github.com/dromara/hutool/blob/v5-master/hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java\n * </p>\n *\n * @author Looly\n * @since 1.33.0\n */\npublic class StrFormatter {\n\n\t/**\n\t * 占位符（保留原有 public 访问权限，避免破坏外部依赖）\n\t * @deprecated 语义不明确，建议内部使用 {@link #DEFAULT_PLACEHOLDER} 替代\n\t */\n\t@Deprecated\n\tpublic static String EMPTY_JSON = \"{}\";\n\n\t/**\n\t * 反斜杠转义字符（保留原有 public 访问权限，避免破坏外部依赖）\n\t * @deprecated 命名不规范，建议内部使用 {@link #BACKSLASH_CHAR} 替代\n\t */\n\t@Deprecated\n\tpublic static char C_BACKSLASH = '\\\\';\n\n\t/**\n\t * 新增内部规范常量（private，仅内部使用）\n\t * 默认占位符 */\n\tprivate static final String DEFAULT_PLACEHOLDER = \"{}\";\n\n\t/**\n\t * 反斜杠转义字符\n\t * */\n\tprivate static final char BACKSLASH_CHAR = '\\\\';\n\n\t/**\n\t * 字符串构建器初始扩容长度\n\t * */\n\tprivate static final int BUFFER_INIT_CAPACITY = 50;\n\n\t/**\n\t * 格式化字符串<br>\n\t * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>\n\t * 如果想输出 {} 使用 \\\\转义 { 即可，如果想输出 {} 之前的 \\ 使用双转义符 \\\\\\\\ 即可<br>\n\t * 例：<br>\n\t * 通常使用：format(\"this is {} for {}\", \"a\", \"b\") =》 this is a for b<br>\n\t * 转义{}： format(\"this is \\\\{} for {}\", \"a\", \"b\") =》 this is \\{} for a<br>\n\t * 转义\\： format(\"this is \\\\\\\\{} for {}\", \"a\", \"b\") =》 this is \\a for b<br>\n\t *\n\t * @param strPattern 字符串模板\n\t * @param argArray   参数列表\n\t * @return 格式化后的结果\n\t */\n\tpublic static String format(String strPattern, Object... argArray) {\n\t\treturn formatWith(strPattern, DEFAULT_PLACEHOLDER, argArray);\n\t}\n\n\t/**\n\t * 格式化字符串<br>\n\t * 此方法只是简单将指定占位符 按照顺序替换为参数<br>\n\t * 如果想输出占位符使用 \\\\转义即可，如果想输出占位符之前的 \\ 使用双转义符 \\\\\\\\ 即可<br>\n\t * 例：<br>\n\t * 通常使用：format(\"this is {} for {}\", \"{}\", \"a\", \"b\") =》 this is a for b<br>\n\t * 转义{}： format(\"this is \\\\{} for {}\", \"{}\", \"a\", \"b\") =》 this is {} for a<br>\n\t * 转义\\： format(\"this is \\\\\\\\{} for {}\", \"{}\", \"a\", \"b\") =》 this is \\a for b<br>\n\t *\n\t * @param strPattern  字符串模板\n\t * @param placeHolder 占位符，例如{}\n\t * @param argArray    参数列表\n\t * @return 格式化后的结果\n\t * @since 1.33.0\n\t */\n\tpublic static String formatWith(String strPattern, String placeHolder, Object... argArray) {\n\t\tif (SaFoxUtil.isEmpty(strPattern) || SaFoxUtil.isEmpty(placeHolder) || SaFoxUtil.isEmpty(argArray)) {\n\t\t\treturn strPattern;\n\t\t}\n\t\tfinal int strPatternLength = strPattern.length();\n\t\tfinal int placeHolderLength = placeHolder.length();\n\n\t\t// 初始化定义好的长度以获得更好的性能\n\t\tfinal StringBuilder sbu = new StringBuilder(strPatternLength + BUFFER_INIT_CAPACITY);\n\n\t\tint handledPosition = 0;// 记录已经处理到的位置\n\t\tint delimIndex;// 占位符所在位置\n\t\tfor (int argIndex = 0; argIndex < argArray.length; argIndex++) {\n\t\t\tdelimIndex = strPattern.indexOf(placeHolder, handledPosition);\n\t\t\tif (delimIndex == -1) {// 剩余部分无占位符\n\t\t\t\tif (handledPosition == 0) { // 不带占位符的模板直接返回\n\t\t\t\t\treturn strPattern;\n\t\t\t\t}\n\t\t\t\t// 字符串模板剩余部分不再包含占位符，加入剩余部分后返回结果\n\t\t\t\tsbu.append(strPattern, handledPosition, strPatternLength);\n\t\t\t\treturn sbu.toString();\n\t\t\t}\n\n\t\t\t// 转义符\n\t\t\tif (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == BACKSLASH_CHAR) {// 转义符\n\t\t\t\tif (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == BACKSLASH_CHAR) {// 双转义符\n\t\t\t\t\t// 转义符之前还有一个转义符，占位符依旧有效\n\t\t\t\t\tsbu.append(strPattern, handledPosition, delimIndex - 1);\n\t\t\t\t\tsbu.append(argArray[argIndex]);\n\t\t\t\t\thandledPosition = delimIndex + placeHolderLength;\n\t\t\t\t} else {\n\t\t\t\t\t// 占位符被转义\n\t\t\t\t\targIndex--;\n\t\t\t\t\tsbu.append(strPattern, handledPosition, delimIndex - 1);\n\t\t\t\t\tsbu.append(placeHolder.charAt(0));\n\t\t\t\t\thandledPosition = delimIndex + 1;\n\t\t\t\t}\n\t\t\t} else {// 正常占位符\n\t\t\t\tsbu.append(strPattern, handledPosition, delimIndex);\n\t\t\t\tsbu.append(argArray[argIndex]);\n\t\t\t\thandledPosition = delimIndex + placeHolderLength;\n\t\t\t}\n\t\t}\n\n\t\t// 加入最后一个占位符后所有的字符\n\t\tsbu.append(strPattern, handledPosition, strPatternLength);\n\n\t\treturn sbu.toString();\n\t}\n\n}"
  },
  {
    "path": "sa-token-demo/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t<packaging>pom</packaging>\n\n\t<!-- 所有子模块 -->\n\t<modules>\n\t\t<module>sa-token-demo-alone-redis</module>\n\t\t<module>sa-token-demo-alone-redis-cluster</module>\n\t\t<module>sa-token-demo-alone-redis-sb4</module>\n\t\t<module>sa-token-demo-apikey</module>\n\t\t<module>sa-token-demo-async</module>\n\t\t<module>sa-token-demo-beetl</module>\n\t\t<module>sa-token-demo-bom-import</module>\n\t\t<module>sa-token-demo-caffeine</module>\n\t\t<module>sa-token-demo-case</module>\n\t\t<module>sa-token-demo-device-lock</module>\n\t\t<module>sa-token-demo-dubbo/sa-token-demo-dubbo-provider</module>\n\t\t<module>sa-token-demo-dubbo/sa-token-demo-dubbo-consumer</module>\n\t\t<module>sa-token-demo-dubbo/sa-token-demo-dubbo3-provider</module>\n\t\t<module>sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer</module>\n\t\t<module>sa-token-demo-freemarker</module>\n<!--\t\t<module>sa-token-demo-grpc</module>-->\n\t\t<module>sa-token-demo-hutool-timed-cache</module>\n\t\t<module>sa-token-demo-jwt</module>\n\t\t<module>sa-token-demo-oauth2/sa-token-demo-oauth2-server</module>\n\t\t<module>sa-token-demo-oauth2/sa-token-demo-oauth2-client</module>\n\t\t<module>sa-token-demo-quick-login</module>\n\t\t<module>sa-token-demo-quick-login-sb3</module>\n\t\t<module>sa-token-demo-remember-me/sa-token-demo-remember-me-server</module>\n\t\t<module>sa-token-demo-solon</module>\n\t\t<module>sa-token-demo-solon-redisson</module>\n\t\t<module>sa-token-demo-springboot</module>\n\t\t<module>sa-token-demo-springboot3-redis</module>\n\t\t<module>sa-token-demo-springboot4-redis</module>\n\t\t<module>sa-token-demo-springboot-low-version</module>\n\t\t<module>sa-token-demo-springboot-redis</module>\n\t\t<module>sa-token-demo-springboot-redisson</module>\n\t\t<module>sa-token-demo-sse</module>\n\t\t<module>sa-token-demo-ssm</module>\n\t\t<module>sa-token-demo-sso/sa-token-demo-sso-server</module>\n\t\t<module>sa-token-demo-sso/sa-token-demo-sso1-client</module>\n\t\t<module>sa-token-demo-sso/sa-token-demo-sso2-client</module>\n\t\t<module>sa-token-demo-sso/sa-token-demo-sso3-client</module>\n\t\t<module>sa-token-demo-sso/sa-token-demo-sso3-client-nosdk</module>\n\t\t<module>sa-token-demo-sso/sa-token-demo-sso3-client-resdk</module>\n\t\t<module>sa-token-demo-sso/sa-token-demo-sso3-client-anon</module>\n\t\t<module>sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon</module>\n\t\t<module>sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon</module>\n\t\t<module>sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon</module>\n\t\t<module>sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon</module>\n\t\t<module>sa-token-demo-test</module>\n\t\t<module>sa-token-demo-thymeleaf</module>\n\t\t<module>sa-token-demo-webflux</module>\n\t\t<module>sa-token-demo-webflux-springboot3</module>\n\t\t<module>sa-token-demo-webflux-springboot4</module>\n\t\t<module>sa-token-demo-websocket</module>\n\t\t<module>sa-token-demo-websocket-spring</module>\n        <module>sa-token-demo-loveqq-boot</module>\n\n    </modules>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-alone-redis</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!--<version>2.3.3.RELEASE</version>-->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-alone-redis</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/SaTokenAloneRedisApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * Sa-Token整合SpringBoot 示例 \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenAloneRedisApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenAloneRedisApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/AjaxJson.java",
    "content": "package com.pj.test;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tStringRedisTemplate stringRedisTemplate;\n\t\n\t// 测试Sa-Token缓存， 浏览器访问： http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic AjaxJson login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tSystem.out.println(\"--------------- 测试Sa-Token缓存\");\n\t\tStpUtil.login(id);\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试业务缓存   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic AjaxJson test() {\n\t\tSystem.out.println(\"--------------- 测试业务缓存\");\n\t\tstringRedisTemplate.opsForValue().set(\"hello\", \"Hello World\");\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# Sa-Token配置\nsa-token: \n    # Token名称 (同时也是cookie名称)\n    token-name: satoken\n    # Token有效期，单位s 默认30天, -1代表永不过期 \n    timeout: 2592000\n    # Token风格\n    token-style: uuid\n    # 配置Sa-Token单独使用的Redis连接 \n    alone-redis:\n        # Redis模式(默认单体)\n        # pattern: single\n        # Redis数据库索引（默认为0）\n        database: 2\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        # password: \n        # 连接超时时间（毫秒）\n        timeout: 10s\n        lettuce: \n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \nspring: \n    # 配置业务使用的Redis连接 \n    redis: \n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间（毫秒）\n        timeout: 10s\n        lettuce: \n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-cluster/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-alone-redis-cluster</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-alone-redis</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/SaTokenAloneRedisClusterApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * Sa-Token整合SpringBoot 示例 \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenAloneRedisClusterApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenAloneRedisClusterApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/AjaxJson.java",
    "content": "package com.pj.test;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tStringRedisTemplate stringRedisTemplate;\n\t\n\t// 测试Sa-Token缓存， 浏览器访问： http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic AjaxJson login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tSystem.out.println(\"--------------- 测试Sa-Token缓存\");\n\t\tStpUtil.login(id);\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试业务缓存   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic AjaxJson test() {\n\t\tSystem.out.println(\"--------------- 测试业务缓存\");\n\t\tstringRedisTemplate.opsForValue().set(\"hello\", \"Hello World\");\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# Sa-Token配置\nsa-token: \n    # Token名称 (同时也是cookie名称)\n    token-name: satoken\n    # Token有效期，单位s 默认30天, -1代表永不过期 \n    timeout: 2592000\n    # Token风格\n    token-style: uuid\n    # 配置Sa-Token单独使用的Redis连接 \n    alone-redis:\n        # 普通集群\n        pattern: cluster\n        # Redis服务器连接用户名（默认为空）\n        username:\n        # Redis服务器连接密码（默认为空）\n        password:\n        # 连接超时时间（毫秒）\n        timeout: 10s\n        cluster:\n            # Redis集群服务器节点地址\n            nodes: 127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002\n            # 最大重定向次数\n            maxRedirects: 2\n        lettuce: \n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \nspring: \n    # 配置业务使用的Redis连接 \n    redis: \n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间（毫秒）\n        timeout: 10s\n        lettuce: \n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-cluster/src/main/resources/application_sentinel.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# Sa-Token配置\nsa-token: \n    # Token名称 (同时也是cookie名称)\n    token-name: satoken\n    # Token有效期，单位s 默认30天, -1代表永不过期 \n    timeout: 2592000\n    # Token风格\n    token-style: uuid\n    # 配置Sa-Token单独使用的Redis连接 \n    alone-redis:\n        # 哨兵模式\n        pattern: sentinel\n        # Redis数据库索引（默认为0）\n        database: 2\n        # Redis服务器连接用户名（默认为空）\n        username:\n        # Redis服务器连接密码（默认为空）\n        password:\n        # 连接超时时间（毫秒）\n        timeout: 10s\n        sentinel:\n            #哨兵的名字\n            master: master_name\n            # Redis集群服务器节点地址\n            nodes: 127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002\n        lettuce: \n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \nspring: \n    # 配置业务使用的Redis连接 \n    redis: \n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间（毫秒）\n        timeout: 10s\n        lettuce: \n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-sb4/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-alone-redis-sb4</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot 4 -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>4.0.3</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot 4 依赖：webmvc 替代 deprecated 的 starter-web，aspectj 替代 starter-aop -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-webmvc</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aspectj</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot4-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token整合 Redis -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token整合 Redis -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template-jdk-serializer</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t\t<!-- Sa-Token插件：权限缓存与业务缓存分离（Spring Boot 4） -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-alone-redis-by-spring-boot4</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/SaTokenAloneRedisSb4Application.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token 整合 SpringBoot4 示例，整合 alone-redis 插件（权限缓存与业务缓存分离）\n *\n * @author click33\n */\n@SpringBootApplication\npublic class SaTokenAloneRedisSb4Application {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenAloneRedisSb4Application.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 测试专用Controller，演示 alone-redis 权限缓存与业务缓存分离\n *\n * @author click33\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tStringRedisTemplate stringRedisTemplate;\n\n\t// 测试Sa-Token缓存，浏览器访问： http://localhost:8083/test/login\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue = \"10001\") String id) {\n\t\tSystem.out.println(\"--------------- 测试Sa-Token缓存\");\n\t\tStpUtil.login(id);\n\t\treturn SaResult.ok();\n\t}\n\n\t// 测试业务缓存，浏览器访问： http://localhost:8083/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"--------------- 测试业务缓存\");\n\t\tstringRedisTemplate.opsForValue().set(\"hello\", \"Hello World\");\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-alone-redis-sb4/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8083\n\n# Sa-Token配置\nsa-token:\n    # Token名称 (同时也是cookie名称)\n    token-name: satoken\n    # Token有效期，单位s 默认30天, -1代表永不过期\n    timeout: 2592000\n    # Token风格\n    token-style: uuid\n    # 配置Sa-Token单独使用的Redis连接\n    alone-redis:\n        # Redis数据库索引（默认为0）\n        database: 2\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        # password:\n        # 连接超时时间（毫秒）\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n\n# 配置业务使用的Redis连接\nspring:\n    data:\n        redis:\n            # Redis数据库索引（默认为0）\n            database: 0\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # 连接超时时间\n            timeout: 10s\n            lettuce:\n                pool:\n                    # 连接池最大连接数\n                    max-active: 200\n                    # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                    max-wait: -1ms\n                    # 连接池中的最大空闲连接\n                    max-idle: 10\n                    # 连接池中的最小空闲连接\n                    min-idle: 0\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-apikey</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t<artifactId>commons-pool2</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 整合 API Key -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-apikey</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 热刷新 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/SaTokenApiKeyApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaTokenApiKeyApplication {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenApiKeyApplication.class, args);\n\t\tSystem.out.println(\"启动成功：Sa-Token 配置如下：\" + SaManager.getConfig());\n\t\tSystem.out.println(\"启动成功：API Key  配置如下：\" + SaApiKeyManager.getConfig());\n\t\tSystem.out.println(\"测试访问：http://localhost:8081/index.html\");\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyDataLoaderImpl.java",
    "content": "package com.pj.mock;\n\nimport cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader;\nimport cn.dev33.satoken.apikey.model.ApiKeyModel;\nimport org.springframework.beans.factory.annotation.Autowired;\n\n/**\n * API Key 数据加载器实现类 （从数据库查询）\n *\n * @author click33\n * @since 2025/4/4\n */\n//@Component  // 打开此注解后，springboot 会自动注入此组件，打开 Sa-Token API Key 模块的数据库模式\npublic class SaApiKeyDataLoaderImpl implements SaApiKeyDataLoader {\n\n    @Autowired\n    SaApiKeyMockMapper apiKeyMockMapper;\n\n    // 指定框架不再维护 API Key 索引信息，而是由我们手动从数据库维护\n    @Override\n    public Boolean getIsRecordIndex() {\n        return false;\n    }\n\n    // 根据 apiKey 从数据库获取 ApiKeyModel 信息 （实现此方法无需为数据做缓存处理，框架内部已包含缓存逻辑）\n    @Override\n    public ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) {\n        return apiKeyMockMapper.getApiKeyModel(apiKey);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/mock/SaApiKeyMockMapper.java",
    "content": "package com.pj.mock;\n\nimport cn.dev33.satoken.apikey.model.ApiKeyModel;\nimport org.springframework.stereotype.Component;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 模拟数据库操作类\n *\n * @author click33\n * @since 2025/4/4\n */\n@Component\npublic class SaApiKeyMockMapper {\n\n    // 添加模拟测试数据\n    public static final Map<String, ApiKeyModel> map = new HashMap<>();\n    static {\n        ApiKeyModel ak1 = new ApiKeyModel();\n        ak1.setLoginId(10001);  // 设置绑定的用户 id\n        ak1.setApiKey(\"AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp\");  // 设置 API Key 值\n        ak1.setTitle(\"test\");\t  // 设置名称\n        ak1.setExpiresTime(System.currentTimeMillis() + 2592000);  // 设置失效时间，13位时间戳，-1=永不失效\n        map.put(ak1.getApiKey(), ak1);\n\n        ApiKeyModel ak2 = new ApiKeyModel();\n        ak2.setLoginId(10001);  // 设置绑定的用户 id\n        ak2.setApiKey(\"AK-NxcO63u57zbOWCmLaiVQuVWXssRwAxFcAxcFF\");  // 设置 API Key 值\n        ak2.setTitle(\"commit2\");\t  // 设置名称\n        ak1.addScope(\"commit\", \"pull\");  // 设置权限范围\n        ak2.setExpiresTime(System.currentTimeMillis() + 2592000);  // 设置失效时间，13位时间戳，-1=永不失效\n        map.put(ak2.getApiKey(), ak2);\n    }\n\n    // 返回指定 API Key 对应的 ApiKeyModel\n    public ApiKeyModel getApiKeyModel(String apiKey) {\n        return map.get(apiKey);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/GlobalException.java",
    "content": "package com.pj.satoken;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\n    /**\n     * 注册 Sa-Token 拦截器打开注解鉴权功能\n     */\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        registry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n    }\n\n    /**\n     * 注册 [Sa-Token 全局过滤器]\n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n\n                // 指定 [拦截路由] 与 [放行路由]\n                .addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n\n                // 认证函数: 每次请求执行\n                .setAuth(obj -> {\n                    // 输出 API 请求日志，方便调试代码\n                    // SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n\n                })\n\n                // 异常处理函数：每次认证函数发生异常时执行此函数\n                .setError(e -> {\n                    System.out.println(\"---------- sa全局异常 \");\n                    e.printStackTrace();\n                    return SaResult.error(e.getMessage());\n                })\n\n                // 前置函数：在每次认证函数之前执行\n                .setBeforeAuth(obj -> {\n                    // ---------- 设置一些安全响应头 ----------\n                    SaHolder.getResponse()\n                            // 允许指定域访问跨域资源\n                            .setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                            // 允许所有请求方式\n                            .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                            // 有效时间\n                            .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                            // 允许的header参数\n                            .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n                    // 如果是预检请求，则立即返回到前端\n                    SaRouter.match(SaHttpMethod.OPTIONS)\n                            .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                            .back();\n                })\n                ;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.apikey.model.ApiKeyModel;\nimport cn.dev33.satoken.apikey.template.SaApiKeyUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n/**\n * API Key 相关接口\n *\n * @author click33\n */\n@RestController\npublic class ApiKeyController {\n\n\t// 返回当前登录用户拥有的 ApiKey 列表\n\t@RequestMapping(\"/myApiKeyList\")\n\tpublic SaResult myApiKeyList() {\n\t\tList<ApiKeyModel> apiKeyList = SaApiKeyUtil.getApiKeyList(StpUtil.getLoginId());\n\t\treturn SaResult.data(apiKeyList);\n\t}\n\n\t// 创建一个新的 ApiKey，并返回\n\t@RequestMapping(\"/createApiKey\")\n\tpublic SaResult createApiKey() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.createApiKeyModel(StpUtil.getLoginId()).setTitle(\"test\");\n\t\tSaApiKeyUtil.saveApiKey(akModel);\n\t\treturn SaResult.data(akModel);\n\t}\n\n\t// 修改 ApiKey\n\t@RequestMapping(\"/updateApiKey\")\n\tpublic SaResult updateApiKey(ApiKeyModel akModel) {\n\t\t// 先验证一下是否为本人的 ApiKey\n\t\tSaApiKeyUtil.checkApiKeyLoginId(akModel.getApiKey(), StpUtil.getLoginId());\n\t\t// 修改\n\t\tApiKeyModel akModel2 = SaApiKeyUtil.getApiKey(akModel.getApiKey());\n\t\takModel2.setTitle(akModel.getTitle());\n\t\takModel2.setExpiresTime(akModel.getExpiresTime());\n\t\takModel2.setIsValid(akModel.getIsValid());\n\t\takModel2.setScopes(akModel.getScopes());\n\t\tSaApiKeyUtil.saveApiKey(akModel2);\n\t\treturn SaResult.ok();\n\t}\n\n\t// 删除 ApiKey\n\t@RequestMapping(\"/deleteApiKey\")\n\tpublic SaResult deleteApiKey(String apiKey) {\n\t\tSaApiKeyUtil.checkApiKeyLoginId(apiKey, StpUtil.getLoginId());\n\t\tSaApiKeyUtil.deleteApiKey(apiKey);\n\t\treturn SaResult.ok();\n\t}\n\n\t// 删除当前用户所有 ApiKey\n\t@RequestMapping(\"/deleteMyAllApiKey\")\n\tpublic SaResult deleteMyAllApiKey() {\n\t\tSaApiKeyUtil.deleteApiKeyByLoginId(StpUtil.getLoginId());\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/ApiKeyResourcesController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.apikey.annotation.SaCheckApiKey;\nimport cn.dev33.satoken.apikey.model.ApiKeyModel;\nimport cn.dev33.satoken.apikey.template.SaApiKeyUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * API Key 资源 相关接口\n *\n * @author click33\n */\n@RestController\npublic class ApiKeyResourcesController {\n\n\t// 必须携带有效的 ApiKey 才能访问\n\t@SaCheckApiKey\n\t@RequestMapping(\"/akRes1\")\n\tpublic SaResult akRes1() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n\t// 必须携带有效的 ApiKey ，且具有 userinfo 权限\n\t@SaCheckApiKey(scope = \"userinfo\")\n\t@RequestMapping(\"/akRes2\")\n\tpublic SaResult akRes2() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n\t// 必须携带有效的 ApiKey ，且同时具有 userinfo、chat 权限\n\t@SaCheckApiKey(scope = {\"userinfo\", \"chat\"})\n\t@RequestMapping(\"/akRes3\")\n\tpublic SaResult akRes3() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n\t// 必须携带有效的 ApiKey ，且具有 userinfo、chat 其中之一权限\n\t@SaCheckApiKey(scope = {\"userinfo\", \"chat\"}, mode = SaMode.OR)\n\t@RequestMapping(\"/akRes4\")\n\tpublic SaResult akRes4() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 登录 Controller\n *\n * @author click33\n */\n@RestController\npublic class LoginController {\n\n\t// 登录 \n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tStpUtil.login(id);\n\t\treturn SaResult.ok().set(\"satoken\", StpUtil.getTokenValue());\n\t}\n\n\t// 查询当前登录人\n\t@RequestMapping(\"getLoginId\")\n\tpublic SaResult getLoginId() {\n\t\treturn SaResult.data(StpUtil.getLoginId());\n\t}\n\n\t// 注销 \n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n\n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token:\n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # 开启日志信息\n    is-log: true\n    # API Key 相关配置\n    api-key:\n        # API Key 前缀\n        prefix: AK-\n        # API Key 有效期，-1=永久有效，默认30天 （修改此配置项不会影响到已创建的 API Key）\n        timeout: 2592000\n        # 框架是否记录索引信息\n        is-record-index: true\n\nspring:\n    # redis配置\n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password:\n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/resources/static/common.js",
    "content": "// 服务器接口主机地址\nvar baseUrl = \"http://localhost:8081\";\n\n// 封装一下Ajax\nfunction ajax(path, data, successFn, errorFn) {\n\tconsole.log(baseUrl + path);\n\tfetch(baseUrl + path, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t'satoken': localStorage.getItem('satoken')\n\t\t\t},\n\t\t\tbody: serializeToQueryString(data),\n\t\t})\n\t\t.then(response => response.json())\n\t\t.then(res => {\n\t\t\tconsole.log('返回数据：', res);\n\t\t\tif(res.code == 200) {\n\t\t\t\tsuccessFn(res);\n\t\t\t} else {\n\t\t\t\tif(errorFn) {\n\t\t\t\t\terrorFn(res);\n\t\t\t\t} else {\n\t\t\t\t\tshowMsg('错误：' + res.msg);\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t.catch(error => {\n\t\t\tconsole.error('请求失败:', error);\n\t\t\treturn alert(\"异常：\" + JSON.stringify(error));\n\t\t});\n}\n\n\n// ------------ 工具方法 ---------------\n\n// 从url中查询到指定名称的参数值\nfunction getParam(name, defaultValue) {\n\tvar query = window.location.search.substring(1);\n\tvar vars = query.split(\"&\");\n\tfor (var i = 0; i < vars.length; i++) {\n\t\tvar pair = vars[i].split(\"=\");\n\t\tif (pair[0] == name) {\n\t\t\treturn pair[1];\n\t\t}\n\t}\n\treturn (defaultValue == undefined ? null : defaultValue);\n}\n\n// 将 json 对象序列化为kv字符串，形如：name=Joh&age=30&active=true\nfunction serializeToQueryString(obj) {\n\treturn Object.entries(obj)\n\t\t.filter(([_, value]) => value != null) // 过滤 null 和 undefined\n\t\t.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n\t\t.join('&');\n}\n\n// 随机生成字符串\nfunction randomString(len) {\n\tlen = len || 32;\n\tvar $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';\n\tvar maxPos = $chars.length;\n\tvar str = '';\n\tfor (i = 0; i < len; i++) {\n\t\tstr += $chars.charAt(Math.floor(Math.random() * maxPos));\n\t}\n\treturn str;\n}\n\n// 带动画的弹出提示 \nfunction showMsg(message) {\n\tconst alertBox = document.createElement('div');\n\n\t// 初始样式（包含隐藏状态）\n\tObject.assign(alertBox.style, {\n\t\tposition: 'fixed',\n\t\tleft: '50%',\n\t\ttop: '50%',\n\t\ttransform: 'translate(-50%, -50%) scale(0.8) translateY(-30px)', // 初始缩放+位移\n\t\topacity: '0',\n\t\tbackground: 'rgba(0, 0, 0, 0.85)',\n\t\tcolor: 'white',\n\t\tpadding: '16px 32px',\n\t\tborderRadius: '8px',\n\t\ttransition: 'all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55)', // 弹性动画曲线\n\t\tpointerEvents: 'none',\n\t\twhiteSpace: 'nowrap',\n\t\tfontSize: '16px',\n\t\tboxShadow: '0 4px 12px rgba(0,0,0,0.25)' // 添加投影增强立体感\n\t});\n\talertBox.textContent = message;\n\n\tdocument.body.appendChild(alertBox);\n\n\t// 强制重绘确保动画触发\n\tvoid alertBox.offsetHeight;\n\n\t// 应用入场动画\n\tObject.assign(alertBox.style, {\n\t\topacity: '1',\n\t\ttransform: 'translate(-50%, -50%) scale(1) translateY(-20px)'\n\t});\n\n\t// 自动消失逻辑\n\tsetTimeout(() => {\n\t\tObject.assign(alertBox.style, {\n\t\t\topacity: '0',\n\t\t\ttransform: 'translate(-50%, -50%) scale(0.9) translateY(-20px)'\n\t\t});\n\n\t\talertBox.addEventListener('transitionend', () => {\n\t\t\talertBox.remove();\n\t\t}, {\n\t\t\tonce: true\n\t\t});\n\t}, 3000);\n}\n\n// 将日期格式化  yyyy-MM-dd HH:mm:ss\nfunction formatDateTime(date) {\n\tdate = new Date(date);\n\t// 补零函数\n\tconst pad = (n, len) => n.toString().padStart(len, '0');\n\n\t// 分解时间组件\n\tconst year = date.getFullYear();\n\tconst month = pad(date.getMonth() + 1, 2); // 0-11 → 1-12\n\tconst day = pad(date.getDate(), 2);\n\tconst hours = pad(date.getHours(), 2); // 24小时制\n\tconst minutes = pad(date.getMinutes(), 2);\n\tconst seconds = pad(date.getSeconds(), 2);\n\tconst milliseconds = pad(date.getMilliseconds(), 3);\n\n\t// 拼接格式\n\t// return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;\n\treturn `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-apikey/src/main/resources/static/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Title</title>\n    <style>\n\t\tbody{background-color: #EFF6FF;}\n\t\ttd{padding: 5px 10px;}\n\t\tbutton{cursor: pointer;}\n\t\ttable{margin-top: 10px; width: 100%;}\n\t\t[name=title]{width: 100px;}\n\t\t.change-tr{background-color: #F5E5F5  ;}\n\t\t.remark{margin-left: 10px; color: #999;}\n    </style>\n</head>\n<body>\n    <div style=\"width: 1200px; margin: auto;\">\n        <h1>Sa-Token - API Key 测试页</h1>\n        <h2>登录</h2>\n        <div>当前登录人：<b class=\"curr-uid\" style=\"color: green\"></b></div>\n        <span>输入账号 id 登录：</span>\n        <input name=\"loginId\" />\n        <button onclick=\"doLogin()\">登录</button>\n        <button onclick=\"doLogout()()\">注销</button>\n\n        <h2>API Key 列表</h2>\n\t\t<button onclick=\"createApiKey()\">+ 创建 API Key</button>\n        <table cellspacing=\"0\" border=\"1\">\n            <tr>\n                <th>名称</th>\n                <th style=\"width: 435px;\">API Key</th>\n                <th>权限(多个用逗号隔开)</th>\n                <th style=\"width: 190px;\">过期时间</th>\n                <th style=\"width: 80px;\">是否生效</th>\n                <th style=\"width: 170px;\">操作</th>\n            </tr>\n            <tbody class=\"ak-tbody\">\n                <!-- <tr class=\"ak-xxxx\">\n                    <td><input name=\"title\" value=\"xx\" /></td>\n                    <td>AK-EG9BKM4bel7OqRoixNvSQ1a6DYusNfEXDjPr</td>\n\t\t\t\t\t<td><input name=\"scopes\" value=\"aaa\" /></td>\n\t\t\t\t\t<td><input name=\"expiresTime\" value=\"2020-02-02 01:50:20\" type=\"datetime-local\" /></td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<label><input name=\"isValid\" checked type=\"checkbox\" />生效</label>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<button onclick=\"updateApiKey('xxx')\">修改</button>\n\t\t\t\t\t\t<button onclick=\"useApiKey('xxx')\">使用</button>\n\t\t\t\t\t\t<button onclick=\"deleteApiKey('xxx')\">删除</button>\n\t\t\t\t\t</td>\n                </tr> -->\n            </tbody>\n        </table>\n\t\t\n        <h2>调用 API</h2>\n        <div style=\"line-height: 30px;\">\n\t\t\t<span>使用的 API Key：</span>\n\t\t\t<input name=\"api-key\" style=\"width: 600px;\"/> <br>\n\t\t\t<button onclick=\"callAPI('/akRes1')\">调用接口 1 </button> <span class=\"remark\">需要正确的 API Key</span> <br>\n\t\t\t<button onclick=\"callAPI('/akRes2')\">调用接口 2 </button> <span class=\"remark\">需要具备 Scope: userinfo</span> <br>\n\t\t\t<button onclick=\"callAPI('/akRes3')\">调用接口 3 </button> <span class=\"remark\">需要具备 Scope: userinfo,chat (需要全部具备)</span> <br>\n\t\t\t<button onclick=\"callAPI('/akRes4')\">调用接口 4 </button> <span class=\"remark\">需要具备 Scope: userinfo,chat (具备其一即可)</span> <br>\n\t\t</div>\n\t\t\n\t\t<div style=\"height: 200px;\"></div>\n\t\t\n    </div>\n    <script src=\"common.js\"></script>\n    <script>\n        // 登录\n        function doLogin() {\n            var loginId = document.querySelector(\"[name=loginId]\").value;\n            if (loginId === \"\") {\n                return alert(\"请输入账号 id\");\n            }\n            ajax(\"/login\", {id: loginId}, function (res) {\n                localStorage.setItem(\"satoken\", res.satoken);\n                showMsg('登录成功');\n\t\t\t\tsetTimeout(function(){\n\t\t\t\t\tlocation.reload();\n\t\t\t\t}, 1000);\n            })\n        }\n        // 查询当前登录人\n        function getLoginInfo() {\n            ajax(\"/getLoginId\", {}, function (res) {\n\t\t\t\tdocument.querySelector(\".curr-uid\").innerHTML = res.data;\n\t\t\t\tdocument.querySelector('[name=loginId]').value = res.data;\n\t\t\t\tmyApiKeyList();\n            }, function(){\n\t\t\t\tdocument.querySelector(\".curr-uid\").innerHTML = '未登录';\n\t\t\t\tdocument.querySelector('[name=loginId]').value = '10001';\n\t\t\t})\n        }\n        getLoginInfo();\n        // 注销登录 \n        function doLogout() {\n            ajax(\"/logout\", {}, function (res) {\n                showMsg('注销成功');\n\t\t\t\tsetTimeout(function(){\n\t\t\t\t\tlocation.reload();\n\t\t\t\t}, 1000)\n            })\n        }\n    </script>\n\t<script>\n\t\t// 渲染一个 API Key 对象到表格 \n\t\tfunction renderApiKey(ak) {\n\t\t\tconst trDom = `\n\t\t\t\t<tr class=\"ak-${ak.apiKey}\">\n\t\t\t\t\t<td><input name=\"title\" value=\"${ak.title}\" oninput=\"changeTr('${ak.apiKey}')\"/></td>\n\t\t\t\t\t<td>${ak.apiKey}</td>\n\t\t\t\t\t<td><input name=\"scopes\" value=\"${ak.scopes.join(',')}\" oninput=\"changeTr('${ak.apiKey}')\" /></td>\n\t\t\t\t\t<td><input name=\"expiresTime\" value=\"${formatDateTime(ak.expiresTime)}\" type=\"datetime-local\" oninput=\"changeTr('${ak.apiKey}')\"/></td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<label><input name=\"isValid\" ${ak.isValid ? 'checked' : ''} type=\"checkbox\" oninput=\"changeTr('${ak.apiKey}')\"/>生效</label>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<button onclick=\"updateApiKey('${ak.apiKey}')\">修改</button>\n\t\t\t\t\t\t<button onclick=\"useApiKey('${ak.apiKey}')\">使用</button>\n\t\t\t\t\t\t<button onclick=\"deleteApiKey('${ak.apiKey}')\">删除</button>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t`;\n\t\t\tdocument.querySelector('.ak-tbody').innerHTML = document.querySelector('.ak-tbody').innerHTML + trDom;\n\t\t}\n\t\t\n        // 查询当前所有 API Key\n        function myApiKeyList() {\n            ajax(\"/myApiKeyList\", {}, function (res) {\n                res.data.forEach(function(item){\n\t\t\t\t\trenderApiKey(item);\n\t\t\t\t})\n            })\n        }\n\t\t// 创建 ApiKey\n\t\tfunction createApiKey() {\n\t\t\tif(document.querySelector(\".curr-uid\").innerHTML === '未登录') {\n\t\t\t\treturn alert('请先登录');\n\t\t\t}\n\t\t\tajax(\"/createApiKey\", {}, function (res) {\n\t\t\t    renderApiKey(res.data);\n                showMsg('创建成功');\n\t\t\t})\n\t\t}\n\t\t// 使用 \n\t\tfunction useApiKey(apiKey){\n\t\t\tdocument.querySelector('[name=api-key]').value = apiKey;\n\t\t\tshowMsg('已填充至输入框，请调用接口');\n\t\t}\n\t\t// 修改 \n\t\tfunction updateApiKey(apiKey) {\n\t\t\tconst tr = document.querySelector(\".ak-\" + apiKey);\n\t\t\tconst data = {\n\t\t\t\tapiKey: apiKey,\n\t\t\t\ttitle: tr.querySelector('[name=title]').value,\n\t\t\t\tscopes: tr.querySelector('[name=scopes]').value,\n\t\t\t\texpiresTime: new Date(tr.querySelector('[name=expiresTime]').value).getTime(),\n\t\t\t\tisValid: tr.querySelector('[name=isValid]').checked,\n\t\t\t}\n\t\t\tajax(\"/updateApiKey\", data, function (res) {\n                showMsg('修改成功');\n\t\t\t\ttr.classList.remove('change-tr');\n\t\t\t})\n\t\t}\n\t\t// 删除 \n\t\tfunction deleteApiKey(apiKey) {\n\t\t\tajax(\"/deleteApiKey\", {apiKey: apiKey}, function (res) {\n                showMsg('删除成功');\n\t\t\t\tconst tr = document.querySelector(\".ak-\" + apiKey);\n\t\t\t\ttr.remove();\n\t\t\t})\n\t\t}\n\t\t\n\t\t// 指定行的输入框变动 \n\t\tfunction changeTr(apiKey) {\n\t\t\tconst tr = document.querySelector(\".ak-\" + apiKey);\n\t\t\ttr.classList.add('change-tr');\n\t\t}\n\t\t\n\t</script>\n\t<script>\n\t\t\n\t\t// 调用指定接口\n\t\tfunction callAPI(apiPath) {\n\t\t\tconst apiKey = document.querySelector('[name=api-key]').value;\n\t\t\tif(!apiKey) {\n\t\t\t\treturn showMsg('请先填写 API Key')\n\t\t\t}\n\t\t\tajax(apiPath, {apikey: apiKey}, function (res) {\n\t\t\t    showMsg(res.msg);\n\t\t\t})\n\t\t}\n\t\t\n\t</script>\n</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-async/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-async</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!--<version>2.3.0.RELEASE</version>-->\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<java.run.main.class>com.pj.SaTokenAsyncApplication</java.run.main.class>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-async/src/main/java/com/pj/SaTokenAsyncApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.scheduling.annotation.EnableAsync;\nimport org.springframework.scheduling.annotation.EnableScheduling;\n\n\n/**\n * Sa-Token 异步方案 测试\n * @author click33\n *\n */\n@EnableAsync    // 启用异步\n@EnableScheduling // 启动定时任务\n@SpringBootApplication\npublic class SaTokenAsyncApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenAsyncApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-async/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-async/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// 输出 API 请求日志，方便调试代码 \n        \t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n                   \n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n\n        \t\t;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.context.mock.SaTokenContextMockUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.scheduling.annotation.Async;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;\nimport org.springframework.web.bind.annotation.CookieValue;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.util.Date;\n\n/**\n * 测试几种场景的异步场景 Controller\n *\n * @author click33\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tpublic ThreadPoolTaskExecutor taskExecutor;\n\n\t// 【同步】登录  \t---- http://localhost:8081/test/login?id=10001\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue = \"10001\") long id) {\n\t\tStpUtil.login(id);\n\t\treturn SaResult.ok(\"登录成功\");\n\t}\n\n\t// 【同步】判断是否登录  \t--- http://localhost:8081/test/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n\t// 【同步】注销   浏览器访问： http://localhost:8081/test/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.data(null);\n\t}\n\n\t// 【异步】new Thread  \t--- http://localhost:8081/test/isLogin2\n\t@RequestMapping(\"isLogin2\")\n\tpublic SaResult isLogin2() {\n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\tString tokenValue = StpUtil.getTokenValue();\n\t\tnew Thread(() -> {\n\t\t\tSaTokenContextMockUtil.setMockContext(()->{\n\t\t\t\tStpUtil.setTokenValueToStorage(tokenValue);\n\t\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\t});\n\t\t}).start();\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n\t// 【异步】线程池 ThreadPoolTaskExecutor    \t--- http://localhost:8081/test/isLogin3\n\t@RequestMapping(\"isLogin3\")\n\tpublic SaResult isLogin3(HttpServletRequest request, HttpServletResponse response) {\n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\tString tokenValue = StpUtil.getTokenValue();\n\t\ttaskExecutor.execute(() -> {\n\t\t\tSaTokenContextMockUtil.setMockContext(()->{\n\t\t\t\tStpUtil.setTokenValueToStorage(tokenValue);\n\t\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\t});\n\t\t});\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n\t// 【异步】@Async    \t--- http://localhost:8081/test/isLogin4\n\t@Async\n\t@RequestMapping(\"isLogin4\")\n\tpublic SaResult isLogin4(@CookieValue(\"satoken\") String satoken) {\n\t\tSaTokenContextMockUtil.setMockContext(()->{\n\t\t\tStpUtil.setTokenValueToStorage(satoken);\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t});\n\t\treturn SaResult.ok();\n\t}\n\n\t// 【异步】定时任务\n\t@Scheduled(cron = \"0 * * * * ?\")  // 一分钟执行一次\n//\t@Scheduled(cron = \"0/10 * * * * ?\")  // 十秒执行一次\n\tpublic void scheduledMethod(){\n\t\t// 错误写法：直接调用 Sa-Token API 会报错\n\t\t// System.out.println(\"定时任务，Mock 范围外：是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(SaFoxUtil.formatDate(new Date()));\n\n\t\t// 需要先设置模拟上下文\n\t\tSaTokenContextMockUtil.setMockContext(() -> {\n\t\t\t// StpUtil.setTokenValueToStorage(\"f452571f-bfdb-413d-aba9-e26992cf07be\");   // 模拟 Token\n\t\t\tSystem.out.println(\"定时任务，Mock 范围内：是否登录：\" + StpUtil.isLogin());\n\t\t\t// 模拟登录\n//\t\t\tStpUtil.login(10066);   // 模拟 登录\n//\t\t\tSystem.out.println(\"定时任务，Mock 范围内：登录账号：\" + StpUtil.getLoginId());\n\t\t});\n\n\t}\n\n}\n\n\t/*\n\t 使用 InheritableThreadLocal 存储上下文带来的坑：\n\n\t\t@RequestMapping(\"isLogin2\")\n\t\tpublic SaResult isLogin2() {\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\n\t\t\tnew Thread(() -> {\n\t\t\t\ttry {\n\t\t\t\t\tThread.sleep(1000);\n\t\t\t\t} catch (InterruptedException e) {\n\t\t\t\t\tthrow new RuntimeException(e);\n\t\t\t\t}\n\t\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\t}).start();\n\n\t\t\treturn SaResult.data(null);\n\t\t}\n\n\t\t如果不 Thread.sleep(1000)，外面 true，里面 true\n\t\t如果 Thread.sleep(1000)，则外面 true，里面false\n\n\t\t因为 SpringBoot 会在请求结束后清除 request 里的数据，\n\t\t此时子线程内部可以读取到 request，但是 request 无值，导致代码既能成功运行，又逻辑错误，是一种难以排查的隐形 bug\n\t\t应该避免使用 InheritableThreadLocal 来存储上下文数据\n\n\t */\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-async/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n        \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password:\n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-beetl</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Beetl 视图引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>com.ibeetl</groupId>\n\t\t\t<artifactId>beetl-framework-starter</artifactId>\n\t\t\t<version>1.2.40.Beetl.RELEASE</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- 热刷新 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/SaTokenBeetlDemoApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n@SpringBootApplication\npublic class SaTokenBeetlDemoApplication {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenBeetlDemoApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功，Sa-Token 配置如下：\" + SaManager.getConfig());\n\t\tSystem.out.println(\"\\n测试访问：http://localhost:8081/\");\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport com.ibeetl.starter.BeetlTemplateCustomize;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\n\t// 为 Beetl 视图引擎注册自定义函数：\n\t// \t通过 stp.xxx() 调用 StpUtil.stpLogic 对象上所有 public 方法\n\t@Bean\n\tpublic BeetlTemplateCustomize beetlTemplateCustomize(){\n\t\treturn groupTemplate -> groupTemplate.registerFunctionPackage(\"stp\", StpUtil.stpLogic);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.ModelAndView;\n\n/**\n * 测试 Controller\n * @author click33\n *\n */\n@RestController\npublic class TestController {\n\n\t// 首页 \n\t@RequestMapping(\"/\")\n\tpublic Object index() {\n\t\treturn new ModelAndView(\"index.btl\");\n\t}\n\t\n\t// 登录 \n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tStpUtil.login(id);\n\t\tStpUtil.getSession().set(\"name\", \"zhangsan\");\n\t\treturn SaResult.ok();\n\t}\n\n\t// 注销 \n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-beetl/src/main/resources/templates/index.btl",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<title>Sa-Token 集成 Beetl 示例</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\" style=\"padding: 30px;\">\n\t\t\t<h2>Sa-Token 集成 Beetl —— 测试页面</h2>\n\t\t\t<p>当前是否登录：${stp.isLogin()}</p>\n\t\t\t<p>\n\t\t\t\t<span>操作：</span>\n\t\t\t\t<a href=\"login\" target=\"_blank\">登录</a>\n\t\t\t\t<a href=\"logout\" target=\"_blank\">注销</a>\n\t\t\t</p>\n\n\t\t\t<p>\n\t\t\t\t登录之后才能显示：\n\t\t\t\t<% if(stp.isLogin()){ %>\n\t\t\t\t\tvalue\n\t\t\t\t<%}%>\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t不登录才能显示：\n\t\t\t\t<% if(!stp.isLogin()){ %>\n\t\t\t\t\tvalue\n\t\t\t\t<%}%>\n\t\t\t</p>\n\n\t\t\t<p>具有角色 admin 才能显示：<% if(stp.hasRole(\"admin\")){ %> value <% } %></p>\n\t\t\t<p>同时具备多个角色才能显示：<% if(stp.hasRoleAnd(\"admin\", \"ceo\", \"cto\")){ %> value <% } %></p>\n\t\t\t<p>只要具有其中一个角色就能显示：<% if(stp.hasRoleOr(\"admin\", \"ceo\", \"cto\")){ %> value <% } %></p>\n\t\t\t<p>不具有角色 admin 才能显示：<% if(!stp.hasRole(\"admin\")){ %> value <% } %></p>\n\n\t\t\t<p>具有权限 user-add 才能显示：<% if(stp.hasPermission(\"user-add\")){ %> value <% } %></p>\n\t\t\t<p>同时具备多个权限才能显示：<% if(stp.hasPermissionAnd(\"user-add\", \"user-delete\", \"user-get\")){ %> value <% } %></p>\n\t\t\t<p>只要具有其中一个权限就能显示：<% if(stp.hasPermissionOr(\"user-add\", \"user-delete\", \"user-get\")){ %> value <% } %></p>\n\t\t\t<p>不具有权限 user-add 才能显示：<% if(!stp.hasPermission(\"user-add\")){ %> value <% } %></p>\n\n\t\t\t<% if(stp.isLogin()){ %>\n\t\t\t\t<p>\n\t\t\t\t\t从SaSession中取值：${stp.getSession()[\"name\"]} <br>\n\t\t\t\t\t或：${stp.getSession().name}\n\t\t\t\t</p>\n\t\t\t<%}%>\n\n\t\t</div>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-bom-import/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-bom-import</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n        </dependency>\n\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n        </dependency>\n\n\t\t<!-- Sa-Token整合 jwt -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jwt</artifactId>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\n\n\t<!-- 依赖管理 (不会真正的引入依赖，只会限定其版本) -->\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<!-- Sa-Token 所有包的版本定义 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-bom</artifactId>\n\t\t\t\t<version>1.45.0</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n\t\t</dependencies>\n\t</dependencyManagement>\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/SaTokenDemoApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.jwt.StpLogicJwtForSimple;\nimport cn.dev33.satoken.stp.StpLogic;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * Sa-Token 使用 bom 包引入框架\n */\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n\t// Sa-Token 整合 jwt (Simple 简单模式)\n\t@Bean\n\tpublic StpLogic getStpLogicJwt() {\n\t\treturn new StpLogicJwtForSimple();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e){\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-bom-import/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\tStpUtil.login(10001);\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\tStpUtil.checkLogin();\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-bom-import/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n    # jwt秘钥\n    jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-caffeine/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-caffeine</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 Caffeine -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-caffeine</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/SaTokenDemoApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token 整合 Caffeine 示例\n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t\tSystem.out.println(SaManager.getSaTokenDao());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-caffeine/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-caffeine/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-case</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 注解鉴权使用 EL 表达式 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-el</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t\t<!-- Sa-Token 临时 token 模块整合 jwt -->\n\t\t<!--<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-temp-jwt</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>-->\n\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/SaTokenCaseApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token 示例 \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenCaseApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenCaseApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/more/SaCheckELController.java",
    "content": "package com.pj.cases.more;\n\nimport cn.dev33.satoken.annotation.SaCheckEL;\nimport cn.dev33.satoken.annotation.SaIgnore;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * SaCheckEL EL表达式注解鉴权示例\n *\n * @author click33\n * @since 2022-10-13\n */\n@RestController\n@RequestMapping(\"/check-el/\")\npublic class SaCheckELController {\n\n\t// 登录校验  ---- http://localhost:8081/check-el/test1\n\t@SaCheckEL(\"stp.checkLogin()\")\n\t@RequestMapping(\"test1\")\n\tpublic SaResult test1() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色校验  ---- http://localhost:8081/check-el/test2\n\t@SaCheckEL(\"stp.checkRole('dev-admin')\")\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限校验  ---- http://localhost:8081/check-el/test3\n\t@SaCheckEL(\"stp.checkPermission('user:edit')\")\n\t@RequestMapping(\"test3\")\n\tpublic SaResult test3() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 二级认证  ---- http://localhost:8081/check-el/test4\n\t@SaCheckEL(\"stp.checkSafe()\")\n\t@RequestMapping(\"test4\")\n\tpublic SaResult test4() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 参数长度校验  ---- http://localhost:8081/check-el/test5?name=zhangsan\n\t@SaCheckEL(\"NEED( #name.length() > 3 )\")\n\t@RequestMapping(\"test5\")\n\tpublic SaResult test5(@RequestParam(defaultValue = \"\") String name) {\n\t\treturn SaResult.ok().set(\"name\", name);\n\t}\n\n\t// 参数长度校验，并自定义异常描述信息  ---- http://localhost:8081/check-el/test6?name=z\n\t@SaCheckEL(\"NEED( #name !=null && #name.length() > 3, 'name长度不够' )\")\n\t@RequestMapping(\"test6\")\n\tpublic SaResult test6(String name) {\n\t\treturn SaResult.ok().set(\"name\", name);\n\t}\n\n\t// 已登录, 或者查询数据在公开范围内 ---- http://localhost:8081/check-el/test7?id=10044\n\t@SaCheckEL(\"NEED( stp.isLogin() or (#id != null and #id > 10010) )\")\n\t@RequestMapping(\"test7\")\n\tpublic SaResult test7(long id) {\n\t\treturn SaResult.ok().set(\"id\", id);\n\t}\n\n\t// SaSession 里取值校验 ---- http://localhost:8081/check-el/test8\n\t@SaCheckEL(\"NEED( stp.getSession().get('name') == 'zhangsan' )\")\n\t@RequestMapping(\"test8\")\n\tpublic SaResult test8() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 多账号体系鉴权测试 ---- http://localhost:8081/check-el/test9\n\t@SaCheckEL(\"stpUser.checkLogin()\")\n\t@RequestMapping(\"test9\")\n\tpublic SaResult test9() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 本模块需要鉴权的权限码\n\tpublic String permissionCode = \"article:add\";\n\n\t// 调用本类的成员变量 ---- http://localhost:8081/check-el/test10\n\t@SaCheckEL(\"stp.checkPermission( this.permissionCode )\")\n\t@RequestMapping(\"test10\")\n\tpublic SaResult test10() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 忽略鉴权测试 ---- http://localhost:8081/check-el/test11\n\t@SaIgnore\n\t@SaCheckEL(\"stp.checkPermission( 'abc' )\")\n\t@RequestMapping(\"test11\")\n\tpublic SaResult test11() {\n\t\treturn SaResult.ok();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java",
    "content": "package com.pj.cases.plugin;\n\nimport cn.dev33.satoken.temp.SaTempUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n/**\n * 测试专用 Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/temp-token/\")\npublic class TempTokenController {\n\n\t// 创建   浏览器访问： http://localhost:8081/temp-token/create\n\t@RequestMapping(\"create\")\n\tpublic SaResult create() {\n\t\tString token = SaTempUtil.createToken(10001, 1200);\n\t\tSystem.out.println(\"创建成功：\" + token);\n\t\treturn SaResult.data(token);\n\t}\n\n\t// 创建，获取时裁剪前缀   浏览器访问： http://localhost:8081/temp-token/create2\n\t@RequestMapping(\"create2\")\n\tpublic SaResult create2() {\n\t\tString token = SaTempUtil.createToken(\"shop_\" + 1001, 1200);\n\t\tSystem.out.println(\"创建成功：\" + token);\n\t\tSystem.out.println(\"获取对应值：\" + SaTempUtil.parseToken(token));\n\t\tSystem.out.println(\"获取对应值，并裁剪前缀：\" + SaTempUtil.parseToken(token, \"shop_\", Long.class));\n\t\tSystem.out.println(\"指定错误前缀来获取：\" + SaTempUtil.parseToken(token, \"art_\", Long.class));\n\t\treturn SaResult.data(token);\n\t}\n\n\t// 创建，回收   浏览器访问： http://localhost:8081/temp-token/create3\n\t@RequestMapping(\"create3\")\n\tpublic SaResult create3() {\n\t\tString token = SaTempUtil.createToken(10003, 1200);\n\t\tSystem.out.println(\"创建成功：\" + token);\n\t\tSystem.out.println(\"获取对应值：\" + SaTempUtil.parseToken(token));\n\t\tSaTempUtil.deleteToken(token);\n\t\tSystem.out.println(\"回收后再获取：\" + SaTempUtil.parseToken(token));\n\t\treturn SaResult.data(token);\n\t}\n\n\t// 创建，记录索引   浏览器访问： http://localhost:8081/temp-token/create4\n\t@RequestMapping(\"create4\")\n\tpublic SaResult create4() {\n\t\tString token1 = SaTempUtil.createToken(10004, 1200, true);\n\t\tString token2 = SaTempUtil.createToken(10004, 1300, true);\n\t\tString token3 = SaTempUtil.createToken(10004, -1, true);\n\n\t\tSystem.out.println(\"token1 剩余有效期：\" + SaTempUtil.getTimeout(token1));\n\t\tSystem.out.println(\"token2 剩余有效期：\" + SaTempUtil.getTimeout(token2));\n\t\tSystem.out.println(\"token3 剩余有效期：\" + SaTempUtil.getTimeout(token3));\n\n\t\tSaTempUtil.deleteToken(token3);\n\n\t\t// 获取已创建的 token 列表\n\t\tSystem.out.println(\"获取已创建的 token 列表 \");\n\t\tList<String> tempTokenList = SaTempUtil.getTempTokenList(10004);\n\t\tSystem.out.println(tempTokenList);\n\t\treturn SaResult.data(token1);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/test/TestController.java",
    "content": "package com.pj.cases.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 测试专用 Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\treturn SaResult.ok(); \n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/DisableController.java",
    "content": "package com.pj.cases.up;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 账号封禁示例 \n * \n * @author click33\n * @since 2022-10-17 \n */\n@RestController\n@RequestMapping(\"/disable/\")\npublic class DisableController {\n\n\t/*\n\t * 测试步骤：\n\t \t1、访问登录接口，可以正常登录    ---- http://localhost:8081/disable/login?userId=10001\n\t \t2、注销登录    ---- http://localhost:8081/disable/logout\n\t \t3、禁用账号    ---- http://localhost:8081/disable/disable?userId=10001\n\t \t4、再次访问登录接口，登录失败    ---- http://localhost:8081/disable/login?userId=10001\n\t \t5、解封账号    ---- http://localhost:8081/disable/untieDisable?userId=10001\n\t \t6、再次访问登录接口，登录成功    ---- http://localhost:8081/disable/login?userId=10001\n\t */\n\n\t// 会话登录接口  ---- http://localhost:8081/disable/login?userId=10001\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(long userId) {\n\t\t// 1、先检查此账号是否已被封禁 \n\t\tStpUtil.checkDisable(userId);\n\t\t// 2、检查通过后，再登录 \n\t\tStpUtil.login(userId);\n\t\treturn SaResult.ok(\"账号登录成功\");\n\t}\n\n\t// 会话注销接口  ---- http://localhost:8081/disable/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok(\"账号退出成功\");\n\t}\n\n\t// 封禁指定账号  ---- http://localhost:8081/disable/disable?userId=10001\n\t@RequestMapping(\"disable\")\n\tpublic SaResult disable(long userId) {\n\t\t/*\n\t\t * 账号封禁：\n\t\t * \t参数1：要封禁的账号id\n\t\t * \t参数2：要封禁的时间，单位：秒，86400秒=1天\n\t\t */\n\t\tStpUtil.disable(userId, 86400);\n\t\treturn SaResult.ok(\"账号 \" + userId + \" 封禁成功\");\n\t}\n\n\t// 解封指定账号  ---- http://localhost:8081/disable/untieDisable?userId=10001\n\t@RequestMapping(\"untieDisable\")\n\tpublic SaResult untieDisable(long userId) {\n\t\tStpUtil.untieDisable(userId);\n\t\treturn SaResult.ok(\"账号 \" + userId + \" 解封成功\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/HttpBasicController.java",
    "content": "package com.pj.cases.up;\n\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * Sa-Token Http Basic 认证 \n * \n * @author click33\n * @since 2022-10-17 \n */\n@RestController\n@RequestMapping(\"/basic/\")\npublic class HttpBasicController {\n\n\t/*\n\t * 测试步骤：\n\t \t1、访问资源接口，被拦截，无法返回数据信息    ---- http://localhost:8081/basic/getInfo\n\t \t2、浏览器弹出窗口，要求输入账号密码，输入：账号=sa，密码=123456，确认\n\t \t3、后端返回数据信息\n\t \t4、后续再次访问接口时，无需重复输入账号密码 \n\t */\n\n\t// 资源接口  ---- http://localhost:8081/basic/getInfo\n\t@RequestMapping(\"getInfo\")\n\tpublic SaResult login() {\n\t\t// 1、Http Basic 认证校验，账号=sa，密码=123456\n\t\tSaHttpBasicUtil.check(\"sa:123456\");\n\t\t\n\t\t// 2、返回数据 \n\t\tString data = \"这是通过 Http Basic 校验后才返回的数据\";\n\t\treturn SaResult.data(data);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java",
    "content": "package com.pj.cases.up;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * Sa-Token 同端互斥登录示例 \n * \n * @author click33\n * @since 2022-10-17 \n */\n@RestController\n@RequestMapping(\"/mutex/\")\npublic class MutexLoginController {\n\n\t/*\n\t * 前提1：准备至少四个浏览器：A、B、C、D\n\t * 前提2：配置文件需要把 sa-token.is-concurrent 的值改为 false \n\t * \n\t * 测试步骤：\n\t \t1、在浏览器A上登录账号10001，设备为PC    ---- http://localhost:8081/mutex/login?userId=10001&device=PC\n\t \t\t检查是否登录成功，返回true：\t\t\t---- http://localhost:8081/mutex/isLogin\n\t \t\t\n\t \t2、在浏览器B上登录账号10001，设备为APP    ---- http://localhost:8081/mutex/login?userId=10001&device=APP\n\t \t\t检查是否登录成功，返回true：\t\t\t---- http://localhost:8081/mutex/isLogin\n\t \t\t\n\t \t3、复查一下览器A上的账号是否登录，发现并没有顶替下去，原因是两个浏览器指定的登录设备不同，允许同时在线 \n\t \t\t\t---- http://localhost:8081/mutex/isLogin\n\t \t\t\t\n\t \t4、在浏览器C上登录账号10001，设备为PC    ---- http://localhost:8081/mutex/login?userId=10001&device=PC\n\t \t\t检查是否登录成功，返回true：\t\t\t---- http://localhost:8081/mutex/isLogin\n\t \t\t\n\t \t5、复查一下浏览器A上的账号是否登录，发现账号已被顶替下线\n\t \t\t\t---- http://localhost:8081/mutex/isLogin\n\t \t\t\n\t \t6、再复查一下浏览器B上的账号是否登录，发现账号未被顶替下线，因为浏览器B上登录的设备是APP，而浏览器C顶替的设备是PC\n\t \t\t\t---- http://localhost:8081/mutex/isLogin\n\t \t\n\t \t7、此时再从浏览器D上登录账号10001，设备为APP    ---- http://localhost:8081/mutex/login?userId=10001&device=APP\n\t \t\t检查是否登录成功，返回true：\t\t\t---- http://localhost:8081/mutex/isLogin\n\t \t\t\n\t \t8、此时再复查一下浏览器B上的账号是否登录，发现账号已被顶替下线\n\t \t\t\t---- http://localhost:8081/mutex/isLogin\n\t */\n\n\t// 会话登录接口  ---- http://localhost:8081/mutex/doLogin?userId=10001&device=PC\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(long userId, String device) {\n\t\t/*\n\t\t * 参数1：要登录的账号\n\t\t * 参数2：此账号在什么设备上登录的 \n\t\t */\n\t\tStpUtil.login(userId, device);\n\t\treturn SaResult.ok(\"登录成功\");\n\t}\n\n\t// 查询当前登录状态  ---- http://localhost:8081/mutex/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\t// StpUtil.isLogin() 查询当前客户端是否登录，返回 true 或 false \n\t\tboolean isLogin = StpUtil.isLogin();\n\t\treturn SaResult.ok(\"当前客户端是否登录：\" + isLogin + \"，登录的设备是：\" + StpUtil.getLoginDeviceType());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/NotCookieController.java",
    "content": "package com.pj.cases.up;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 前后端分离模式示例 \n * \n * @author click33\n * @since 2022-10-17 \n */\n@RestController\n@RequestMapping(\"/NotCookie/\")\npublic class NotCookieController {\n\n\t// 前后端一体模式的登录样例    ---- http://localhost:8081/NotCookie/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t// 会话登录 \n\t\t\tStpUtil.login(10001);\n\t\t    return SaResult.ok();\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\t\n\t// 前后端分离模式的登录样例    ---- http://localhost:8081/NotCookie/doLogin2?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin2\")\n\tpublic SaResult doLogin2(String name, String pwd) {\n\t\t\n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\n\t\t\t// 会话登录 \n\t\t\tStpUtil.login(10001);\n\t\t\t\n\t\t\t// 与常规登录不同点之处：这里需要把 Token 信息从响应体中返回到前端 \n\t\t\tSaTokenInfo tokenInfo = StpUtil.getTokenInfo();\n\t\t    return SaResult.data(tokenInfo);\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\t\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/RememberMeController.java",
    "content": "package com.pj.cases.up;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 记住我模式登录 \n * \n * @author click33\n * @since 2022-10-17 \n */\n@RestController\n@RequestMapping(\"/RememberMe/\")\npublic class RememberMeController {\n\n\t// 记住我登录    ---- http://localhost:8081/RememberMe/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001, true);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\t\n\t// 不记住我登录    ---- http://localhost:8081/RememberMe/doLogin2?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin2\")\n\tpublic SaResult doLogin2(String name, String pwd) {\n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001, false);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 七天免登录    ---- http://localhost:8081/RememberMe/doLogin3?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin3\")\n\tpublic SaResult doLogin3(String name, String pwd) {\n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001, 60 * 60 * 24 * 7);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SafeAuthController.java",
    "content": "package com.pj.cases.up;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 二级认证示例 \n * \n * @author click33\n * @since 2022-10-16 \n */\n@RestController\n@RequestMapping(\"/safe/\")\npublic class SafeAuthController {\n\n\t/*\n\t * 前提：首先调用登录接口进行登录，代码在 com.pj.cases.use.LoginAuthController 中有详细解释，此处不再赘述 \n\t * \t\t---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t * \n\t * 测试步骤：\n\t\t1、前端调用 deleteProject 接口，尝试删除仓库。    ---- http://localhost:8081/safe/deleteProject\n\t\t2、后端校验会话尚未完成二级认证，返回： 仓库删除失败，请完成二级认证后再次访问接口。\n\t\t3、前端将信息提示给用户，用户输入密码，调用 openSafe 接口。    ---- http://localhost:8081/safe/openSafe?password=123456\n\t\t4、后端比对用户输入的密码，完成二级认证，有效期为：120秒。\n\t\t5、前端在 120 秒内再次调用 deleteProject 接口，尝试删除仓库。    ---- http://localhost:8081/safe/deleteProject\n\t\t6、后端校验会话已完成二级认证，返回：仓库删除成功。\n\t */\n\t\n\t// 删除仓库    ---- http://localhost:8081/safe/deleteProject\n\t@RequestMapping(\"deleteProject\")\n\tpublic SaResult deleteProject(String projectId) {\n\t    // 第1步，先检查当前会话是否已完成二级认证 \n\t\t// \t\t这个地方既可以通过 StpUtil.isSafe() 手动判断，\n\t\t// \t\t也可以通过 StpUtil.checkSafe() 或者 @SaCheckSafe 来校验（校验不通过时将抛出 NotSafeException 异常）\n\t    if(!StpUtil.isSafe()) {\n\t        return SaResult.error(\"仓库删除失败，请完成二级认证后再次访问接口\");\n\t    }\n\t\n\t    // 第2步，如果已完成二级认证，则开始执行业务逻辑\n\t    // ... \n\t\n\t    // 第3步，返回结果 \n\t    return SaResult.ok(\"仓库删除成功\"); \n\t}\n\t\n\t// 提供密码进行二级认证    ---- http://localhost:8081/safe/openSafe?password=123456\n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe(String password) {\n\t    // 比对密码（此处只是举例，真实项目时可拿其它参数进行校验）\n\t    if(\"123456\".equals(password)) {\n\t\n\t        // 比对成功，为当前会话打开二级认证，有效期为120秒，意为在120秒内再调用 deleteProject 接口都无需提供密码 \n\t        StpUtil.openSafe(120);\n\t        return SaResult.ok(\"二级认证成功\");\n\t    }\n\t\n\t    // 如果密码校验失败，则二级认证也会失败\n\t    return SaResult.error(\"二级认证失败\"); \n\t}\n\n\t// 手动关闭二级认证    ---- http://localhost:8081/safe/closeSafe\n\t@RequestMapping(\"closeSafe\")\n\tpublic SaResult closeSafe() {\n\t\tStpUtil.closeSafe();\n\t    return SaResult.ok();\n\t}\n\n\t\n\t// ------------------ 指定业务类型进行二级认证 \n\n\t// 获取应用秘钥    ---- http://localhost:8081/safe/getClientSecret\n\t@RequestMapping(\"getClientSecret\")\n\tpublic SaResult getClientSecret() {\n\t    // 第1步，先检查当前会话是否已完成 client业务 的二级认证 \n\t\tStpUtil.checkSafe(\"client\");\n\t\n\t    // 第2步，如果已完成二级认证，则返回数据 \n\t    return SaResult.data(\"aaaa-bbbb-cccc-dddd-eeee\");\n\t}\n\t\n\t// 提供手势密码进行二级认证    ---- http://localhost:8081/safe/openClientSafe?gesture=35789\n\t@RequestMapping(\"openClientSafe\")\n\tpublic SaResult openClientSafe(String gesture) {\n\t    // 比对手势密码（此处只是举例，真实项目时可拿其它参数进行校验）\n\t    if(\"35789\".equals(gesture)) {\n\t\n\t        // 比对成功，为当前会话打开二级认证：\n\t    \t// 业务类型为：client \n\t    \t// 有效期为600秒==10分钟，意为在10分钟内，调用 getClientSecret 时都无需再提供手势密码 \n\t        StpUtil.openSafe(\"client\", 600);\n\t        return SaResult.ok(\"二级认证成功\");\n\t    }\n\t\n\t    // 如果密码校验失败，则二级认证也会失败\n\t    return SaResult.error(\"二级认证失败\"); \n\t}\n\n\t// 查询当前会话是否已完成指定的二级认证    ---- http://localhost:8081/safe/isClientSafe\n\t@RequestMapping(\"isClientSafe\")\n\tpublic SaResult isClientSafe() {\n\t    return SaResult.ok(\"当前是否已完成 client 二级认证：\" + StpUtil.isSafe(\"client\")); \n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SearchSessionController.java",
    "content": "package com.pj.cases.up;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.model.SysUser;\n\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 会话查询示例 \n * \n * @author click33\n * @since 2022-10-17 \n */\n@RestController\n@RequestMapping(\"/search/\")\npublic class SearchSessionController {\n\n\t/*\n\t * 测试步骤：\n\t \t1、先登录5个账号\n\t \t\t ---- http://localhost:8081/search/login?userId=10001&张三&age=18\n\t \t\t ---- http://localhost:8081/search/login?userId=10002&李四&age=20\n\t \t\t ---- http://localhost:8081/search/login?userId=10003&王五&age=22\n\t \t\t ---- http://localhost:8081/search/login?userId=10004&赵六&age=24\n\t \t\t ---- http://localhost:8081/search/login?userId=10005&冯七&age=26\n\t \t\t \n\t \t2、根据分页参数获取会话列表\n\t \t\thttp://localhost:8081/search/getList?start=0&size=10\n\t */\n\n\t// 会话登录接口  ---- http://localhost:8081/search/login?userId=10001&张三&age=18\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(long userId, String name, int age) {\n\t\t// 先登录上\n\t\tStpUtil.login(userId);\n\t\t\n\t\t// 再把 User 对象存储在 SaSession 上\n\t\tSysUser user = new SysUser();\n\t\tuser.setId(userId);\n\t\tuser.setName(name);\n\t\tuser.setAge(age);\n\t\tStpUtil.getSession().set(\"user\", user);\n\t\t\n\t\t// 返回 \n\t\treturn SaResult.ok(\"账号登录成功\");\n\t}\n\n\t// 会话查询接口  ---- http://localhost:8081/search/getList?start=0&size=10\n\t@RequestMapping(\"getList\")\n\tpublic SaResult getList(int start, int size) {\n\t\t// 创建集合\n\t\tList<SaSession> sessionList = new ArrayList<>();\n\n\t\t// 分页查询数据 \n\t\tList<String> sessionIdList = StpUtil.searchSessionId(\"\", start, size, false);\n\t\tfor (String sessionId: sessionIdList) {\n\t\t\tSaSession session = StpUtil.getSessionBySessionId(sessionId);\n\t\t\tsessionList.add(session);\n\t\t}\n\n\t\t// 返回 \n\t\treturn SaResult.data(sessionList);\n\t}\n\n\t// 会话查询接口  ---- http://localhost:8081/disable/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok(\"账号退出成功\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java",
    "content": "package com.pj.cases.up;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.secure.SaBase64Util;\nimport cn.dev33.satoken.secure.SaSecureUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 密码加密示例\n * \n * @author click33\n * @since 2022-10-17\n */\n@RestController\n@RequestMapping(\"/secure/\")\npublic class SecureController {\n\n\t// 摘要加密   ---- http://localhost:8081/secure/digest\n\t@RequestMapping(\"digest\")\n\tpublic SaResult digest() {\n\t\t// md5加密 \n\t\tSystem.out.println(SaSecureUtil.md5(\"123456\"));\n\n\t\t// sha1加密 \n\t\tSystem.out.println(SaSecureUtil.sha1(\"123456\"));\n\n\t\t// sha256加密 \n\t\tSystem.out.println(SaSecureUtil.sha256(\"123456\"));\n\n\t\treturn SaResult.ok();\n\t}\n\n\t// AES加密   ---- http://localhost:8081/secure/aes\n\t@RequestMapping(\"aes\")\n\tpublic SaResult aes() {\n\t\t// 定义秘钥和明文\n\t\tString key = \"123456\";\n\t\tString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n\t\t// 加密 \n\t\tString ciphertext = SaSecureUtil.aesEncrypt(key, text);\n\t\tSystem.out.println(\"AES加密后：\" + ciphertext);\n\n\t\t// 解密 \n\t\tString text2 = SaSecureUtil.aesDecrypt(key, ciphertext);\n\t\tSystem.out.println(\"AES解密后：\" + text2);\n\n\t\treturn SaResult.ok();\n\t}\n\n\t// RSA加密   ---- http://localhost:8081/secure/rsa\n//\t@RequestMapping(\"rsa\")\n//\tpublic SaResult rsa() {\n//\t\t// 定义私钥和公钥\n//\t\tString 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==\";\n//\t\tString publicKey = \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB\";\n//\n//\t\t// 文本\n//\t\tString text = \"Sa-Token 一个轻量级java权限认证框架\";\n//\n//\t\t// 使用公钥加密\n//\t\tString ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);\n//\t\tSystem.out.println(\"公钥加密后：\" + ciphertext);\n//\n//\t\t// 使用私钥解密\n//\t\tString text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);\n//\t\tSystem.out.println(\"私钥解密后：\" + text2);\n//\n//\t\treturn SaResult.ok();\n//\t}\n\n\t// Base64 编码   ---- http://localhost:8081/secure/base64\n\t@RequestMapping(\"base64\")\n\tpublic SaResult base64() {\n\t\t// 文本\n\t\tString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n\t\t// 使用Base64编码\n\t\tString base64Text = SaBase64Util.encode(text);\n\t\tSystem.out.println(\"Base64编码后：\" + base64Text);\n\n\t\t// 使用Base64解码\n\t\tString text2 = SaBase64Util.decode(base64Text);\n\t\tSystem.out.println(\"Base64解码后：\" + text2); \n\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SwitchToController.java",
    "content": "package com.pj.cases.up;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 身份切换 \n * \n * @author click33\n * @since 2022-10-17 \n */\n@RestController\n@RequestMapping(\"/SwitchTo/\")\npublic class SwitchToController {\n\n\t/*\n\t * 前提：首先调用登录接口进行登录，代码在 com.pj.cases.use.LoginAuthController 中有详细解释，此处不再赘述 \n\t * \t\t---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t */\n\t\n\t// 身份切换    ---- http://localhost:8081/SwitchTo/switchTo?userId=10044\n\t@RequestMapping(\"switchTo\")\n\tpublic SaResult switchTo(long userId) {\n\t\t// 将当前会话 [身份临时切换] 为其它账号\n\t\t// \t\t--- 切换的有效期为本次请求 \n\t\tStpUtil.switchTo(userId);\n\n\t\t// 此时再调用此方法会返回 10044 (我们临时切换到的账号id)\n\t\tSystem.out.println(StpUtil.getLoginId());;\n\n\t\t// 结束 [身份临时切换]\n\t\tStpUtil.endSwitch();\n\t\t\n\t\t// 此时再打印账号，就又回到了原来的值：10001 \n\t\tSystem.out.println(StpUtil.getLoginId());\n\t\t\n\t\treturn SaResult.ok();\n\t}\n\n\t// 以 lambda 表达式的方式身份切换    ---- http://localhost:8081/SwitchTo/switchTo2?userId=10044\n\t@RequestMapping(\"switchTo2\")\n\tpublic SaResult switchTo2(long userId) {\n\t\t\n\t\t// 输出 10001\n\t\tSystem.out.println(\"------- [身份临时切换] 调用前，当前登录账号id是：\" + StpUtil.getLoginId());\n\t\t\n\t\t// 以 lambda 表达式的方式身份切换，作用范围只在这个 lambda 表达式内有效 \n\t\tStpUtil.switchTo(userId, () -> {\n\t\t    System.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch());  // 输出 true\n\t\t    System.out.println(\"获取当前登录账号id: \" + StpUtil.getLoginId());   // 输出 10044\n\t\t});\n\t\t\n\t\t// 结束后，再次获取当前登录账号，输出10001\n\t\tSystem.out.println(\"------- [身份临时切换] 调用结束，当前登录账号id是：\" + StpUtil.getLoginId()); \n\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/AtCheckController.java",
    "content": "package com.pj.cases.use;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaIgnore;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 注解鉴权示例 \n * \n * @author click33\n * @since 2022-10-13\n */\n@RestController\n@RequestMapping(\"/at-check/\")\npublic class AtCheckController {\n\n\t/*\n\t * 前提1：首先调用登录接口进行登录，代码在 com.pj.cases.use.LoginAuthController 中有详细解释，此处不再赘述 \n\t * \t\t---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t * \n\t * 前提2：项目在配置类中注册拦截器 SaInterceptor ，代码在  com.pj.satoken.SaTokenConfigure\n\t * \t\t此拦截器将打开注解鉴权功能 \n\t * \n\t * 然后我们就可以使用以下示例中的代码进行注解鉴权了 \n\t */\n\t\n\t// 登录鉴权   ---- http://localhost:8081/at-check/checkLogin\n\t// \t\t登录认证后才可以进入方法 \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\t// 通过注解鉴权后才能进入方法 ... \n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限校验   ---- http://localhost:8081/at-check/checkPermission\n\t//\t\t只有具有 user.add 权限的账号才可以进入方法 \n\t@SaCheckPermission(\"user.add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\t// ... \n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限校验2   ---- http://localhost:8081/at-check/checkPermission2\n\t//\t\t一次性校验多个权限，必须全部拥有，才可以进入方法 \n\t@SaCheckPermission(value = {\"user.add\", \"user.delete\", \"user.update\"}, mode = SaMode.AND)\n\t@RequestMapping(\"checkPermission2\")\n\tpublic SaResult checkPermission2() {\n\t\t// ... \n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限校验3   ---- http://localhost:8081/at-check/checkPermission3\n\t//\t\t一次性校验多个权限，只要拥有其中一个，就可以进入方法 \n\t@SaCheckPermission(value = {\"user.add\", \"user.delete\", \"user.update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermission3\")\n\tpublic SaResult checkPermission3() {\n\t\t// ... \n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色校验   ---- http://localhost:8081/at-check/checkRole\n\t//\t\t只有具有 super-admin 角色的账号才可以进入方法 \n\t@SaCheckRole(\"super-admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\t// ... \n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 角色权限双重 “or校验”   ---- http://localhost:8081/at-check/userAdd\n\t//\t\t具备 \"user.add\"权限 或者 \"admin\"角色 即可通过校验\n\t@RequestMapping(\"userAdd\")\n\t@SaCheckPermission(value = \"user.add\", orRole = \"admin\")        \n\tpublic SaResult userAdd() {\n\t    return SaResult.data(\"用户信息\");\n\t}\n\n\t// 忽略校验 ---- http://localhost:8081/at-check/ignore\n\t//\t\t使用 @SaIgnore 修饰的方法，无需任何校验即可进入，具体使用示例可参照在线文档 \n\t@SaIgnore\n\t@SaCheckLogin   \n\t@RequestMapping(\"ignore\")\n\tpublic SaResult ignore() {\n\t    return SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/JurAuthController.java",
    "content": "package com.pj.cases.use;\n\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 权限认证示例 \n * \n * @author click33\n * @since 2022-10-13\n */\n@RestController\n@RequestMapping(\"/jur/\")\npublic class JurAuthController {\n\n\t/*\n\t * 前提1：首先调用登录接口进行登录，代码在 com.pj.cases.use.LoginAuthController 中有详细解释，此处不再赘述 \n\t * \t\t---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t * \n\t * 前提2：项目实现 StpInterface 接口，代码在  com.pj.satoken.StpInterfaceImpl\n\t * \t\tSa-Token 将从此实现类获取 每个账号拥有哪些权限。\n\t * \n\t * 然后我们就可以使用以下示例中的代码进行鉴权了 \n\t */\n\t\n\t// 查询权限   ---- http://localhost:8081/jur/getPermission\n\t@RequestMapping(\"getPermission\")\n\tpublic SaResult getPermission() {\n\t\t// 查询权限信息 ，如果当前会话未登录，会返回一个空集合 \n\t\tList<String> permissionList = StpUtil.getPermissionList();\n\t\tSystem.out.println(\"当前登录账号拥有的所有权限：\" + permissionList);\n\t\t\n\t\t// 查询角色信息 ，如果当前会话未登录，会返回一个空集合 \n\t\tList<String> roleList = StpUtil.getRoleList();\n\t\tSystem.out.println(\"当前登录账号拥有的所有角色：\" + roleList);\n\t\t\n\t\t// 返回给前端 \n\t\treturn SaResult.ok()\n\t\t\t\t.set(\"roleList\", roleList)\n\t\t\t\t.set(\"permissionList\", permissionList);\n\t}\n\t\n\t// 权限校验  ---- http://localhost:8081/jur/checkPermission\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\t\n\t\t// 判断：当前账号是否拥有一个权限，返回 true 或 false\n\t\t// \t\t如果当前账号未登录，则永远返回 false \n\t\tStpUtil.hasPermission(\"user.add\");\n\t\tStpUtil.hasPermissionAnd(\"user.add\", \"user.delete\", \"user.get\");  // 指定多个，必须全部拥有才会返回 true \n\t\tStpUtil.hasPermissionOr(\"user.add\", \"user.delete\", \"user.get\");\t // 指定多个，只要拥有一个就会返回 true \n\t\t\n\t\t// 校验：当前账号是否拥有一个权限，校验不通过时会抛出 `NotPermissionException` 异常 \n\t\t// \t\t如果当前账号未登录，则永远校验失败 \n\t\tStpUtil.checkPermission(\"user.add\");\n\t\tStpUtil.checkPermissionAnd(\"user.add\", \"user.delete\", \"user.get\");  // 指定多个，必须全部拥有才会校验通过 \n\t\tStpUtil.checkPermissionOr(\"user.add\", \"user.delete\", \"user.get\");  // 指定多个，只要拥有一个就会校验通过 \n\t\t\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色校验  ---- http://localhost:8081/jur/checkRole\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\t\n\t\t// 判断：当前账号是否拥有一个角色，返回 true 或 false\n\t\t// \t\t如果当前账号未登录，则永远返回 false \n\t\tStpUtil.hasRole(\"admin\");\n\t\tStpUtil.hasRoleAnd(\"admin\", \"ceo\", \"cfo\");  // 指定多个，必须全部拥有才会返回 true \n\t\tStpUtil.hasRoleOr(\"admin\", \"ceo\", \"cfo\");\t  // 指定多个，只要拥有一个就会返回 true \n\t\t\n\t\t// 校验：当前账号是否拥有一个角色，校验不通过时会抛出 `NotRoleException` 异常 \n\t\t// \t\t如果当前账号未登录，则永远校验失败 \n\t\tStpUtil.checkRole(\"admin\");\n\t\tStpUtil.checkRoleAnd(\"admin\", \"ceo\", \"cfo\");  // 指定多个，必须全部拥有才会校验通过 \n\t\tStpUtil.checkRoleOr(\"admin\", \"ceo\", \"cfo\");  // 指定多个，只要拥有一个就会校验通过 \n\t\t\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限通配符  ---- http://localhost:8081/jur/wildcardPermission\n\t@RequestMapping(\"wildcardPermission\")\n\tpublic SaResult wildcardPermission() {\n\t\t\n\t\t// 前提条件：在 StpInterface 实现类中，为账号返回了 \"art.*\" 泛权限\n\t\tStpUtil.hasPermission(\"art.add\");  // 返回 true \n\t\tStpUtil.hasPermission(\"art.delete\");  // 返回 true \n\t\tStpUtil.hasPermission(\"goods.add\");  // 返回 false，因为前缀不符合  \n\t\t\n\t\t// * 符合可以出现在任意位置，比如权限码的开头，当账号拥有 \"*.delete\" 时  \n\t\tStpUtil.hasPermission(\"goods.add\");        // false\n\t\tStpUtil.hasPermission(\"goods.delete\");     // true\n\t\tStpUtil.hasPermission(\"art.delete\");      // true\n\t\t\n\t\t// 也可以出现在权限码的中间，比如当账号拥有 \"shop.*.user\" 时  \n\t\tStpUtil.hasPermission(\"shop.add.user\");  // true\n\t\tStpUtil.hasPermission(\"shop.delete.user\");  // true\n\t\tStpUtil.hasPermission(\"shop.delete.goods\");  // false，因为后缀不符合 \n\n\t\t// 注意点：\n\t\t// 1、上帝权限：当一个账号拥有 \"*\" 权限时，他可以验证通过任何权限码\n\t\t// 2、角色校验也可以加 * ，指定泛角色，例如： \"*.admin\"，暂不赘述 \n\t\t\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/KickoutController.java",
    "content": "package com.pj.cases.use;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 权限认证示例 \n * \n * @author click33\n * @since 2022-10-13\n */\n@RestController\n@RequestMapping(\"/kickout/\")\npublic class KickoutController {\n\n\t/*\n\t * 前提：首先调用登录接口进行登录，代码在 com.pj.cases.use.LoginAuthController 中有详细解释，此处不再赘述 \n\t * \t\t---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t */\n\t\n\t// 将指定账号强制注销   ---- http://localhost:8081/kickout/logout?userId=10001\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout(long userId) {\n\t\t\n\t\t// 强制注销等价于对方主动调用了注销方法，再次访问会提示：Token无效。\n\t\tStpUtil.logout(userId);\n\t\t\n\t\t// 返回\n\t\treturn SaResult.ok();\n\t}\n\n\t// 将指定账号踢下线   ---- http://localhost:8081/kickout/kickout?userId=10001\n\t@RequestMapping(\"kickout\")\n\tpublic SaResult kickout(long userId) {\n\t\t\n\t\t// 踢人下线不会清除Token信息，而是将其打上特定标记，再次访问会提示：Token已被踢下线。\n\t\tStpUtil.kickout(userId);\n\t\t\n\t\t// 返回\n\t\treturn SaResult.ok();\n\t}\n\t\n\t/* \n\t * 你可以分别在强制注销和踢人下线后，再次访问一下登录校验接口，对比一下两者返回的提示信息有何不同 \n\t * \t\t---- http://localhost:8081/acc/checkLogin\n\t */\n\t\n\t// 根据 Token 值踢人    ---- http://localhost:8081/kickout/kickoutByTokenValue?tokenValue=xxxx-xxxx-xxxx-xxxx已登录账号的token值\n\t@RequestMapping(\"kickoutByTokenValue\")\n\tpublic SaResult kickoutByTokenValue(String tokenValue) {\n\t\t\n\t\tStpUtil.kickoutByTokenValue(tokenValue);\n\t\t\n\t\t// 返回\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/LoginAuthController.java",
    "content": "package com.pj.cases.use;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token 登录认证示例 \n * \n * @author click33\n * @since 2022-10-13\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginAuthController {\n\n\t// 会话登录接口  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t\n\t\t// 第一步：比对前端提交的 账号名称 & 密码 是否正确，比对成功后开始登录 \n\t\t// \t\t此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\n\t\t\t// 第二步：根据账号id，进行登录 \n\t\t\t// \t\t此处填入的参数应该保持用户表唯一，比如用户id，不可以直接填入整个 User 对象 \n\t\t\tStpUtil.login(10001);\n\t\t\t\n\t\t\t// SaResult 是 Sa-Token 中对返回结果的简单封装，下面的示例将不再赘述 \n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\t\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询当前登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\t// StpUtil.isLogin() 查询当前客户端是否登录，返回 true 或 false \n\t\tboolean isLogin = StpUtil.isLogin();\n\t\treturn SaResult.ok(\"当前客户端是否登录：\" + isLogin);\n\t}\n\n\t// 校验当前登录状态  ---- http://localhost:8081/acc/checkLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\t// 检验当前会话是否已经登录, 如果未登录，则抛出异常：`NotLoginException`\n\t\tStpUtil.checkLogin();\n\n\t\t// 抛出异常后，代码将走入全局异常处理（GlobalException.java），如果没有抛出异常，则代表通过了登录校验，返回下面信息 \n\t\treturn SaResult.ok(\"校验登录成功，这行字符串是只有登录后才会返回的信息\");\n\t}\n\n\t// 获取当前登录的账号是谁  ---- http://localhost:8081/acc/getLoginId\n\t@RequestMapping(\"getLoginId\")\n\tpublic SaResult getLoginId() {\n\t\t// 需要注意的是，StpUtil.getLoginId() 自带登录校验效果\n\t\t// 也就是说如果在未登录的情况下调用这句代码，框架就会抛出 `NotLoginException` 异常，效果和 StpUtil.checkLogin() 是一样的 \n\t\tObject userId = StpUtil.getLoginId();\n\t\tSystem.out.println(\"当前登录的账号id是：\" + userId);\n\t\t\n\t\t// 如果不希望 StpUtil.getLoginId() 触发登录校验效果，可以填入一个默认值\n\t\t// 如果会话未登录，则返回这个默认值，如果会话已登录，将正常返回登录的账号id \n\t\tObject userId2 = StpUtil.getLoginId(0);\n\t\tSystem.out.println(\"当前登录的账号id是：\" + userId2);\n\t\t\n\t\t// 或者使其在未登录的时候返回 null \n\t\tObject userId3 = StpUtil.getLoginIdDefaultNull();\n\t\tSystem.out.println(\"当前登录的账号id是：\" + userId3);\n\t\t\n\t\t// 类型转换：\n\t\t// StpUtil.getLoginId() 返回的是 Object 类型，你可以使用以下方法指定其返回的类型 \n\t\tint userId4 = StpUtil.getLoginIdAsInt();  // 将返回值转换为 int 类型 \n\t\tlong userId5 = StpUtil.getLoginIdAsLong();  // 将返回值转换为 long 类型 \n\t\tString userId6 = StpUtil.getLoginIdAsString();  // 将返回值转换为 String 类型 \n\t\t\n\t\t// 疑问：数据基本类型不是有八个吗，为什么只封装以上三种类型的转换？\n\t\t// 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的，实在没见过哪个项目用 double、float、boolean 之类来声明 UserId \n\t\tSystem.out.println(\"当前登录的账号id是：\" + userId4 + \" --- \" + userId5 + \" --- \" + userId6);\n\t\t\n\t\t// 返回给前端 \n\t\treturn SaResult.ok(\"当前客户端登录的账号id是：\" + userId);\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\t// TokenName 是 Token 名称的意思，此值也决定了前端提交 Token 时应该使用的参数名称 \n\t\tString tokenName = StpUtil.getTokenName();\n\t\tSystem.out.println(\"前端提交 Token 时应该使用的参数名称：\" + tokenName);\n\t\t\n\t\t// 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值 \n\t\t// 框架默认前端可以从以下三个途径中提交 Token：\n\t\t// \t\tCookie \t\t（浏览器自动提交）\n\t\t// \t\tHeader头\t（代码手动提交）\n\t\t// \t\tQuery 参数\t（代码手动提交） 例如： /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx \n\t\t// 读取顺序为： Query 参数 --> Header头 -- > Cookie \n\t\t// 以上三个地方都读取不到 Token 信息的话，则视为前端没有提交 Token \n\t\tString tokenValue = StpUtil.getTokenValue();\n\t\tSystem.out.println(\"前端提交的Token值为：\" + tokenValue);\n\t\t\n\t\t// TokenInfo 包含了此 Token 的大多数信息 \n\t\tSaTokenInfo info = StpUtil.getTokenInfo();\n\t\tSystem.out.println(\"Token 名称：\" + info.getTokenName());\n\t\tSystem.out.println(\"Token 值：\" + info.getTokenValue());\n\t\tSystem.out.println(\"当前是否登录：\" + info.getIsLogin());\n\t\tSystem.out.println(\"当前登录的账号id：\" + info.getLoginId());\n\t\tSystem.out.println(\"当前登录账号的类型：\" + info.getLoginType());\n\t\tSystem.out.println(\"当前登录客户端的设备类型：\" + info.getLoginDeviceType());\n\t\tSystem.out.println(\"当前 Token 的剩余有效期：\" + info.getTokenTimeout()); // 单位：秒，-1代表永久有效，-2代表值不存在\n\t\tSystem.out.println(\"当前 Token 距离被冻结还剩：\" + info.getTokenActiveTimeout()); // 单位：秒，-1代表永久有效，-2代表值不存在\n\t\tSystem.out.println(\"当前 Account-Session 的剩余有效期\" + info.getSessionTimeout()); // 单位：秒，-1代表永久有效，-2代表值不存在\n\t\tSystem.out.println(\"当前 Token-Session 的剩余有效期\" + info.getTokenSessionTimeout()); // 单位：秒，-1代表永久有效，-2代表值不存在\n\t\t\n\t\t// 返回给前端 \n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 会话注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\t// 退出登录会清除三个地方的数据：\n\t\t// \t\t1、Redis中保存的 Token 信息\n\t\t// \t\t2、当前请求上下文中保存的 Token 信息 \n\t\t// \t\t3、Cookie 中保存的 Token 信息（如果未使用Cookie模式则不会清除）\n\t\tStpUtil.logout();\n\t\t\n\t\t// StpUtil.logout() 在未登录时也是可以调用成功的，\n\t\t// 也就是说，无论客户端有没有登录，执行完 StpUtil.logout() 后，都会处于未登录状态 \n\t\tSystem.out.println(\"当前是否处于登录状态：\" + StpUtil.isLogin());\n\t\t\n\t\t// 返回给前端 \n\t\treturn SaResult.ok(\"退出登录成功\");\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/RouterCheckController.java",
    "content": "package com.pj.cases.use;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 为路由拦截鉴权准备的路示例 \n * \n * @author click33\n * @since 2022-10-15\n */\n@RestController\npublic class RouterCheckController {\n\n\t// 路由拦截鉴权测试   ---- http://localhost:8081/xxx\n\t@RequestMapping({\n\t\t\"/user/doLogin\", \"/user/doLogin2\",\n\t\t\"/user/info\", \"/admin/info\", \"/goods/info\", \"/orders/info\", \"/notice/info\", \"/comment/info\",\n\t\t\"/router/print\", \"/router/print2\"\n\t})\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/SaSessionController.java",
    "content": "package com.pj.cases.use;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.model.SysUser;\n\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaSessionCustomUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * Sa-Token Session会话示例 \n * \n * @author click33\n * @since 2022-10-15\n */\n@RestController\n@RequestMapping(\"/session/\")\npublic class SaSessionController {\n\n\t/*\n\t * 前提：首先调用登录接口进行登录，代码在 com.pj.cases.use.LoginAuthController 中有详细解释，此处不再赘述 \n\t * \t\t---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t */\n\t\n\t// 简单存取值   ---- http://localhost:8081/session/getValue\n\t@RequestMapping(\"getValue\")\n\tpublic SaResult getValue() {\n\t\t// 获取当前登录账号的专属 SaSession 对象 \n\t\t// \t\t注意点1：只有登录后才可以调用这个方法 \n\t\t//\t\t注意点2：每个账号获取到的都是不同的 SaSession 对象，存取值时不会互相影响 \n\t\t//\t\t注意点3：SaSession 和 HttpSession 是两个完全不同的对象，不可混淆使用 \n\t\tSaSession session = StpUtil.getSession();\n\t\t\n\t\t// 存值\n\t\tsession.set(\"name\", \"zhangsan\");\n\t\tsession.set(\"age\", 18);\n\t\t\n\t\t// 取值 \n\t\tObject name = session.get(\"name\"); \n\t\tString name2 = session.getString(\"name\");   // 取值，并转化为 String 数据类型  \n\t\tint age = session.getInt(\"age\");\t// 转 int 类型\n\t\tlong age2 = session.getLong(\"age\");\t// 转 long 类型\n\t\tfloat age3 = session.getFloat(\"age\");\t// 转 float 类型\n\t\tdouble age4 = session.getDouble(\"age\");\t// 转 double 类型 \n\t\tint age5 = session.get(\"age5\", 22);  // 取不到时就返回默认值 \n\t\tint age6 = session.get(\"age5\", () -> {  // 取不到时就执行 lambda 获取值 \n\t\t\treturn 26;\n\t\t});\n\t\t\n\t\t/*\n\t\t * 存取值范围是一次会话有效的，也就是说，在一次登录有效期内，你可以在一个请求里存值，然后在另一个请求里取值 \n\t\t */\n\t\t\n\t\tList<Object> list = Arrays.asList(name, name2, age, age2, age3, age4, age5, age6);\n\t\tSystem.out.println(list);\n\t\t\n\t\treturn SaResult.data(list);\n\t}\n\n\t// 复杂存取值   ---- http://localhost:8081/session/getModel\n\t@RequestMapping(\"getModel\")\n\tpublic SaResult getModel() {\n\t\t// 实例化  \n\t\tSysUser user = new SysUser();\n\t\tuser.setId(10001);\n\t\tuser.setName(\"张三\");\n\t\tuser.setAge(19);\n\t\t\n\t\t// 写入这个对象到 SaSession 中 \n\t\tStpUtil.getSession().set(\"user\", user);\n\t\t\n\t\t// 然后我们就可以在任意代码处获取这个 user 了\n\t\tSysUser user2 = StpUtil.getSession().getModel(\"user\", SysUser.class);\n\t\t\n\t\t// 返回\n\t\treturn SaResult.data(user2);\n\t}\n\t\n\t// 自定义Session   ---- http://localhost:8081/session/customSession\n\t@RequestMapping(\"customSession\")\n\tpublic SaResult customSession() {\n\t\t\n\t\t// 自定义 Session 就是指使用一个特定的 key，来获取 Session 对象 \n\t\tSaSession roleSession = SaSessionCustomUtil.getSessionById(\"role-1001\");\n\t\t\n\t\t// 一样可以自由的存值写值 \n\t\troleSession.set(\"nnn\", \"lalala\");\n\t\tSystem.out.println(roleSession.get(\"nnn\"));\n\t\t\n\t\t// 返回\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotHttpBasicAuthException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\nimport cn.dev33.satoken.exception.NotSafeException;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 拦截：未登录异常\n\t@ExceptionHandler(NotLoginException.class)\n\tpublic SaResult handlerException(NotLoginException e) {\n\n\t\t// 打印堆栈，以供调试\n\t\te.printStackTrace(); \n\n\t\t// 返回给前端\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\n\t// 拦截：缺少权限异常\n\t@ExceptionHandler(NotPermissionException.class)\n\tpublic SaResult handlerException(NotPermissionException e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(\"缺少权限：\" + e.getPermission());\n\t}\n\n\t// 拦截：缺少角色异常\n\t@ExceptionHandler(NotRoleException.class)\n\tpublic SaResult handlerException(NotRoleException e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(\"缺少角色：\" + e.getRole());\n\t}\n\n\t// 拦截：二级认证校验失败异常\n\t@ExceptionHandler(NotSafeException.class)\n\tpublic SaResult handlerException(NotSafeException e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(\"二级认证校验失败：\" + e.getService());\n\t}\n\n\t// 拦截：服务封禁异常 \n\t@ExceptionHandler(DisableServiceException.class)\n\tpublic SaResult handlerException(DisableServiceException e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(\"当前账号 \" + e.getService() + \" 服务已被封禁 (level=\" + e.getLevel() + \")：\" + e.getDisableTime() + \"秒后解封\");\n\t}\n\n\t// 拦截：Http Basic 校验失败异常 \n\t@ExceptionHandler(NotHttpBasicAuthException.class)\n\tpublic SaResult handlerException(NotHttpBasicAuthException e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\n\t// 拦截：其它所有异常\n\t@ExceptionHandler(Exception.class)\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/model/SysUser.java",
    "content": "package com.pj.model;\n\nimport java.io.Serializable;\n\n/**\n * User 实体类 \n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysUser implements Serializable {\n\n\t/**\n\t * \n\t */\n\tprivate static final long serialVersionUID = -2853125262828437774L;\n\n\tpublic SysUser() {\n\t}\n\t\n\tpublic SysUser(long id, String name, int age) {\n\t\tsuper();\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.age = age;\n\t}\n\t\n\n\t/**\n\t * 用户id\n\t */\n\tprivate long id;\n\t\n\t/**\n\t * 用户名称\n\t */\n\tprivate String name;\n\t\n\t/**\n\t * 用户年龄\n\t */\n\tprivate int age;\n\n\t/**\n\t * @return id\n\t */\n\tpublic long getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id 要设置的 id\n\t */\n\tpublic void setId(long id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return name\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 要设置的 name\n\t */\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return age\n\t */\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\t/**\n\t * @param age 要设置的 age\n\t */\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SysUser [id=\" + id + \", name=\" + name + \", age=\" + age + \"]\";\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTempTemplate.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.temp.SaTempTemplate;\n\n/**\n * 自定义临时 token 认证组件子类\n *\n * @author click33\n * @since 2025/4/9\n */\n//@Component\npublic class MySaTempTemplate extends SaTempTemplate {\n\n    @Override\n    public String createToken(Object value, long timeout, boolean isRecordIndex) {\n        System.out.println(\"------- 自定义一些逻辑 createToken \");\n        return super.createToken(value, timeout, isRecordIndex);\n    }\n\n    @Override\n    public Object parseToken(String token) {\n        System.out.println(\"------- 自定义一些逻辑 parseToken \");\n        return super.parseToken(token);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTokenListener.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.listener.SaTokenListener;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\n\n/**\n * Sa-Token 自定义侦听器的实现 \n * \n * @author click33\n * @since 2022-10-17\n */\n//@Component\t// 打开此注解，让 SpringBoot 扫描到组件，即可完成自定义侦听器的注入 \npublic class MySaTokenListener implements SaTokenListener {\n\n    /** 每次登录时触发 */\n    @Override\n    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n        System.out.println(\"---------- 自定义侦听器实现 doLogin\");\n    }\n\n    /** 每次注销时触发 */\n    @Override\n    public void doLogout(String loginType, Object loginId, String tokenValue) {\n        System.out.println(\"---------- 自定义侦听器实现 doLogout\");\n    }\n\n    /** 每次被踢下线时触发 */\n    @Override\n    public void doKickout(String loginType, Object loginId, String tokenValue) {\n        System.out.println(\"---------- 自定义侦听器实现 doKickout\");\n    }\n\n    /** 每次被顶下线时触发 */\n    @Override\n    public void doReplaced(String loginType, Object loginId, String tokenValue) {\n        System.out.println(\"---------- 自定义侦听器实现 doReplaced\");\n    }\n\n    /** 每次被封禁时触发 */\n    @Override\n    public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {\n        System.out.println(\"---------- 自定义侦听器实现 doDisable\");\n    }\n\n    /** 每次被解封时触发 */\n    @Override\n    public void doUntieDisable(String loginType, Object loginId, String service) {\n        System.out.println(\"---------- 自定义侦听器实现 doUntieDisable\");\n    }\n\t\n    /** 每次打开二级认证时触发 */\n    @Override\n\tpublic void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {\n    \t System.out.println(\"---------- 自定义侦听器实现 doOpenSafe\");\n\t}\n\n    /** 每次关闭二级认证时触发 */\n\t@Override\n\tpublic void doCloseSafe(String loginType, String tokenValue, String service) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doCloseSafe\");\n\t}\n    \n    /** 每次创建Session时触发 */\n    @Override\n    public void doCreateSession(String id) {\n        System.out.println(\"---------- 自定义侦听器实现 doCreateSession\");\n    }\n\n    /** 每次注销Session时触发 */\n    @Override\n    public void doLogoutSession(String id) {\n        System.out.println(\"---------- 自定义侦听器实现 doLogoutSession\");\n    }\n\n    /** 每次Token续期时触发 */\n    @Override\n    public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {\n        System.out.println(\"---------- 自定义侦听器实现 doRenewTimeout\");\n    }\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaLogForSlf4j.java",
    "content": "package com.pj.satoken;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport cn.dev33.satoken.log.SaLog;\n\n/**\n * 将 Sa-Token log 信息转接到 Slf4j \n * \n * @author click33\n * @since 2022-11-2\n */\n//@Component\npublic class SaLogForSlf4j implements SaLog {\n\n\tLogger log = LoggerFactory.getLogger(SaLogForSlf4j.class);\n\t\n\t@Override\n\tpublic void trace(String str, Object... args) {\n\t\tlog.trace(str, args);\n\t}\n\n\t@Override\n\tpublic void debug(String str, Object... args) {\n\t\tlog.debug(str, args);\n\t}\n\n\t@Override\n\tpublic void info(String str, Object... args) {\n\t\tlog.info(str, args);\n\t}\n\n\t@Override\n\tpublic void warn(String str, Object... args) {\n\t\tlog.trace(str, args);\n\t}\n\n\t@Override\n\tpublic void error(String str, Object... args) {\n\t\tlog.error(str, args);\n\t}\n\n\t@Override\n\tpublic void fatal(String str, Object... args) {\n\t\tlog.error(str, args);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.annotation.AnnotatedElementUtils;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\nimport javax.annotation.PostConstruct;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n\n\t\t\t// 指定一条 match 规则\n            SaRouter\n                .match(\"/user/**\")    // 拦截的 path 列表，可以写多个\n                .notMatch(\"/user/doLogin\", \"/user/doLogin2\")     // 排除掉的 path 列表，可以写多个\n                .check(r -> StpUtil.checkLogin());        // 要执行的校验动作，可以写完整的 lambda 表达式\n\n            // 权限校验 -- 不同模块认证不同权限\n            SaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n            SaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n            SaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n            SaRouter.match(\"/notice/**\", r -> StpUtil.checkPermission(\"notice\"));\n            SaRouter.match(\"/comment/**\", r -> StpUtil.checkPermission(\"comment\"));\n\n\t\t\t// 甚至你可以随意的写一个打印语句\n\t\t\tSaRouter.match(\"/router/print\", r -> System.out.println(\"----啦啦啦----\"));\n\n\t\t\t// 写一个完整的 lambda\n\t\t\tSaRouter.match(\"/router/print2\", r -> {\n\t\t\t\tSystem.out.println(\"----啦啦啦2----\");\n\t\t\t\t// ... 其它代码\n\t\t\t});\n\n\t\t\t/*\n\t\t\t * 相关路由都定义在 com.pj.cases.use.RouterCheckController 中\n\t\t\t */\n\n\t\t})).addPathPatterns(\"/**\");\n\t\t\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// System.out.println(\"---------- sa全局认证 \" + SaHolder.getRequest().getRequestPath()); \n        \t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n\n                    // 权限校验 -- 不同模块认证不同权限 \n        \t\t\t//\t\t这里你可以写和拦截器鉴权同样的代码，不同点在于：\n        \t\t\t// \t\t校验失败后不会进入全局异常组件，而是进入下面的 .setError 函数 \n                    SaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n                    SaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n                    SaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n                    SaRouter.match(\"/notice/**\", r -> StpUtil.checkPermission(\"notice\"));\n                    SaRouter.match(\"/comment/**\", r -> StpUtil.checkPermission(\"comment\"));\n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n    }\n\n\t/**\n\t * CORS 跨域处理\n\t */\n\t@Bean\n\tpublic SaCorsHandleFunction corsHandle() {\n\t\treturn (req, res, sto) -> {\n\t\t\tres.\n\t\t\t\t\t// 允许指定域访问跨域资源\n\t\t\t\t\t\t\tsetHeader(\"Access-Control-Allow-Origin\", \"*\")\n\t\t\t\t\t// 允许所有请求方式\n\t\t\t\t\t.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n\t\t\t\t\t// 有效时间\n\t\t\t\t\t.setHeader(\"Access-Control-Max-Age\", \"3600\")\n\t\t\t\t\t// 允许的header参数\n\t\t\t\t\t.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n\t\t\t// 如果是预检请求，则立即返回到前端\n\t\t\tSaRouter.match(SaHttpMethod.OPTIONS)\n\t\t\t\t\t.free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n\t\t\t\t\t.back();\n\t\t};\n\t}\n\n\t/**\n     * 重写 Sa-Token 框架内部算法策略 \n     */\n    @PostConstruct\n    public void rewriteSaStrategy() {\n    \t// 重写Sa-Token的注解处理器，增加注解合并功能 \n    \tSaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {\n    \t\treturn AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);\n    \t};\n\n\t\t// 重写 SaCheckELRootMap 扩展函数，增加注解鉴权 EL 表达式可使用的根对象\n\t\tSaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> {\n\t\t\tSystem.out.println(\"--------- 执行 SaCheckELRootMap 增强，目前已包含的的跟对象包括：\" + rootMap.keySet());\n\t\t\t// 新增 stpUser 根对象，使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权\n\t\t\trootMap.put(\"stpUser\", StpUserUtil.getStpLogic());\n\t\t};\n    }\n\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport org.springframework.stereotype.Component;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 自定义权限认证接口扩展，Sa-Token 将从此实现类获取每个账号拥有的权限码 \n * \n * @author click33\n * @since 2022-10-13\n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user.add\");\n\t\tlist.add(\"user.update\");\n\t\tlist.add(\"user.get\");\n\t\t// list.add(\"user.delete\");\n\t\tlist.add(\"art.*\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaTwoParamFunction;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\n\nimport java.util.List;\n\n/**\n * 【User账号体系】Sa-Token 权限认证工具类\n *\n * @author click33\n * @since 1.0.0\n */\npublic class StpUserUtil {\n\n\tprivate StpUserUtil() {}\n\n\t/**\n\t * 多账号体系下的类型标识\n\t */\n\tpublic static final String TYPE = \"user\";\n\n\t/**\n\t * 底层使用的 StpLogic 对象\n\t */\n\tpublic static StpLogic stpLogic = new StpLogic(TYPE);\n\n\t/**\n\t * 获取当前 StpLogic 的账号类型\n\t *\n\t * @return /\n\t */\n\tpublic static String getLoginType(){\n\t\treturn stpLogic.getLoginType();\n\t}\n\n\t/**\n\t * 安全的重置 StpLogic 对象\n\t *\n\t * <br> 1、更改此账户的 StpLogic 对象\n\t * <br> 2、put 到全局 StpLogic 集合中\n\t * <br> 3、发送日志\n\t *\n\t * @param newStpLogic /\n\t */\n\tpublic static void setStpLogic(StpLogic newStpLogic) {\n\t\t// 1、重置此账户的 StpLogic 对象\n\t\tstpLogic = newStpLogic;\n\n\t\t// 2、添加到全局 StpLogic 集合中\n\t\t//    以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic\n\t\tSaManager.putStpLogic(newStpLogic);\n\n\t\t// 3、$$ 发布事件：更新了 stpLogic 对象\n\t\tSaTokenEventCenter.doSetStpLogic(stpLogic);\n\t}\n\n\t/**\n\t * 获取 StpLogic 对象\n\t *\n\t * @return /\n\t */\n\tpublic static StpLogic getStpLogic() {\n\t\treturn stpLogic;\n\t}\n\n\n\t// ------------------- 获取 token 相关 -------------------\n\n\t/**\n\t * 返回 token 名称，此名称在以下地方体现：Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀\n\t *\n\t * @return /\n\t */\n\tpublic static String getTokenName() {\n\t\treturn stpLogic.getTokenName();\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t */\n\tpublic static void setTokenValue(String tokenValue){\n\t\tstpLogic.setTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param cookieTimeout Cookie存活时间(秒)\n\t */\n\tpublic static void setTokenValue(String tokenValue, int cookieTimeout){\n\t\tstpLogic.setTokenValue(tokenValue, cookieTimeout);\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param loginParameter 登录参数\n\t */\n\tpublic static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){\n\t\tstpLogic.setTokenValue(tokenValue, loginParameter);\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值\n\t *\n\t * @return 当前tokenValue\n\t */\n\tpublic static String getTokenValue() {\n\t\treturn stpLogic.getTokenValue();\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值 （不裁剪前缀）\n\t *\n\t * @return /\n\t */\n\tpublic static String getTokenValueNotCut(){\n\t\treturn stpLogic.getTokenValueNotCut();\n\t}\n\n\t/**\n\t * 获取当前会话的 token 参数信息\n\t *\n\t * @return token 参数信息\n\t */\n\tpublic static SaTokenInfo getTokenInfo() {\n\t\treturn stpLogic.getTokenInfo();\n\t}\n\n\n\t// ------------------- 登录相关操作 -------------------\n\n\t// --- 登录\n\n\t/**\n\t * 会话登录\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t */\n\tpublic static void login(Object id) {\n\t\tstpLogic.login(id);\n\t}\n\n\t/**\n\t * 会话登录，并指定登录设备类型\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param deviceType 设备类型\n\t */\n\tpublic static void login(Object id, String deviceType) {\n\t\tstpLogic.login(id, deviceType);\n\t}\n\n\t/**\n\t * 会话登录，并指定是否 [记住我]\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param isLastingCookie 是否为持久Cookie，值为 true 时记住我，值为 false 时关闭浏览器需要重新登录\n\t */\n\tpublic static void login(Object id, boolean isLastingCookie) {\n\t\tstpLogic.login(id, isLastingCookie);\n\t}\n\n\t/**\n\t * 会话登录，并指定此次登录 token 的有效期, 单位:秒\n\t *\n\t * @param id      账号id，建议的类型：（long | int | String）\n\t * @param timeout 此次登录 token 的有效期, 单位:秒\n\t */\n\tpublic static void login(Object id, long timeout) {\n\t\tstpLogic.login(id, timeout);\n\t}\n\n\t/**\n\t * 会话登录，并指定所有登录参数 Model\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model\n\t */\n\tpublic static void login(Object id, SaLoginParameter loginParameter) {\n\t\tstpLogic.login(id, loginParameter);\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic static String createLoginSession(Object id) {\n\t\treturn stpLogic.createLoginSession(id);\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model\n\t * @return 返回会话令牌\n\t */\n\tpublic static String createLoginSession(Object id, SaLoginParameter loginParameter) {\n\t\treturn stpLogic.createLoginSession(id, loginParameter);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的登录会话数据，如果获取不到则创建并返回\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic static String getOrCreateLoginSession(Object id) {\n\t\treturn stpLogic.getOrCreateLoginSession(id);\n\t}\n\n\t// --- 注销 (根据 token)\n\n\t/**\n\t * 在当前客户端会话注销\n\t */\n\tpublic static void logout() {\n\t\tstpLogic.logout();\n\t}\n\n\t/**\n\t * 在当前客户端会话注销，根据注销参数\n\t */\n\tpublic static void logout(SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logout(logoutParameter);\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void logoutByTokenValue(String tokenValue) {\n\t\tstpLogic.logoutByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token、注销参数\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logoutByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void kickoutByTokenValue(String tokenValue) {\n\t\tstpLogic.kickoutByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.kickoutByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void replacedByTokenValue(String tokenValue) {\n\t\tstpLogic.replacedByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.replacedByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t// --- 注销 (根据 loginId)\n\n\t/**\n\t * 会话注销，根据账号id\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void logout(Object loginId) {\n\t\tstpLogic.logout(loginId);\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 设备类型\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型)\n\t */\n\tpublic static void logout(Object loginId, String deviceType) {\n\t\tstpLogic.logout(loginId, deviceType);\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 注销参数\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void logout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logout(loginId, logoutParameter);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void kickout(Object loginId) {\n\t\tstpLogic.kickout(loginId);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型)\n\t */\n\tpublic static void kickout(Object loginId, String deviceType) {\n\t\tstpLogic.kickout(loginId, deviceType);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void kickout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.kickout(loginId, logoutParameter);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void replaced(Object loginId) {\n\t\tstpLogic.replaced(loginId);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 （填 null 代表顶替该账号的所有设备类型）\n\t */\n\tpublic static void replaced(Object loginId, String deviceType) {\n\t\tstpLogic.replaced(loginId, deviceType);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void replaced(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.replaced(loginId, logoutParameter);\n\t}\n\n\t// --- 注销 (会话管理辅助方法)\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (注销下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByLogout(session, terminal);\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByKickout(session, terminal);\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByReplaced(session, terminal);\n\t}\n\n\n\t// 会话查询\n\n\t/**\n\t * 判断当前会话是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic static boolean isLogin() {\n\t\treturn stpLogic.isLogin();\n\t}\n\n\t/**\n\t * 判断指定账号是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic static boolean isLogin(Object loginId) {\n\t\treturn stpLogic.isLogin(loginId);\n\t}\n\n\t/**\n\t * 检验当前会话是否已经登录，如未登录，则抛出异常\n\t */\n\tpublic static void checkLogin() {\n\t\tstpLogic.checkLogin();\n\t}\n\n\t/**\n\t * 获取当前会话账号id，如果未登录，则抛出异常\n\t *\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginId() {\n\t\treturn stpLogic.getLoginId();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回默认值\n\t *\n\t * @param <T> 返回类型\n\t * @param defaultValue 默认值\n\t * @return 登录id\n\t */\n\tpublic static <T> T getLoginId(T defaultValue) {\n\t\treturn stpLogic.getLoginId(defaultValue);\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回null\n\t *\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginIdDefaultNull() {\n\t\treturn stpLogic.getLoginIdDefaultNull();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 String 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static String getLoginIdAsString() {\n\t\treturn stpLogic.getLoginIdAsString();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 int 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static int getLoginIdAsInt() {\n\t\treturn stpLogic.getLoginIdAsInt();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 long 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static long getLoginIdAsLong() {\n\t\treturn stpLogic.getLoginIdAsLong();\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶、被冻结等状态，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginIdByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginIdByToken(tokenValue);\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结)，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n\tpublic Object getLoginIdByTokenNotThinkFreeze(String tokenValue) {\n\t\treturn stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param key 键值\n\t * @return 对应的扩展数据\n\t */\n\tpublic static Object getExtra(String key) {\n\t\treturn stpLogic.getExtra(key);\n\t}\n\n\t/**\n\t * 获取指定 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param tokenValue 指定的 Token 值\n\t * @param key 键值\n\t * @return 对应的扩展数据\n\t */\n\tpublic static Object getExtra(String tokenValue, String key) {\n\t\treturn stpLogic.getExtra(tokenValue, key);\n\t}\n\n\n\t// ------------------- Account-Session 相关 -------------------\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @param isCreate 是否新建\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionByLoginId(Object loginId, boolean isCreate) {\n\t\treturn stpLogic.getSessionByLoginId(loginId, isCreate);\n\t}\n\n\t/**\n\t * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建，则返回 null\n\t *\n\t * @param sessionId SessionId\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSessionBySessionId(String sessionId) {\n\t\treturn stpLogic.getSessionBySessionId(sessionId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionByLoginId(Object loginId) {\n\t\treturn stpLogic.getSessionByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param isCreate 是否新建\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSession(boolean isCreate) {\n\t\treturn stpLogic.getSession(isCreate);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSession() {\n\t\treturn stpLogic.getSession();\n\t}\n\n\n\t// ------------------- Token-Session 相关 -------------------\n\n\t/**\n\t * 获取指定 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param tokenValue Token值\n\t * @return Session对象\n\t */\n\tpublic static SaSession getTokenSessionByToken(String tokenValue) {\n\t\treturn stpLogic.getTokenSessionByToken(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic static SaSession getTokenSession() {\n\t\treturn stpLogic.getTokenSession();\n\t}\n\n\t/**\n\t * 获取当前匿名 Token-Session （可在未登录情况下使用的Token-Session）\n\t *\n\t * @return Token-Session 对象\n\t */\n\tpublic static SaSession getAnonTokenSession() {\n\t\treturn stpLogic.getAnonTokenSession();\n\t}\n\n\n\t// ------------------- Active-Timeout token 最低活跃度 验证相关 -------------------\n\n\t/**\n\t * 续签当前 token：(将 [最后操作时间] 更新为当前时间戳)\n\t * <h2>\n\t * \t\t请注意: 即使 token 已被冻结 也可续签成功，\n\t * \t\t如果此场景下需要提示续签失败，可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可\n\t * </h2>\n\t */\n\tpublic static void updateLastActiveToNow() {\n\t\tstpLogic.updateLastActiveToNow();\n\t}\n\n\t/**\n\t * 检查当前 token 是否已被冻结，如果是则抛出异常\n\t */\n\tpublic static void checkActiveTimeout() {\n\t\tstpLogic.checkActiveTimeout();\n\t}\n\n\n\t// ------------------- 过期时间相关 -------------------\n\n\t/**\n\t * 获取当前会话 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getTokenTimeout() {\n\t\treturn stpLogic.getTokenTimeout();\n\t}\n\n\t/**\n\t * 获取指定 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @param token 指定token\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getTokenTimeout(String token) {\n\t\treturn stpLogic.getTokenTimeout(token);\n\t}\n\n\t/**\n\t * 获取当前登录账号的 Account-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getSessionTimeout() {\n\t\treturn stpLogic.getSessionTimeout();\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getTokenSessionTimeout() {\n\t\treturn stpLogic.getTokenSessionTimeout();\n\t}\n\n\t/**\n\t * 获取当前 token 剩余活跃有效期：当前 token 距离被冻结还剩多少时间（单位: 秒，返回 -1 代表永不冻结，-2 代表没有这个值或 token 已被冻结了）\n\t *\n\t * @return /\n\t */\n\tpublic static long getTokenActiveTimeout() {\n\t\treturn stpLogic.getTokenActiveTimeout();\n\t}\n\n\t/**\n\t * 对当前 token 的 timeout 值进行续期\n\t *\n\t * @param timeout 要修改成为的有效时间 (单位: 秒)\n\t */\n\tpublic static void renewTimeout(long timeout) {\n\t\tstpLogic.renewTimeout(timeout);\n\t}\n\n\t/**\n\t * 对指定 token 的 timeout 值进行续期\n\t *\n\t * @param tokenValue 指定 token\n\t * @param timeout 要修改成为的有效时间 (单位: 秒，填 -1 代表要续为永久有效)\n\t */\n\tpublic static void renewTimeout(String tokenValue, long timeout) {\n\t\tstpLogic.renewTimeout(tokenValue, timeout);\n\t}\n\n\n\t// ------------------- 角色认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的角色集合\n\t *\n\t * @return /\n\t */\n\tpublic static List<String> getRoleList() {\n\t\treturn stpLogic.getRoleList();\n\t}\n\n\t/**\n\t * 获取：指定账号的角色集合\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic static List<String> getRoleList(Object loginId) {\n\t\treturn stpLogic.getRoleList(loginId);\n\t}\n\n\t/**\n\t * 判断：当前账号是否拥有指定角色, 返回 true 或 false\n\t *\n\t * @param role 角色\n\t * @return /\n\t */\n\tpublic static boolean hasRole(String role) {\n\t\treturn stpLogic.hasRole(role);\n\t}\n\n\t/**\n\t * 判断：指定账号是否含有指定角色标识, 返回 true 或 false\n\t *\n\t * @param loginId 账号id\n\t * @param role 角色标识\n\t * @return 是否含有指定角色标识\n\t */\n\tpublic static boolean hasRole(Object loginId, String role) {\n\t\treturn stpLogic.hasRole(loginId, role);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n\tpublic static boolean hasRoleAnd(String... roleArray){\n\t\treturn stpLogic.hasRoleAnd(roleArray);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n\tpublic static boolean hasRoleOr(String... roleArray){\n\t\treturn stpLogic.hasRoleOr(roleArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识, 如果验证未通过，则抛出异常: NotRoleException\n\t *\n\t * @param role 角色标识\n\t */\n\tpublic static void checkRole(String role) {\n\t\tstpLogic.checkRole(role);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n\tpublic static void checkRoleAnd(String... roleArray){\n\t\tstpLogic.checkRoleAnd(roleArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n\tpublic static void checkRoleOr(String... roleArray){\n\t\tstpLogic.checkRoleOr(roleArray);\n\t}\n\n\n\t// ------------------- 权限认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的权限码集合\n\t *\n\t * @return /\n\t */\n\tpublic static List<String> getPermissionList() {\n\t\treturn stpLogic.getPermissionList();\n\t}\n\n\t/**\n\t * 获取：指定账号的权限码集合\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic static List<String> getPermissionList(Object loginId) {\n\t\treturn stpLogic.getPermissionList(loginId);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic static boolean hasPermission(String permission) {\n\t\treturn stpLogic.hasPermission(permission);\n\t}\n\n\t/**\n\t * 判断：指定账号 id 是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param loginId 账号 id\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic static boolean hasPermission(Object loginId, String permission) {\n\t\treturn stpLogic.hasPermission(loginId, permission);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，必须全部具有 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n\tpublic static boolean hasPermissionAnd(String... permissionArray){\n\t\treturn stpLogic.hasPermissionAnd(permissionArray);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n\tpublic static boolean hasPermissionOr(String... permissionArray){\n\t\treturn stpLogic.hasPermissionOr(permissionArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限, 如果验证未通过，则抛出异常: NotPermissionException\n\t *\n\t * @param permission 权限码\n\t */\n\tpublic static void checkPermission(String permission) {\n\t\tstpLogic.checkPermission(permission);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic static void checkPermissionAnd(String... permissionArray) {\n\t\tstpLogic.checkPermissionAnd(permissionArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic static void checkPermissionOr(String... permissionArray) {\n\t\tstpLogic.checkPermissionOr(permissionArray);\n\t}\n\n\n\t// ------------------- id 反查 token 相关操作 -------------------\n\n\t/**\n\t * 获取指定账号 id 的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @return token值\n\t */\n\tpublic static String getTokenValueByLoginId(Object loginId) {\n\t\treturn stpLogic.getTokenValueByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return token值\n\t */\n\tpublic static String getTokenValueByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTokenValueByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有相关 token\n\t */\n\tpublic static List<String> getTokenValueListByLoginId(Object loginId) {\n\t\treturn stpLogic.getTokenValueListByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic static List<String> getTokenValueListByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTokenValueListByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic static List<SaTerminalInfo> getTerminalListByLoginId(Object loginId) {\n\t\treturn stpLogic.getTerminalListByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return /\n\t */\n\tpublic static List<SaTerminalInfo> getTerminalListByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTerminalListByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合，执行特定函数\n\t *\n\t * @param loginId 账号id\n\t * @param function 需要执行的函数\n\t */\n\tpublic static void forEachTerminalList(Object loginId, SaTwoParamFunction<SaSession, SaTerminalInfo> function) {\n\t\tstpLogic.forEachTerminalList(loginId, function);\n\t}\n\n\t/**\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic static String getLoginDeviceType() {\n\t\treturn stpLogic.getLoginDeviceType();\n\t}\n\n\t/**\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic static String getLoginDeviceTypeByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginDeviceTypeByToken(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 token 的最后活跃时间（13位时间戳），如果不存在则返回 -2\n\t *\n\t * @return /\n\t */\n\tpublic static long getTokenLastActiveTime() {\n\t\treturn stpLogic.getTokenLastActiveTime();\n\t}\n\n\t/**\n\t * 判断对于指定 loginId 来讲，指定设备 id 是否为可信任设备\n\t * @param deviceId /\n\t * @return /\n\t */\n\tpublic static boolean isTrustDeviceId(Object userId, String deviceId) {\n\t\treturn stpLogic.isTrustDeviceId(userId, deviceId);\n\t}\n\n\n\n\t// ------------------- 会话管理 -------------------\n\n\t/**\n\t * 根据条件查询缓存中所有的 token\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return token集合\n\t */\n\tpublic static List<String> searchTokenValue(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchTokenValue(keyword, start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 SessionId\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量  (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic static List<String> searchSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchSessionId(keyword, start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 Token-Session-Id\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic static List<String> searchTokenSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchTokenSessionId(keyword, start, size, sortType);\n\t}\n\n\n\t// ------------------- 账号封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号\n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disable(Object loginId, long time) {\n\t\tstpLogic.disable(loginId, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁 (true=已被封禁, false=未被封禁)\n\t *\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic static boolean isDisable(Object loginId) {\n\t\treturn stpLogic.isDisable(loginId);\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void checkDisable(Object loginId) {\n\t\tstpLogic.checkDisable(loginId);\n\t}\n\n\t/**\n\t * 获取：指定账号剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic static long getDisableTime(Object loginId) {\n\t\treturn stpLogic.getDisableTime(loginId);\n\t}\n\n\t/**\n\t * 解封：指定账号\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void untieDisable(Object loginId) {\n\t\tstpLogic.untieDisable(loginId);\n\t}\n\n\n\t// ------------------- 分类封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号的指定服务\n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定服务\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disable(Object loginId, String service, long time) {\n\t\tstpLogic.disable(loginId, service, time);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务 是否已被封禁（true=已被封禁, false=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务\n\t * @return /\n\t */\n\tpublic static boolean isDisable(Object loginId, String service) {\n\t\treturn stpLogic.isDisable(loginId, service);\n\t}\n\n\t/**\n\t * 校验：指定账号 指定服务 是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个\n\t */\n\tpublic static void checkDisable(Object loginId, String... services) {\n\t\tstpLogic.checkDisable(loginId, services);\n\t}\n\n\t/**\n\t * 获取：指定账号 指定服务 剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务\n\t * @return see note\n\t */\n\tpublic static long getDisableTime(Object loginId, String service) {\n\t\treturn stpLogic.getDisableTime(loginId, service);\n\t}\n\n\t/**\n\t * 解封：指定账号、指定服务\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个\n\t */\n\tpublic static void untieDisable(Object loginId, String... services) {\n\t\tstpLogic.untieDisable(loginId, services);\n\t}\n\n\n\t// ------------------- 阶梯封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 指定封禁等级\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disableLevel(Object loginId, int level, long time) {\n\t\tstpLogic.disableLevel(loginId, level, time);\n\t}\n\n\t/**\n\t * 封禁：指定账号的指定服务，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 指定封禁等级\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disableLevel(Object loginId, String service, int level, long time) {\n\t\tstpLogic.disableLevel(loginId, service, level, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁到指定等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 指定封禁等级\n\t * @return /\n\t */\n\tpublic static boolean isDisableLevel(Object loginId, int level) {\n\t\treturn stpLogic.isDisableLevel(loginId, level);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务，是否已被封禁到指定等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 指定封禁等级\n\t * @return /\n\t */\n\tpublic static boolean isDisableLevel(Object loginId, String service, int level) {\n\t\treturn stpLogic.isDisableLevel(loginId, service, level);\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic static void checkDisableLevel(Object loginId, int level) {\n\t\tstpLogic.checkDisableLevel(loginId, level);\n\t}\n\n\t/**\n\t * 校验：指定账号的指定服务，是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic static void checkDisableLevel(Object loginId, String service, int level) {\n\t\tstpLogic.checkDisableLevel(loginId, service, level);\n\t}\n\n\t/**\n\t * 获取：指定账号被封禁的等级，如果未被封禁则返回-2\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic static int getDisableLevel(Object loginId) {\n\t\treturn stpLogic.getDisableLevel(loginId);\n\t}\n\n\t/**\n\t * 获取：指定账号的 指定服务 被封禁的等级，如果未被封禁则返回-2\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @return /\n\t */\n\tpublic static int getDisableLevel(Object loginId, String service) {\n\t\treturn stpLogic.getDisableLevel(loginId, service);\n\t}\n\n\n\t// ------------------- 临时身份切换 -------------------\n\n\t/**\n\t * 临时切换身份为指定账号id\n\t *\n\t * @param loginId 指定loginId\n\t */\n\tpublic static void switchTo(Object loginId) {\n\t\tstpLogic.switchTo(loginId);\n\t}\n\n\t/**\n\t * 结束临时切换身份\n\t */\n\tpublic static void endSwitch() {\n\t\tstpLogic.endSwitch();\n\t}\n\n\t/**\n\t * 判断当前请求是否正处于 [ 身份临时切换 ] 中\n\t *\n\t * @return /\n\t */\n\tpublic static boolean isSwitch() {\n\t\treturn stpLogic.isSwitch();\n\t}\n\n\t/**\n\t * 在一个 lambda 代码段里，临时切换身份为指定账号id，lambda 结束后自动恢复\n\t *\n\t * @param loginId 指定账号id\n\t * @param function 要执行的方法\n\t */\n\tpublic static void switchTo(Object loginId, SaFunction function) {\n\t\tstpLogic.switchTo(loginId, function);\n\t}\n\n\n\t// ------------------- 二级认证 -------------------\n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param safeTime 维持时间 (单位: 秒)\n\t */\n\tpublic static void openSafe(long safeTime) {\n\t\tstpLogic.openSafe(safeTime);\n\t}\n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param service 业务标识\n\t * @param safeTime 维持时间 (单位: 秒)\n\t */\n\tpublic static void openSafe(String service, long safeTime) {\n\t\tstpLogic.openSafe(service, safeTime);\n\t}\n\n\t/**\n\t * 判断：当前会话是否处于二级认证时间内\n\t *\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic static boolean isSafe() {\n\t\treturn stpLogic.isSafe();\n\t}\n\n\t/**\n\t * 判断：当前会话 是否处于指定业务的二级认证时间内\n\t *\n\t * @param service 业务标识\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic static boolean isSafe(String service) {\n\t\treturn stpLogic.isSafe(service);\n\t}\n\n\t/**\n\t * 判断：指定 token 是否处于二级认证时间内\n\t *\n\t * @param tokenValue Token 值\n\t * @param service 业务标识\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic static boolean isSafe(String tokenValue, String service) {\n\t\treturn stpLogic.isSafe(tokenValue, service);\n\t}\n\n\t/**\n\t * 校验：当前会话是否已通过二级认证，如未通过则抛出异常\n\t */\n\tpublic static void checkSafe() {\n\t\tstpLogic.checkSafe();\n\t}\n\n\t/**\n\t * 校验：检查当前会话是否已通过指定业务的二级认证，如未通过则抛出异常\n\t *\n\t * @param service 业务标识\n\t */\n\tpublic static void checkSafe(String service) {\n\t\tstpLogic.checkSafe(service);\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @return 剩余有效时间\n\t */\n\tpublic static long getSafeTime() {\n\t\treturn stpLogic.getSafeTime();\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @param service 业务标识\n\t * @return 剩余有效时间\n\t */\n\tpublic static long getSafeTime(String service) {\n\t\treturn stpLogic.getSafeTime(service);\n\t}\n\n\t/**\n\t * 在当前会话 结束二级认证\n\t */\n\tpublic static void closeSafe() {\n\t\tstpLogic.closeSafe();\n\t}\n\n\t/**\n\t * 在当前会话 结束指定业务标识的二级认证\n\t *\n\t * @param service 业务标识\n\t */\n\tpublic static void closeSafe(String service) {\n\t\tstpLogic.closeSafe(service);\n\t}\n\n\n\t// ------------------- Bean 对象、字段代理 -------------------\n\n\t/**\n\t * 根据当前配置对象创建一个 SaLoginParameter 对象\n\t *\n\t * @return /\n\t */\n\tpublic static SaLoginParameter createSaLoginParameter() {\n\t\treturn stpLogic.createSaLoginParameter();\n\t}\n\n\n\t// ------------------- 过期方法 -------------------\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceType </h2>\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic static String getLoginDevice() {\n\t\treturn stpLogic.getLoginDevice();\n\t}\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceTypeByToken </h2>\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic static String getLoginDeviceByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginDeviceByToken(tokenValue);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java",
    "content": "package com.pj.satoken.custom_annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 账号校验：在标注一个方法上时，要求前端必须提交相应的账号密码参数才能访问方法。\n *\n * @author click33\n *\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface CheckAccount {\n\n    /**\n     * 需要校验的账号\n     *\n     * @return /\n     */\n    String name();\n\n    /**\n     * 需要校验的密码\n     *\n     * @return /\n     */\n    String pwd();\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckLogin.java",
    "content": "package com.pj.satoken.custom_annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 登录认证(User版)：只有登录之后才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckLogin {\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckPermission.java",
    "content": "package com.pj.satoken.custom_annotation;\n\nimport cn.dev33.satoken.annotation.SaMode;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 权限认证(User版)：必须具有指定权限才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n *\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckPermission {\n\n\t/**\n\t * 需要校验的权限码\n\t * @return 需要校验的权限码\n\t */\n\tString [] value() default {};\n\n\t/**\n\t * 验证模式：AND | OR，默认AND\n\t * @return 验证模式\n\t */\n\tSaMode mode() default SaMode.AND;\n\n\t/**\n\t * 在权限校验不通过时的次要选择，两者只要其一校验成功即可通过校验\n\t *\n\t * <p>\n\t * \t例1：@SaCheckPermission(value=\"user-add\", orRole=\"admin\")，\n\t * \t代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。\n\t * </p>\n\t *\n\t * <p>\n\t * \t例2： orRole = {\"admin\", \"manager\", \"staff\"}，具有三个角色其一即可。 <br>\n\t * \t例3： orRole = {\"admin, manager, staff\"}，必须三个角色同时具备。\n\t * </p>\n\t *\n\t * @return /\n\t */\n\tString[] orRole() default {};\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckRole.java",
    "content": "package com.pj.satoken.custom_annotation;\n\nimport cn.dev33.satoken.annotation.SaMode;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 角色认证(User版)：必须具有指定角色标识才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上） \n * @author click33\n *\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckRole {\n\n\t/**\n\t * 需要校验的角色标识\n\t * @return 需要校验的角色标识\n\t */\n\tString [] value() default {};\n\n\t/**\n\t * 验证模式：AND | OR，默认AND\n\t * @return 验证模式\n\t */\n\tSaMode mode() default SaMode.AND;\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/SaUserCheckSafe.java",
    "content": "package com.pj.satoken.custom_annotation;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 二级认证校验(User版)：客户端必须完成二级认证之后，才能进入该方法，否则将被抛出异常。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）。\n *\n * @author click33\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaUserCheckSafe {\n\n\t/**\n\t * 要校验的服务\n\t *\n\t * @return /\n\t */\n\tString value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE;\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java",
    "content": "package com.pj.satoken.custom_annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport com.pj.satoken.custom_annotation.CheckAccount;\nimport org.springframework.stereotype.Component;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 CheckAccount 的处理器\n *\n * @author click33\n *\n */\n@Component\npublic class CheckAccountHandler implements SaAnnotationHandlerInterface<CheckAccount> {\n\n    // 指定这个处理器要处理哪个注解\n    @Override\n    public Class<CheckAccount> getHandlerAnnotationClass() {\n        return CheckAccount.class;\n    }\n\n    // 每次请求校验注解时，会执行的方法\n    @Override\n    public void checkMethod(CheckAccount at, AnnotatedElement element) {\n        // 获取前端请求提交的参数\n        String name = SaHolder.getRequest().getParamNotNull(\"name\");\n        String pwd = SaHolder.getRequest().getParamNotNull(\"pwd\");\n\n        // 与注解中指定的值相比较\n        if(name.equals(at.name()) && pwd.equals(at.pwd()) ) {\n            // 校验通过，什么也不做\n        } else {\n            // 校验不通过，则抛出异常\n            throw new SaTokenException(\"账号或密码错误，未通过校验\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckLoginHandler.java",
    "content": "package com.pj.satoken.custom_annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.annotation.handler.SaCheckLoginHandler;\nimport com.pj.satoken.StpUserUtil;\nimport com.pj.satoken.custom_annotation.SaUserCheckLogin;\nimport org.springframework.stereotype.Component;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaUserCheckLogin 的处理器\n *\n * @author click33\n */\n@Component\npublic class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface<SaUserCheckLogin> {\n\n    @Override\n    public Class<SaUserCheckLogin> getHandlerAnnotationClass() {\n        return SaUserCheckLogin.class;\n    }\n\n    @Override\n    public void checkMethod(SaUserCheckLogin at, AnnotatedElement element) {\n        SaCheckLoginHandler._checkMethod(StpUserUtil.TYPE);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckPermissionHandler.java",
    "content": "package com.pj.satoken.custom_annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.annotation.handler.SaCheckPermissionHandler;\nimport com.pj.satoken.StpUserUtil;\nimport com.pj.satoken.custom_annotation.SaUserCheckPermission;\nimport org.springframework.stereotype.Component;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaUserCheckPermission 的处理器\n *\n * @author click33\n */\n@Component\npublic class SaUserCheckPermissionHandler implements SaAnnotationHandlerInterface<SaUserCheckPermission> {\n\n    @Override\n    public Class<SaUserCheckPermission> getHandlerAnnotationClass() {\n        return SaUserCheckPermission.class;\n    }\n\n    @Override\n    public void checkMethod(SaUserCheckPermission at, AnnotatedElement element) {\n        SaCheckPermissionHandler._checkMethod(StpUserUtil.TYPE, at.value(), at.mode(), at.orRole());\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckRoleHandler.java",
    "content": "package com.pj.satoken.custom_annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.annotation.handler.SaCheckRoleHandler;\nimport com.pj.satoken.StpUserUtil;\nimport com.pj.satoken.custom_annotation.SaUserCheckRole;\nimport org.springframework.stereotype.Component;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaUserCheckRole 的处理器\n *\n * @author click33\n */\n@Component\npublic class SaUserCheckRoleHandler implements SaAnnotationHandlerInterface<SaUserCheckRole> {\n\n    @Override\n    public Class<SaUserCheckRole> getHandlerAnnotationClass() {\n        return SaUserCheckRole.class;\n    }\n\n    @Override\n    public void checkMethod(SaUserCheckRole at, AnnotatedElement element) {\n        SaCheckRoleHandler._checkMethod(StpUserUtil.TYPE, at.value(), at.mode());\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation/handler/SaUserCheckSafeHandler.java",
    "content": "package com.pj.satoken.custom_annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.annotation.handler.SaCheckSafeHandler;\nimport com.pj.satoken.StpUserUtil;\nimport com.pj.satoken.custom_annotation.SaUserCheckSafe;\nimport org.springframework.stereotype.Component;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaUserCheckPermission 的处理器\n *\n * @author click33\n */\n@Component\npublic class SaUserCheckSafeHandler implements SaAnnotationHandlerInterface<SaUserCheckSafe> {\n\n    @Override\n    public Class<SaUserCheckSafe> getHandlerAnnotationClass() {\n        return SaUserCheckSafe.class;\n    }\n\n    @Override\n    public void checkMethod(SaUserCheckSafe at, AnnotatedElement element) {\n        SaCheckSafeHandler._checkMethod(StpUserUtil.TYPE, at.value());\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckLogin.java",
    "content": "package com.pj.satoken.merge_annotation;\n\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport com.pj.satoken.StpUserUtil;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 登录认证(User版)：只有登录之后才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上） \n * @author click33\n *\n */\n@SaCheckLogin(type = StpUserUtil.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckLogin {\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckPermission.java",
    "content": "package com.pj.satoken.merge_annotation;\n\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaMode;\nimport com.pj.satoken.StpUserUtil;\nimport org.springframework.core.annotation.AliasFor;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 权限认证(User版)：必须具有指定权限才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上） \n * @author click33\n *\n */\n@SaCheckPermission(type = StpUserUtil.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckPermission {\n\n\t/**\n\t * 需要校验的权限码\n\t * @return 需要校验的权限码\n\t */\n\t@AliasFor(annotation = SaCheckPermission.class)\n\tString [] value() default {};\n\n\t/**\n\t * 验证模式：AND | OR，默认AND\n\t * @return 验证模式\n\t */\n\t@AliasFor(annotation = SaCheckPermission.class)\n\tSaMode mode() default SaMode.AND;\n\n\t/**\n\t * 在权限校验不通过时的次要选择，两者只要其一校验成功即可通过校验\n\t *\n\t * <p>\n\t * \t例1：@SaCheckPermission(value=\"user-add\", orRole=\"admin\")，\n\t * \t代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。\n\t * </p>\n\t *\n\t * <p>\n\t * \t例2： orRole = {\"admin\", \"manager\", \"staff\"}，具有三个角色其一即可。 <br>\n\t * \t例3： orRole = {\"admin, manager, staff\"}，必须三个角色同时具备。\n\t * </p>\n\t *\n\t * @return /\n\t */\n\t@AliasFor(annotation = SaCheckPermission.class)\n\tString[] orRole() default {};\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckRole.java",
    "content": "package com.pj.satoken.merge_annotation;\n\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaMode;\nimport com.pj.satoken.StpUserUtil;\nimport org.springframework.core.annotation.AliasFor;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 角色认证(User版)：必须具有指定角色标识才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上） \n * @author click33\n *\n */\n@SaCheckRole(type = StpUserUtil.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckRole {\n\n\t/**\n\t * 需要校验的角色标识\n\t * @return 需要校验的角色标识\n\t */\n\t@AliasFor(annotation = SaCheckRole.class)\n\tString [] value() default {};\n\n\t/**\n\t * 验证模式：AND | OR，默认AND\n\t * @return 验证模式\n\t */\n\t@AliasFor(annotation = SaCheckRole.class)\n\tSaMode mode() default SaMode.AND;\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation/SaUserCheckSafe.java",
    "content": "package com.pj.satoken.merge_annotation;\n\nimport cn.dev33.satoken.annotation.SaCheckSafe;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.pj.satoken.StpUserUtil;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 二级认证校验(User版)：客户端必须完成二级认证之后，才能进入该方法，否则将被抛出异常。\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）。\n *\n * @author click33\n */\n@SaCheckSafe(type = StpUserUtil.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaUserCheckSafe {\n\n\t/**\n\t * 要校验的服务\n\t *\n\t * @return /\n\t */\n\tString value() default SaTokenConsts.DEFAULT_SAFE_AUTH_SERVICE;\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-case/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n        \n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n    # jwt 秘钥\n    jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-device-lock</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!--<version>2.3.0.RELEASE</version>-->\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<java.run.main.class>com.pj.SaTokenDeviceLockApplication</java.run.main.class>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token整合 Redis -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t</dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/SaTokenDeviceLockApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n\n/**\n * Sa-Token 测试  \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenDeviceLockApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenDeviceLockApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// 输出 API 请求日志，方便调试代码 \n//        \t\t\t SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n                   \n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行\n        \t\t.setBeforeAuth(obj -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t\n        \t\t\t// ---------- 设置跨域响应头 ----------\n        \t\t\t// 允许指定域访问跨域资源\n        \t\t\t.setHeader(\"Access-Control-Allow-Origin\", \"*\")\n        \t\t\t// 允许所有请求方式\n        \t\t\t.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n        \t\t\t// 有效时间\n        \t\t\t.setHeader(\"Access-Control-Max-Age\", \"3600\")\n        \t\t\t// 允许的header参数\n        \t\t\t.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n        \t\t\t\n        \t\t\t// 如果是预检请求，则立即返回到前端 \n        \t\t\tSaRouter.match(SaHttpMethod.OPTIONS)\n        \t\t\t\t.free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n        \t\t\t\t.back();\n        \t\t})\n        \t\t;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.util.DeviceLockCheckUtil;\nimport com.pj.util.PhoneCodeUtil;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 登录测试\n *\n * @author click33\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t@Autowired\n\tSysUserMockDao userMockDao;\n\n\t// 账号密码登录\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd, String deviceId) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tlong userId = userMockDao.getUserIdByName(name);\n\n\t\t\t// 登录前，检测设备锁\n\t\t\tif ( ! StpUtil.isTrustDeviceId(userId, deviceId)) {\n\t\t\t\tDeviceLockCheckUtil.setDeviceIdToUserId(deviceId, 10001);\n\t\t\t\t// 与前端约定好，返回421表示此设备需要验证\n\t\t\t\treturn SaResult.get(421, \"新设备登录，需要验证设备\", deviceId);\n\t\t\t}\n\n\t\t\t// 登录\n\t\t\treturn login(userId, deviceId);\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.data(StpUtil.isLogin());\n\t}\n\n\t// 注销登录\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\n\t// 返回设备id绑定的 userId 的手机号，脱敏形式\n\t@RequestMapping(\"getPhone\")\n\tpublic SaResult getPhone(String deviceId) {\n\t\tlong userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId);\n\t\tString phone = userMockDao.getPhoneByUserId(userId);\n\t\treturn SaResult.data(phone.substring(0, 3) + \"****\" + phone.substring(7));\n\t}\n\n\t// 发送验证码\n\t@RequestMapping(\"sendCode\")\n\tpublic SaResult sendCode(String deviceId) {\n\t\tlong userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId);\n\t\tString phone = userMockDao.getPhoneByUserId(userId);\n\t\tPhoneCodeUtil.sendCode(phone);\n\t\treturn SaResult.ok();\n\t}\n\n\t// 验证验证码\n\t@RequestMapping(\"checkCode\")\n\tpublic SaResult checkCode(String deviceId, String code) {\n\t\tlong userId = DeviceLockCheckUtil.getUserIdByDeviceId(deviceId);\n\t\tString phone = userMockDao.getPhoneByUserId(userId);\n\t\tPhoneCodeUtil.checkCode(phone, code);\n\t\t// 校验通过，开始登录\n\t\treturn login(userId, deviceId);\n\t}\n\n\t// 指定账号登录\n\tprivate SaResult login(long userId, String deviceId) {\n\t\tStpUtil.login(userId, new SaLoginParameter().setDeviceId(deviceId));\n\t\treturn SaResult.ok(\"登录成功\").set(\"token\", StpUtil.getTokenValue());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/test/SysUserMockDao.java",
    "content": "package com.pj.test;\n\nimport org.springframework.stereotype.Service;\n\n/**\n * 模拟数据库操作类\n *\n * @author click33\n * @since 2025/3/5\n */\n@Service\npublic class SysUserMockDao {\n\n    // 返回指定 userId 绑定的手机号\n    public String getPhoneByUserId(long userId) {\n        return \"13112341234\";\n    }\n\n    // 返回指定用户名对应的 userId\n    public long getUserIdByName(String name) {\n        return 10001;\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/DeviceLockCheckUtil.java",
    "content": "package com.pj.util;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 设备锁操作工具类\n * @author click33\n * @since 2025/3/5\n */\npublic class DeviceLockCheckUtil {\n\n    /**\n     * 保存设备id与用户id的映射关系\n     * @param deviceId /\n     * @param userId /\n     */\n    public static void setDeviceIdToUserId(String deviceId, long userId) {\n        if(SaFoxUtil.isEmpty(deviceId) || SaFoxUtil.isEmpty(userId)) {\n            throw new RuntimeException(\"设备id或用户id不能为空\");\n        }\n        SaManager.getSaTokenDao().set(saveKeyPrefix() + deviceId, String.valueOf(userId), 1200);\n    }\n\n    /**\n     * 返回设备id绑定的用户id\n     * @param deviceId /\n     */\n    public static long getUserIdByDeviceId(String deviceId) {\n        String userIdStr = SaManager.getSaTokenDao().get(saveKeyPrefix() + deviceId);\n        if(userIdStr == null) {\n            throw new RuntimeException(\"此设备id目前未绑定任何用户\");\n        }\n        return Long.parseLong(userIdStr);\n    }\n\n    // 返回数据保存时使用的前缀\n    public static Object saveKeyPrefix() {\n        return SaManager.getConfig().getTokenName() + \":device-to-userid:\";\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/java/com/pj/util/PhoneCodeUtil.java",
    "content": "package com.pj.util;//package com.pj.oauth2.custom;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 手机验证码工具类 （仅做逻辑模拟，不做真实发送）\n *\n * @author click33\n * @since 2024/8/23\n */\npublic class PhoneCodeUtil {\n\n    // 指定手机号发送验证码\n    public static void sendCode(String phone) {\n        String code = SaFoxUtil.getRandomNumber(100000, 999999) + \"\";\n        SaManager.getSaTokenDao().set(\"phone_code:\" + phone, code, 60 * 5);\n        System.out.println(\"手机号：\" + phone + \"，验证码：\" + code + \"，已发送成功\");\n    }\n\n    // 校验验证码是否正确，不正确则抛出异常\n    public static void checkCode(String phone, String code) {\n        String oldCode = SaManager.getSaTokenDao().get(\"phone_code:\" + phone);\n        if( ! code.equals(oldCode) ) {\n            throw new RuntimeException(\"验证码错误\");\n        }\n        // 验证通过后，立即删除验证码\n        SaManager.getSaTokenDao().delete(\"phone_code:\" + phone);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n        \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock-h5/common.js",
    "content": "// 服务器接口主机地址 \nvar baseUrl = \"http://localhost:8081\";\n\n// 封装一下Ajax\nfunction ajax(path, data, successFn) {\n\tconsole.log(baseUrl + path);\n\tfetch(baseUrl + path, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t'satoken': localStorage.getItem('satoken')\n\t\t},\n\t\tbody: serializeToQueryString(data),\t\t\n\t})\n\t.then(response => response.json())\n\t.then(res => {\n\t\tconsole.log('返回数据：', res);\n\t\tsuccessFn(res);\n\t})\n\t.catch(error => {\n\t\tconsole.error('提交失败:', error);\n\t\treturn alert(\"异常：\" + JSON.stringify(error));\n\t});\n}\n\n// 获取本地的 设备id \nfunction getLocalDeviceId() {\n\tlet localDeviceId = localStorage.getItem('local-device-id');\n\tif(!localDeviceId) {\n\t\tlocalDeviceId = randomString(60);\n\t\tlocalStorage.setItem('local-device-id', localDeviceId);\n\t}\n\treturn localDeviceId;\n}\n\n\n\n// ------------ 工具方法 ---------------\n\n// 从url中查询到指定名称的参数值 \nfunction getParam(name, defaultValue){\n\tvar query = window.location.search.substring(1);\n\tvar vars = query.split(\"&\");\n\tfor (var i=0;i<vars.length;i++) {\n\t\tvar pair = vars[i].split(\"=\");\n\t\tif(pair[0] == name){return pair[1];}\n\t}\n\treturn(defaultValue == undefined ? null : defaultValue);\n}\n\n// 将 json 对象序列化为kv字符串，形如：name=Joh&age=30&active=true\nfunction serializeToQueryString(obj) {\n  return Object.entries(obj)\n    .filter(([_, value]) => value != null) // 过滤 null 和 undefined\n    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n    .join('&');\n}\n\n// 随机生成字符串 \nfunction randomString(len) {\n　　len = len || 32;\n　　var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'; \n　　var maxPos = $chars.length;\n　　var str = '';\n　　for (i = 0; i < len; i++) {\n　　　　str += $chars.charAt(Math.floor(Math.random() * maxPos));\n　　}\n　　return str;\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock-h5/device-lock-auth.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>设备锁测试-认证页</title>\n\t\t<style type=\"text/css\">\n\t\t\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t<h2>设备锁测试-认证页</h2>\n\t\t\t<div style=\"color: red;\">您正在一台新设备上登录此账号，需要进行身份验证</div>\n\t\t\t<div>您绑定的手机号为：<b class=\"phone\"></b></div>\n\t\t\t<div>\n\t\t\t\t验证码：<input name=\"ck\" >\n\t\t\t\t<button class=\"send-code\" onclick=\"sendCode()\">发送验证码</button>\n\t\t\t</div>\n\t\t\t<div><button onclick=\"checkCode()\">确认</button></div>\n\t\t</div>\n\t\t<script src=\"common.js\"></script>\n\t\t<script type=\"text/javascript\">\n\t\t\t\n\t\t\t// 获取手机号数据  \n\t\t\tfunction getPhone() {\n\t\t\t\tajax('/acc/getPhone', { deviceId: getLocalDeviceId() }, function(res) {\n\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\tdocument.querySelector('.phone').innerHTML = res.data;\n\t\t\t\t\t} else {\n\t\t\t\t\t\talert('失败：' + res.msg);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\tgetPhone();\n\t\t\t\n\t\t\t// 发送验证码\n\t\t\tfunction sendCode(){\n\t\t\t\tajax('/acc/sendCode', { deviceId: getLocalDeviceId() }, function(res) {\n\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\talert('验证码发送成功，请注意接收');\n\t\t\t\t\t\tdocument.querySelector('.send-code').disabled = true;\n\t\t\t\t\t} \n\t\t\t\t\t// 触发设备锁校验，需要进一步去认证 \n\t\t\t\t\telse {\n\t\t\t\t\t\talert('失败：' + res.msg);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\t\t\t\n\t\t\t// 校验验证码 \n\t\t\tfunction checkCode(){\n\t\t\t\tajax('/acc/checkCode', { deviceId: getLocalDeviceId(), code: document.querySelector('[name=ck]').value }, function(res) {\n\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\talert('验证成功！');\n\t\t\t\t\t\tlocalStorage.setItem('satoken', res.token);\n\t\t\t\t\t\tlocation.href = 'index.html';\n\t\t\t\t\t} \n\t\t\t\t\t// 触发设备锁校验，需要进一步去认证 \n\t\t\t\t\telse {\n\t\t\t\t\t\talert('失败：' + res.msg);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\t\t\t\t\t\n\t\t</script>\n\t\t<script type=\"text/javascript\">\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock-h5/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>设备锁测试-首页</title>\n\t</head>\n\t<body>\n\t\t<h2>设备锁测试-首页</h2>\n\t\t<p>当前是否登录：<b class=\"is-login\"></b></p>\n\t\t<p>\n\t\t\t<a href=\"login.html\">登录</a> &nbsp;\n\t\t\t<a href=\"javascript: doLogout(); \">注销</a>\n\t\t\t<!-- <a href=\"javascript: doLogout(); \">注销</a>&nbsp;<span style=\"color: #888;\">(需要重新验证设备)</span>&nbsp;\n\t\t\t<a href=\"javascript: doLogout2(); \">注销2</a>&nbsp;<span style=\"color: #888;\">(不需要重新验证设备)</span>&nbsp; -->\n\t\t</p>\n\t\t<script src=\"common.js\"></script>\n\t\t<script type=\"text/javascript\">\n\t\t\n\t\t\t// 查询当前会话是否登录 \n\t\t\tfunction isLogin(){\n\t\t\t\tajax('/acc/isLogin', {}, function(res) {\n\t\t\t\t\tdocument.querySelector('.is-login').innerHTML = res.data;\n\t\t\t\t})\n\t\t\t}\n\t\t\tisLogin();\n\t\t\n\t\t\t\n\t\t\t// 注销\n\t\t\tfunction doLogout(){\n\t\t\t\tajax('/acc/logout', {}, function(res) {\n\t\t\t\t\tisLogin();\n\t\t\t\t})\n\t\t\t}\n\t\t\n\t\t\t// 注销2\n\t\t\tfunction doLogout2(){\n\t\t\t\tlocalStorage.removeItem('satoken');\n\t\t\t\tisLogin();\n\t\t\t}\n\t\t\t\t\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-device-lock-h5/login.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>设备锁测试-登录页</title>\n\t\t<style type=\"text/css\">\n\t\t\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t<h2>设备锁测试-登录页</h2>\n\t\t\t<div>用户：<input name=\"name\" type=\"text\"></div>\n\t\t\t<div>密码：<input name=\"pwd\" type=\"password\"></div>\n\t\t\t<div><button onclick=\"doLogin()\">登录</button></div>\n\t\t</div>\n\t\t<script src=\"common.js\"></script>\n\t\t<script type=\"text/javascript\">\n\t\t\n\t\t\t// 登录方法 \n\t\t\tfunction doLogin() {\n\t\t\t\tconst data = {\n\t\t\t\t\tname: document.querySelector('[name=name]').value,\n\t\t\t\t\tpwd: document.querySelector('[name=pwd]').value,\n\t\t\t\t\tdeviceId: getLocalDeviceId()\n\t\t\t\t}\n\t\t\t\tajax('/acc/doLogin', data, function(res) {\n\t\t\t\t\tconsole.log(res);\n\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\talert('登录成功！');\n\t\t\t\t\t\tlocalStorage.setItem('satoken', res.token);\n\t\t\t\t\t\tlocation.href = 'index.html';\n\t\t\t\t\t} \n\t\t\t\t\t// 触发设备锁校验，需要进一步去认证 \n\t\t\t\t\telse if(res.code == 421) {\n\t\t\t\t\t\tlocation.href = 'device-lock-auth.html';\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t})\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t</script>\n\t\t<script type=\"text/javascript\">\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>com.pj</groupId>\n\t<artifactId>sa-token-demo-dubbo-consumer</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<!--<version>2.3.1.RELEASE</version>-->\n\t\t<version>2.5.15</version>\n\t</parent>\n\n\t<!-- 指定一些属性 -->\n\t<properties> \n\t\t<java.version>1.8</java.version>\n\t\t<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<dubbo.version>2.7.21</dubbo.version>\n\t\t<nacos.version>1.4.2</nacos.version>\n\t</properties>\n\t\n\t<dependencies>\n\n\t\t<!-- SpringBoot Web模块 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-spring-boot-starter</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Dubbo 注册到 Nacos -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-registry-nacos</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.alibaba.nacos</groupId>\n\t\t\t<artifactId>nacos-client</artifactId>\n\t\t\t<version>${nacos.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 整合 Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-dubbo</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/DubboConsumerApplication.java",
    "content": "package com.pj;\n\nimport org.apache.dubbo.config.spring.context.annotation.EnableDubbo;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Dubbo 服务消费端 \n * \n * @author click33\n *\n */\n@EnableDubbo\n@SpringBootApplication\npublic class DubboConsumerApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(DubboConsumerApplication.class, args);\n\t\tSystem.out.println(\"DubboConsumerApplication 启动成功\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/controller/TestController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.service.DemoService;\nimport org.apache.dubbo.config.annotation.DubboReference;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class TestController {\n\t\n\t@DubboReference\n    private DemoService demoService;\n\n\t// Consumer端登录，状态传播到Provider端    --- http://localhost:8081/test\n    @RequestMapping(\"test\")\n    public SaResult test() {\n\t\tdemoService.isLogin(\"----------- 登录前 \");\n\t\t\n\t\tStpUtil.login(10001);\n\t\t\n\t\tdemoService.isLogin(\"----------- 登录后 \");\n\t\t\n        return SaResult.ok();\n    }\n\n\t// Provider端登录，状态回传到Consumer端     --- http://localhost:8081/test2\n    @RequestMapping(\"test2\")\n    public SaResult test2() {\n    \tSystem.out.println(\"----------- 登录前 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin()); \n    \t\n    \tdemoService.doLogin(10002);\n\n    \tSystem.out.println(\"----------- 登录后 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\n\t\treturn SaResult.ok();\n    }\n\n\t// Consumer端登录，状态在Consumer端保持     --- http://localhost:8081/test3\n    @RequestMapping(\"test3\")\n    public SaResult test3() {\n    \tSystem.out.println(\"----------- 登录前 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue());\n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\n\t\tStpUtil.login(10003);\n\t\tdemoService.isLogin(\"----------- Provider状态\");\n\n    \tSystem.out.println(\"----------- 登录后 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue());\n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\n\t\treturn SaResult.ok();\n    }\n\n\t// Provider端登录，状态在Provider端保持     --- http://localhost:8081/test4\n    @RequestMapping(\"test4\")\n    public SaResult test4() {\n    \t// 登录 \n    \tdemoService.doLogin(10004);\n\t\t\n    \t// 打印一下 \n\t\tdemoService.isLogin(\"----------- 会话信息 \");\n\n\t\treturn SaResult.ok();\n    }\n    \n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/java/com/pj/service/DemoService.java",
    "content": "package com.pj.service;\n\npublic interface DemoService {\n\t\n\t/**\n\t * 登录 \n\t * @param loginId 账号id \n\t */\n\tvoid doLogin(Object loginId);\n\t\n\t/**\n\t * 判断是否登录，打印状态 \n\t */\n\tvoid isLogin(String str); \n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-consumer/src/main/resources/application.yml",
    "content": "server:\n    # 端口号 \n    port: 8081\n\nspring: \n    # redis配置 \n    redis: \n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        \ndubbo: \n    application: \n        # 服务名称 \n        name: dubbo-consumer-demo\n    registry: \n        # 注册中心地址 \n        address: nacos://127.0.0.1:8001\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>com.pj</groupId>\n\t<artifactId>sa-token-demo-dubbo-provider</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<!--<version>2.3.1.RELEASE</version>-->\n\t\t<version>2.5.15</version>\n\t</parent>\n\n\t<!-- 指定一些属性 -->\n\t<properties> \n\t\t<java.version>1.8</java.version>\n\t\t<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<dubbo.version>2.7.21</dubbo.version>\n\t\t<nacos.version>1.4.2</nacos.version>\n\t</properties>\n\t\n\t<dependencies>\n\n\t\t<!-- SpringBoot Web模块 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-spring-boot-starter</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Dubbo 注册到 Nacos -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-registry-nacos</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.alibaba.nacos</groupId>\n\t\t\t<artifactId>nacos-client</artifactId>\n\t\t\t<version>${nacos.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 整合 Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-dubbo</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/DubboProviderApplication.java",
    "content": "package com.pj;\n\nimport org.apache.dubbo.config.spring.context.annotation.EnableDubbo;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Dubbo 服务提供端 \n * \n * @author click33\n *\n */\n@EnableDubbo\n@SpringBootApplication\npublic class DubboProviderApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(DubboProviderApplication.class, args);\n\t\tSystem.out.println(\"DubboProviderApplication 启动成功\");\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/controller/TestController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.service.DemoService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class TestController {\n\t\n\t@Autowired\n    private DemoService demoService;\n\n\t// test\n    @RequestMapping(\"test\")\n    public SaResult test() {\n\t\tdemoService.isLogin(\"----------- 登录前 \" + StpUtil.isLogin());\n\t\t\n\t\tStpUtil.login(10001);\n\t\t\n\t\tdemoService.isLogin(\"----------- 登录后 \" + StpUtil.isLogin());\n\t\t\n        return SaResult.ok();\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoService.java",
    "content": "package com.pj.service;\n\npublic interface DemoService {\n\t\n\t/**\n\t * 登录 \n\t * @param loginId 账号id \n\t */\n\tvoid doLogin(Object loginId);\n\t\n\t/**\n\t * 判断是否登录，打印状态 \n\t */\n\tvoid isLogin(String str); \n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/java/com/pj/service/DemoServiceImpl.java",
    "content": "package com.pj.service;\n\nimport org.apache.dubbo.config.annotation.DubboService;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n@DubboService()\npublic class DemoServiceImpl implements DemoService {\n\n\t@Override\n\tpublic void doLogin(Object loginId) {\n\t\tStpUtil.login(loginId);\n\t}\n\n\t@Override\n\tpublic void isLogin(String str) {\n\t\tSystem.out.println(str);\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin()); \n\t}\n\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo-provider/src/main/resources/application.yml",
    "content": "server:\n    # 端口号 \n    port: 8080\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        \n# Dubbo\ndubbo: \n    # 服务名 \n    application: \n        name: dubbo-provider-demo\n    # 扫描包 \n    scan: \n        base-packages: com.pj\n    # 注册中心地址 \n    registry: \n        address: nacos://127.0.0.1:8001\n    # 协议\n    protocol: \n        name: dubbo\n        port: 12345\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>com.pj</groupId>\n\t<artifactId>sa-token-demo-dubbo3-consumer</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<!--<version>2.3.1.RELEASE</version>-->\n<!--\t\t<version>2.5.15</version>-->\n\t\t<version>3.4.3</version>\n\t</parent>\n\n\t<!-- 指定一些属性 -->\n\t<properties> \n\t\t<java.version>17</java.version>\n\t\t<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<dubbo.version>3.2.2</dubbo.version>\n\t\t<nacos.version>2.2.2</nacos.version>\n\t</properties>\n\t\n\t<dependencies>\n\n\t\t<!-- SpringBoot Web模块 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot3-starter</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-spring-boot-starter</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Dubbo 注册到 Nacos -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-registry-nacos</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.alibaba.nacos</groupId>\n\t\t\t<artifactId>nacos-client</artifactId>\n\t\t\t<version>${nacos.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 整合 Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-dubbo3</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/Dubbo3ConsumerApplication.java",
    "content": "package com.pj;\n\nimport org.apache.dubbo.config.spring.context.annotation.EnableDubbo;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Dubbo3 服务消费端\n * \n * @author click33\n *\n */\n@EnableDubbo\n@SpringBootApplication\npublic class Dubbo3ConsumerApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(Dubbo3ConsumerApplication.class, args);\n\t\tSystem.out.println(\"Dubbo3ConsumerApplication 启动成功\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/controller/TestController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.service.DemoService;\nimport org.apache.dubbo.config.annotation.DubboReference;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class TestController {\n\t\n\t@DubboReference\n    private DemoService demoService;\n\n\t// Consumer端登录，状态传播到Provider端 \n    @RequestMapping(\"test\")\n    public SaResult test() {\n\t\tdemoService.isLogin(\"----------- 登录前 \");\n\t\t\n\t\tStpUtil.login(10001);\n\t\t\n\t\tdemoService.isLogin(\"----------- 登录后 \");\n\t\t\n        return SaResult.ok();\n    }\n\n\t// Provider端登录，状态回传到Consumer端 \n    @RequestMapping(\"test2\")\n    public SaResult test2() {\n    \tSystem.out.println(\"----------- 登录前 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin()); \n    \t\n    \tdemoService.doLogin(10002);\n\n    \tSystem.out.println(\"----------- 登录后 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\n\t\treturn SaResult.ok();\n    }\n\n\t// Consumer端登录，状态在Consumer端保持 \n    @RequestMapping(\"test3\")\n    public SaResult test3() {\n    \tSystem.out.println(\"----------- 登录前 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin()); \n\n\t\tStpUtil.login(10003);\n\t\tdemoService.isLogin(\"----------- Provider状态\");\n    \t\n    \tSystem.out.println(\"----------- 登录后 \");\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\n\t\treturn SaResult.ok();\n    }\n\n\t// Provider端登录，状态在Provider端保持 \n    @RequestMapping(\"test4\")\n    public SaResult test4() {\n    \t// 登录 \n    \tdemoService.doLogin(10004);\n\t\t\n    \t// 打印一下 \n\t\tdemoService.isLogin(\"----------- 会话信息 \");\n\n\t\treturn SaResult.ok();\n    }\n    \n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/java/com/pj/service/DemoService.java",
    "content": "package com.pj.service;\n\npublic interface DemoService {\n\t\n\t/**\n\t * 登录 \n\t * @param loginId 账号id \n\t */\n\tvoid doLogin(Object loginId);\n\t\n\t/**\n\t * 判断是否登录，打印状态 \n\t */\n\tvoid isLogin(String str); \n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-consumer/src/main/resources/application.yml",
    "content": "server:\n    # 端口号 \n    port: 8081\n\nspring:\n    data:\n        # redis配置\n        redis:\n            # Redis数据库索引（默认为0）\n            database: 0\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # 连接超时时间\n        \ndubbo: \n    application: \n        # 服务名称 \n        name: dubbo-consumer-demo\n    registry: \n        # 注册中心地址 \n        address: nacos://127.0.0.1:8001\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>com.pj</groupId>\n\t<artifactId>sa-token-demo-dubbo3-provider</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<!--<version>2.3.1.RELEASE</version>-->\n<!--\t\t<version>2.5.15</version>-->\n\t\t<version>3.4.3</version>\n\t</parent>\n\n\t<!-- 指定一些属性 -->\n\t<properties> \n\t\t<java.version>17</java.version>\n\t\t<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<dubbo.version>3.2.2</dubbo.version>\n\t\t<nacos.version>2.2.2</nacos.version>\n\t</properties>\n\t\n\t<dependencies>\n\n\t\t<!-- SpringBoot Web模块 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot3-starter</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-spring-boot-starter</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Dubbo 注册到 Nacos -->\n\t\t<dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-registry-nacos</artifactId>\n\t\t\t<version>${dubbo.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.alibaba.nacos</groupId>\n\t\t\t<artifactId>nacos-client</artifactId>\n\t\t\t<version>${nacos.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 整合 Dubbo -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-dubbo3</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/Dubbo3ProviderApplication.java",
    "content": "package com.pj;\n\nimport org.apache.dubbo.config.spring.context.annotation.EnableDubbo;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Dubbo3 服务提供端\n * \n * @author click33\n *\n */\n@EnableDubbo\n@SpringBootApplication\npublic class Dubbo3ProviderApplication {\n\n\tpublic static void main(String[] args) throws Exception {\n\t\tSpringApplication.run(Dubbo3ProviderApplication.class, args);\n\t\tSystem.out.println(\"Dubbo3ProviderApplication 启动成功\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/controller/TestController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.service.DemoService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class TestController {\n\n\n//    如果把 @Autowired 改为 @DubboReference\n//    则可能在首次调用 dubbo 服务时控制台出现以下异常（只打印异常信息，不影响调用）：\n//          java.lang.reflect.InaccessibleObjectException: Unable to make field private byte java.lang.StackTraceElement.format accessible:\n//          module java.base does not \"opens java.lang\" to unnamed module @3a52dba3\n//\n//    在启动参数上加上如下即可解决：\n//          --add-opens java.base/java.math=ALL-UNNAMED\n\n    @Autowired\n    public DemoService demoService;\n\n\t// test\n    @RequestMapping(\"test\")\n    public SaResult test() {\n\t\tdemoService.isLogin(\"----------- 登录前 \" + StpUtil.isLogin());\n\t\t\n\t\tStpUtil.login(10001);\n\t\t\n\t\tdemoService.isLogin(\"----------- 登录后 \" + StpUtil.isLogin());\n\t\t\n        return SaResult.ok();\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoService.java",
    "content": "package com.pj.service;\n\npublic interface DemoService {\n\t\n\t/**\n\t * 登录 \n\t * @param loginId 账号id \n\t */\n\tvoid doLogin(Object loginId);\n\t\n\t/**\n\t * 判断是否登录，打印状态 \n\t */\n\tvoid isLogin(String str); \n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/java/com/pj/service/DemoServiceImpl.java",
    "content": "package com.pj.service;\n\nimport org.apache.dubbo.config.annotation.DubboService;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n@DubboService()\npublic class DemoServiceImpl implements DemoService {\n\n\t@Override\n\tpublic void doLogin(Object loginId) {\n\t\tStpUtil.login(loginId);\n\t}\n\n\t@Override\n\tpublic void isLogin(String str) {\n\t\tSystem.out.println(str);\n\t\tSystem.out.println(\"Token值：\" + StpUtil.getTokenValue()); \n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin()); \n\t}\n\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-dubbo/sa-token-demo-dubbo3-provider/src/main/resources/application.yml",
    "content": "server:\n    # 端口号 \n    port: 8080\n    \nspring:\n    data:\n        # redis配置\n        redis:\n            # Redis数据库索引（默认为0）\n            database: 0\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # 连接超时时间\n            timeout: 10s\n        \n# Dubbo\ndubbo: \n    # 服务名 \n    application: \n        name: dubbo-provider-demo\n    # 扫描包 \n    scan: \n        base-packages: com.pj\n    # 注册中心地址 \n    registry: \n        address: nacos://127.0.0.1:8001\n    # 协议\n    protocol: \n        name: dubbo\n        port: 12345\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-freemarker</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Freemarker 视图引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-freemarker</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n        <!-- 在 Freemarker 页面中使用 Sa-Token 自定义标签 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-freemarker</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- 热刷新 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/SaTokenFreemarkerDemoApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * 参考链接：https://blog.csdn.net/m0_64210833/article/details/135994864\n */\n@SpringBootApplication\npublic class SaTokenFreemarkerDemoApplication {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenFreemarkerDemoApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功，Sa-Token 配置如下：\" + SaManager.getConfig());\n\t\tSystem.out.println(\"\\n测试访问：http://localhost:8081/\");\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.freemarker.dialect.SaTokenTemplateModel;\nimport cn.dev33.satoken.stp.StpUtil;\nimport freemarker.template.TemplateModelException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;\n\nimport javax.annotation.PostConstruct;\n\n\n/**\n * [Sa-Token 权限认证] 配置类\n *\n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\n\t@Autowired\n\tFreeMarkerConfigurer configurer;\n\n\t/**\n\t * 注入 Sa-Token Freemarker 标签模板模型 对象\n\t */\n\t@PostConstruct\n\tpublic void setSaTokenTemplateModel() throws TemplateModelException {\n\n\t\t// 注入 Sa-Token Freemarker 标签模板模型，使之可以在 xxx.ftl 文件中使用 sa 标签，\n\t\t// 例如：<#if sa.login()>...</#if>\n\t\tconfigurer.getConfiguration().setSharedVariable(\"sa\", new SaTokenTemplateModel());\n\n\t\t// 注入 Sa-Token Freemarker 全局对象，使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法，\n\t\t// 例如：<span>${stp.getSession().get('name')}</span>\n\t\tconfigurer.getConfiguration().setSharedVariable(\"stp\", StpUtil.stpLogic);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport org.springframework.stereotype.Component;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.ModelAndView;\n\n/**\n * 测试 Controller\n *\n * @author click33\n */\n@RestController\npublic class TestController {\n\n\t// 首页 \n\t@RequestMapping(\"/\")\n\tpublic Object index() {\n\t\treturn new ModelAndView(\"index\");\n\t}\n\t\n\t// 登录 \n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tStpUtil.login(id);\n\t\tStpUtil.getSession().set(\"name\", \"zhangsan\");\n\t\treturn SaResult.ok();\n\t}\n\n\t// 注销 \n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\nspring:\n    # Freemarker 相关配置\n    freemarker:\n        # 指定模板文件的目录\n        template-loader-path: classpath:/templates\n        # 指定Freemarker模板文件的后缀名\n        suffix: .ftl\n        # 关闭模板缓存，方便测试\n        cache: false\n        # 检查模板更新延迟时间，设置为0表示立即检查，如果时间大于0会有缓存不方便进行模板测试\n        settings:\n            template_update_delay: 0"
  },
  {
    "path": "sa-token-demo/sa-token-demo-freemarker/src/main/resources/templates/index.ftl",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<title>Sa-Token 集成 Freemarker 标签方言</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\" style=\"padding: 30px;\">\n\t\t\t<h2>Sa-Token 集成 Freemarker 标签方言 —— 测试页面</h2>\n\t\t\t<p>当前是否登录：<#if stp.isLogin()>是<#else>否</#if></p>\n\t\t\t<p>\n\t\t\t\t<a href=\"login\" target=\"_blank\">登录</a>\n\t\t\t\t<a href=\"logout\" target=\"_blank\">注销</a>\n\t\t\t</p>\n\n\t\t\t<p>登录之后才能显示：<@sa.login>value</@sa.login></p>\n\t\t\t<p>不登录才能显示：<@sa.notLogin>value</@sa.notLogin></p>\n\n\t\t\t<p>具有角色 admin 才能显示：<@sa.hasRole value=\"admin\">value</@sa.hasRole></p>\n\t\t\t<p>同时具备多个角色才能显示：<@sa.hasRoleAnd value=\"admin, ceo, cto\">value</@sa.hasRoleAnd></p>\n\t\t\t<p>只要具有其中一个角色就能显示：<@sa.hasRoleOr value=\"admin, ceo, cto\">value</@sa.hasRoleOr></p>\n\t\t\t<p>不具有角色 admin 才能显示：<@sa.notRole value=\"admin\">value</@sa.notRole></p>\n\n\t\t\t<p>具有权限 user-add 才能显示：<@sa.hasPermission value=\"user-add\">value</@sa.hasPermission></p>\n\t\t\t<p>同时具备多个权限才能显示：<@sa.hasPermissionAnd value=\"user-add, user-delete, user-get\">value</@sa.hasPermissionAnd></p>\n\t\t\t<p>只要具有其中一个权限就能显示：<@sa.hasPermissionOr value=\"user-add, user-delete, user-get\">value</@sa.hasPermissionOr></p>\n\t\t\t<p>不具有权限 user-add 才能显示：<@sa.notPermission value=\"user-add\">value</@sa.notPermission></p>\n\n\t\t\t<p>\n\t\t\t\t从SaSession中取值：\n\t\t\t\t<#if stp.isLogin()>\n\t\t\t\t\t<span>${stp.getSession().get('name')}</span>\n\t\t\t\t</#if>\n\t\t\t</p>\n\n\t\t</div>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/client/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-demo-grpc</artifactId>\n        <groupId>com.lym</groupId>\n        <version>0.0.1-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>client</artifactId>\n\n    <properties>\n        <maven.compiler.source>8</maven.compiler.source>\n        <maven.compiler.target>8</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/Client.java",
    "content": "package com.lym;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.client.discovery.EnableDiscoveryClient;\n\n/**\n * @author lym\n * @description\n * @date 2022/8/26 10:58\n **/\n@SpringBootApplication\n@EnableDiscoveryClient\npublic class Client {\n    public static void main(String[] args) {\n        SpringApplication.run(Client.class);\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/controller/TestController.java",
    "content": "package com.lym.controller;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport com.lym.grpc.client.GrpcAuthService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * @author lym\n * @description\n * @date 2022/8/26 11:01\n **/\n@RestController\npublic class TestController {\n    @Autowired\n    private GrpcAuthService grpcAuthService;\n\n    // 客户端登录，状态带到服务端。\n    @RequestMapping(\"test\")\n    public void test() {\n        System.out.println(\"登录前：\" + grpcAuthService.isLogin());\n        System.out.println(\"登录前：\" + StpUtil.isLogin());\n\n        StpUtil.login(1);\n\n        System.out.println(\"登录后：\" + grpcAuthService.isLogin());\n        System.out.println(\"登录后：\" + StpUtil.getTokenValue());\n        System.out.println(\"登录后：\" + StpUtil.getLoginId());\n    }\n\n    // 服务端登录，登录状态带回客户端\n    @RequestMapping(\"test2\")\n    public String test2() {\n        System.out.println(\"登录前：\" + grpcAuthService.isLogin());\n        System.out.println(\"登录前：\" + StpUtil.isLogin());\n\n        String token = grpcAuthService.login(3);\n\n        System.out.println(\"登录后：\" + grpcAuthService.isLogin());\n        System.out.println(\"登录后：\" + StpUtil.getTokenValue());\n        System.out.println(\"登录后：\" + StpUtil.getLoginId());\n        assert StpUtil.getTokenValue().equals(token);\n\n        return token;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/client/src/main/java/com/lym/grpc/client/GrpcAuthService.java",
    "content": "package com.lym.grpc.client;\n\nimport com.google.protobuf.Empty;\nimport com.lym.grpc.auth.Auth;\nimport com.lym.grpc.auth.AuthServiceGrpc;\nimport net.devh.boot.grpc.client.inject.GrpcClient;\nimport org.springframework.stereotype.Service;\n\n/**\n * @author lym\n * @description\n * @date 2022/8/26 11:02\n **/\n@Service\npublic class GrpcAuthService {\n    @GrpcClient(\"test-server\")\n    private AuthServiceGrpc.AuthServiceBlockingStub grpcAuthService;\n\n    public boolean isLogin() {\n        Auth.GrpcBool resp = grpcAuthService.isLogin(Empty.getDefaultInstance());\n        return resp.getVal();\n    }\n\n    public String login(Integer id) {\n        Auth.GrpcString resp = grpcAuthService.login(Auth.GrpcInt.newBuilder().setVal(id).build());\n        return resp.getVal();\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/client/src/main/proto/auth.proto",
    "content": "syntax = \"proto3\";\n\npackage auth;\n\noption java_package = \"com.lym.grpc.auth\";\nimport \"google/protobuf/empty.proto\";\n\nmessage GrpcBool{\n  bool val = 1;\n}\n\nmessage GrpcInt{\n  int32 val = 1;\n}\n\nmessage GrpcString{\n  string val = 1;\n}\n\n\nservice AuthService{\n  rpc isLogin(google.protobuf.Empty) returns (GrpcBool);\n  rpc login(GrpcInt) returns (GrpcString);\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/client/src/main/resources/application.yml",
    "content": "server:\n  port: 2222\nspring:\n  application:\n    name: test-client\n  redis:\n    host: localhost\n  cloud:\n    nacos:\n      server-addr: localhost:8848\nsa-token:\n  is-read-cookie: false\ngrpc:\n  server:\n    port: 2223\n  client:\n    test-server:\n      negotiation-type: PLAINTEXT"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <packaging>pom</packaging>\n    <modules>\n        <module>client</module>\n        <module>server</module>\n    </modules>\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.6.3</version>\n        <relativePath/>\n    </parent>\n    <groupId>com.lym</groupId>\n    <artifactId>sa-token-demo-grpc</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <name>sa-token-demo-grpc</name>\n    <description>sa-token-demo-grpc</description>\n\n    <properties>\n        <maven.compiler.source>8</maven.compiler.source>\n        <maven.compiler.target>8</maven.compiler.target>\n        <java.version>1.8</java.version>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <lombok.version>1.18.10</lombok.version>\n        <sa-token.version>1.45.0</sa-token.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.alibaba.cloud</groupId>\n            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n            <!-- <version>2.7.2</version> -->\n        </dependency>\n\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-grpc</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n    </dependencies>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>com.alibaba.cloud</groupId>\n                <artifactId>spring-cloud-alibaba-dependencies</artifactId>\n                <version>2021.0.1.0</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.springframework.cloud</groupId>\n                <artifactId>spring-cloud-dependencies</artifactId>\n                <version>2021.0.1</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <extensions>\n            <extension>\n                <groupId>kr.motd.maven</groupId>\n                <artifactId>os-maven-plugin</artifactId>\n            </extension>\n        </extensions>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.xolstice.maven.plugins</groupId>\n                <artifactId>protobuf-maven-plugin</artifactId>\n                <configuration>\n                    <protocArtifact>com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier}</protocArtifact>\n                    <pluginId>grpc-java</pluginId>\n                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.12.0:exe:${os.detected.classifier}</pluginArtifact>\n                </configuration>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>compile</goal>\n                            <goal>compile-custom</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-demo-grpc</artifactId>\n        <groupId>com.lym</groupId>\n        <version>0.0.1-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>server</artifactId>\n\n    <properties>\n        <maven.compiler.source>8</maven.compiler.source>\n        <maven.compiler.target>8</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/Server.java",
    "content": "package com.lym;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.cloud.client.discovery.EnableDiscoveryClient;\n\n/**\n * @author lym\n * @description\n * @date 2022/8/26 10:58\n **/\n@SpringBootApplication\n@EnableDiscoveryClient\npublic class Server {\n    public static void main(String[] args) {\n        SpringApplication.run(Server.class);\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/grpc/server/GrpcAuthService.java",
    "content": "package com.lym.grpc.server;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport com.google.protobuf.Empty;\nimport com.lym.grpc.auth.Auth;\nimport com.lym.grpc.auth.AuthServiceGrpc;\nimport com.lym.service.AuthService;\nimport io.grpc.stub.StreamObserver;\nimport net.devh.boot.grpc.server.service.GrpcService;\nimport org.springframework.beans.factory.annotation.Autowired;\n\n/**\n * @author lym\n * @description\n * @date 2022/8/26 11:29\n **/\n@GrpcService\npublic class GrpcAuthService extends AuthServiceGrpc.AuthServiceImplBase {\n\n    @Autowired\n    private AuthService authService;\n\n    @Override\n    public void isLogin(Empty request, StreamObserver<Auth.GrpcBool> responseObserver) {\n        boolean isLogin = authService.isLogin();\n        responseObserver.onNext(Auth.GrpcBool.newBuilder().setVal(isLogin).build());\n        responseObserver.onCompleted();\n    }\n\n    @Override\n    public void login(Auth.GrpcInt request, StreamObserver<Auth.GrpcString> responseObserver) {\n        StpUtil.login(request.getVal());\n        Auth.GrpcString resp = Auth.GrpcString.newBuilder().setVal(StpUtil.getTokenValue()).build();\n        responseObserver.onNext(resp);\n        responseObserver.onCompleted();\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/server/src/main/java/com/lym/service/AuthService.java",
    "content": "package com.lym.service;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Service;\n\n/**\n * @author lym\n * @description\n * @date 2022/8/26 11:30\n **/\n@Service\npublic class AuthService {\n    public boolean isLogin() {\n        if (StpUtil.isLogin()) {\n            System.out.println(\"id:\" + StpUtil.getLoginIdAsInt());\n            System.out.println(\"token：\" + StpUtil.getTokenValue());\n        } else {\n            System.out.println(\"未登录\");\n        }\n\n        return StpUtil.isLogin();\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/server/src/main/proto/auth.proto",
    "content": "syntax = \"proto3\";\n\npackage auth;\n\noption java_package = \"com.lym.grpc.auth\";\nimport \"google/protobuf/empty.proto\";\n\nmessage GrpcBool{\n  bool val = 1;\n}\n\nmessage GrpcInt{\n  int32 val = 1;\n}\n\nmessage GrpcString{\n  string val = 1;\n}\n\n\nservice AuthService{\n  rpc isLogin(google.protobuf.Empty) returns (GrpcBool);\n  rpc login(GrpcInt) returns (GrpcString);\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-grpc/server/src/main/resources/application.yml",
    "content": "server:\n  port: 5555\nspring:\n  application:\n    name: test-server\n  redis:\n    host: localhost\n  cloud:\n    nacos:\n      server-addr: localhost:8848\nsa-token:\n  is-read-cookie: false\n\ngrpc:\n  server:\n    port: 5556\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-hutool-timed-cache</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 Hutool-TimedCache -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-hutool-timed-cache</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/SaTokenDemoApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token 整合 Hutool-TimedCache 示例\n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t\tSystem.out.println(SaManager.getSaTokenDao());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)\n\t\t\tthrows Exception {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace();\n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} \n\t\telse if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} \n\t\telse if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} \n\t\telse if(e instanceof DisableServiceException) {\t// 如果是被封禁异常\n\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\taj = AjaxJson.getNotJur(\"当前账号 \" + ee.getService() + \" 服务已被封禁 (level=\" + ee.getLevel() + \")：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t} \n\t\telse {\t// 普通异常, 输出：500 + 异常信息 \n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能\n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n        \t\t\t\n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行 （BeforeAuth不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.util.Ttime;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 压力测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/s-test/\")\npublic class StressTestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@RequestMapping(\"login\")\n\tpublic SaResult login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次\n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-hutool-timed-cache/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-jwt/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-jwt</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n        <!-- Sa-Token 整合 jwt -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jwt</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->\n\t\t <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t\t<!-- test -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-test</artifactId>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/SaTokenJwtDemoApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaTokenJwtDemoApplication {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenJwtDemoApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.jwt.StpLogicJwtForSimple;\nimport cn.dev33.satoken.stp.StpLogic;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n    /**\n     * Sa-Token 整合 jwt \n     */\n\t@Bean\n    public StpLogic getStpLogicJwt() {\n\t\treturn new StpLogicJwtForSimple();\n//\t\treturn new StpLogicJwtForMixin();\n//\t\treturn new StpLogicJwtForStateless();\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice // 可指定包前缀，比如：(basePackages = \"com.pj.admin\")\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)\n\t\t\tthrows Exception {\n\n\t\t// 打印堆栈，以供调试\n\t\te.printStackTrace(); \n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} else if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} else if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} else {\t// 普通异常, 输出：500 + 异常信息\n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/test/TestJwtController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.pj.util.AjaxJson;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.Date;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestJwtController {\n\n\t// 测试登录接口， 浏览器访问： http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic AjaxJson login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tSystem.out.println(\"======================= 进入方法，测试登录接口 ========================= \");\n\t\tSystem.out.println(\"当前会话的token：\" + StpUtil.getTokenValue());\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginIdDefaultNull());\n\n\t\tStpUtil.login(id, new SaLoginParameter().setExtra(\"name\", \"张三\"));\t\t\t// 在当前会话登录此账号\n\t\tSystem.out.println(\"登录成功\");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginId());\n//\t\tSystem.out.println(\"当前登录账号并转为int：\" + StpUtil.getLoginIdAsInt());\n\t\tSystem.out.println(\"当前登录设备：\" + StpUtil.getLoginDeviceType());\n//\t\tSystem.out.println(\"当前token信息：\" + StpUtil.getTokenInfo());\t\n\t\t\n\t\treturn AjaxJson.getSuccess().setData(StpUtil.getTokenValue());\n\t}\n\t\n\t// 打印当前token信息， 浏览器访问： http://localhost:8081/test/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic AjaxJson tokenInfo() {\n\t\tSystem.out.println(\"======================= 进入方法，打印当前token信息 ========================= \");\n\t\tSaTokenInfo tokenInfo = StpUtil.getTokenInfo();\n\t\tSystem.out.println(tokenInfo);\n\t\treturn AjaxJson.getSuccessData(tokenInfo);\n\t}\n\t\t\n\n\t// 测试会话session接口， 浏览器访问： http://localhost:8081/test/session \n\t@RequestMapping(\"session\")\n\tpublic AjaxJson session() throws JsonProcessingException {\n\t\tSystem.out.println(\"======================= 进入方法，测试会话session接口 ========================= \");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tStpUtil.getSession().set(\"name\", new Date());\t// 写入一个值 \n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tSystem.out.println( new ObjectMapper().writeValueAsString(StpUtil.getSession()));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\t@SaCheckLogin\n\tpublic AjaxJson test() {\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"--------------进入请求--------------\");\n\t\tSystem.out.println(StpUtil.getExtra(\"username\"));\n\t\tSystem.out.println(StpUtil.getExtra(\"nick\"));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-jwt/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-jwt/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n    # jwt秘钥 \n    jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间（毫秒）\n        timeout: 10000ms\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.kfyty</groupId>\n        <artifactId>loveqq-framework</artifactId>\n        <version>1.1.2</version>\n        <relativePath/>\n    </parent>\n\n    <artifactId>sa-token-demo-loveqq-boot</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n\n    <properties>\n        <jdk.version>17</jdk.version>\n        <java.version>17</java.version>\n        <maven.source.version>17</maven.source.version>\n        <maven.compile.version>17</maven.compile.version>\n        <sa-token.version>1.45.0</sa-token.version>\n    </properties>\n\n    <dependencies>\n        <!-- 引导启动模块 -->\n        <dependency>\n            <groupId>com.kfyty</groupId>\n            <artifactId>loveqq-boot</artifactId>\n            <version>${loveqq.framework.version}</version>\n        </dependency>\n\n        <!-- reactor-netty 服务器，同时支持命令式/响应式编程范式 -->\n        <dependency>\n            <groupId>com.kfyty</groupId>\n            <artifactId>loveqq-boot-starter-netty</artifactId>\n            <version>${loveqq.framework.version}</version>\n        </dependency>\n\n        <!-- sa-token 集成 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-loveqq-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n        <!-- logback 启动器 -->\n        <dependency>\n            <groupId>com.kfyty</groupId>\n            <artifactId>loveqq-boot-starter-logback</artifactId>\n            <version>${loveqq.framework.version}</version>\n        </dependency>\n\n        <!-- yaml 支持，默认使用 properties 文件，如果使用 yaml 需自行引入依赖 -->\n        <dependency>\n            <groupId>org.yaml</groupId>\n            <artifactId>snakeyaml</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.10.1</version>\n                <configuration>\n                    <source>17</source>\n                    <target>17</target>\n                    <encoding>UTF-8</encoding>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/SaTokenLoveqqApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport com.kfyty.loveqq.framework.boot.K;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.BootApplication;\nimport com.kfyty.loveqq.framework.web.core.autoconfig.annotation.EnableWebMvc;\n\n/**\n * Sa-Token 整合 loveqq-framework 示例\n *\n * @author kfyty725\n */\n@EnableWebMvc\n@BootApplication\npublic class SaTokenLoveqqApplication {\n\n\tpublic static void main(String[] args) {\n\t\tK.run(SaTokenLoveqqApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/MyFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.pj.satoken;\n\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.web.core.filter.Filter;\nimport com.kfyty.loveqq.framework.web.core.filter.FilterChain;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\n/**\n * 自定义过滤器\n */\n@Component\npublic class MyFilter implements Filter {\n    /**\n     * 实现该方法，可以实现 servlet/reactor 的统一\n     * 但是该方法内部是同步方法，若需要异步，可以实现仅 reactor 支持的 {@link Filter#doFilter(ServerRequest, ServerResponse, FilterChain)} 方法\n     *\n     * @param request  请求\n     * @param response 响应\n     */\n    @Override\n    public Continue doFilter(ServerRequest request, ServerResponse response) {\n        System.out.println(\"进入自定义过滤器\");\n\n        // 先 set 上下文，再调用 Sa-Token 同步 API，并在 finally 里清除上下文\n        SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response);\n        try {\n            System.out.println(StpUtil.isLogin());\n        } finally {\n            SaTokenContextUtil.clearContext(prev);\n        }\n\n        return Continue.TRUE;\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.loveqq.boot.filter.SaRequestFilter;\nimport cn.dev33.satoken.util.SaResult;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure {\n\n\t/**\n     * 注册 [sa-token全局过滤器] \n     */\n    @Bean\n    public SaRequestFilter getSaReactorFilter() {\n        return new SaRequestFilter()\n        \t\t// 指定 [拦截路由]\n        \t\t.addInclude(\"/**\")\n        \t\t// 指定 [放行路由]\n        \t\t.addExclude(\"/favicon.ico\")\n        \t\t// 指定[认证函数]: 每次请求执行 \n        \t\t.setAuth(r -> {\n        \t\t\tSystem.out.println(\"---------- sa全局认证\");\n                    // SaRouter.match(\"/test/test\", () -> StpUtil.checkLogin());\n        \t\t})\n        \t\t// 指定[异常处理函数]：每次[认证函数]发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t;\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component    // 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport com.kfyty.loveqq.framework.web.core.annotation.ExceptionHandler;\nimport com.kfyty.loveqq.framework.web.core.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.loveqq.boot.context.SaReactorHolder;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired;\nimport com.kfyty.loveqq.framework.web.core.annotation.GetMapping;\nimport com.kfyty.loveqq.framework.web.core.annotation.RequestMapping;\nimport com.kfyty.loveqq.framework.web.core.annotation.RestController;\nimport com.kfyty.loveqq.framework.web.core.annotation.bind.CookieValue;\nimport com.kfyty.loveqq.framework.web.core.annotation.bind.RequestParam;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\nimport reactor.core.publisher.Mono;\n\nimport java.time.Duration;\n\n/**\n * 测试专用 Controller\n * 本示例是基于 reactor 编写，如果是 servlet，去除 SaReactorHolder/SaTokenContextUtil 包装，直接调用 sa-token api 即可\n *\n * @author click33\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n    @Autowired\n    UserService userService;\n\n    // 登录测试：Controller 里调用 Sa-Token API   --- http://localhost:8081/test/login\n    @GetMapping(\"login\")\n    public Mono<SaResult> login(@RequestParam(defaultValue = \"10001\") String id) {\n        return SaReactorHolder.sync(() -> {\n            StpUtil.login(id);\n            return SaResult.ok(\"登录成功\");\n        });\n    }\n\n    // API测试：手动设置上下文、try-finally 形式     \t--- http://localhost:8081/test/isLogin\n    @GetMapping(\"isLogin\")\n    public SaResult isLogin(ServerRequest request, ServerResponse response) {\n        try {\n            SaTokenContextUtil.setContext(request, response);\n            System.out.println(\"是否登录：\" + StpUtil.isLogin());\n            return SaResult.data(StpUtil.getTokenInfo());\n        } finally {\n            SaTokenContextUtil.clearContext(null);\n        }\n    }\n\n    // API测试：手动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin2\n    @GetMapping(\"isLogin2\")\n    public SaResult isLogin2(ServerRequest request, ServerResponse response) {\n        SaResult res = SaTokenContextUtil.setContext(request, response, () -> {\n            System.out.println(\"是否登录：\" + StpUtil.isLogin());\n            return SaResult.data(StpUtil.getTokenInfo());\n        });\n        return SaResult.data(res);\n    }\n\n    // API测试：自动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin3\n    @GetMapping(\"isLogin3\")\n    public Mono<SaResult> isLogin3() {\n        return SaReactorHolder.sync(() -> {\n            System.out.println(\"是否登录：\" + StpUtil.isLogin());\n            userService.isLogin();\n            return SaResult.data(StpUtil.getTokenInfo());\n        });\n    }\n\n    // API测试：自动设置上下文、调用 userService Mono 方法     \t--- http://localhost:8081/test/isLogin4\n    @GetMapping(\"isLogin4\")\n    public Mono<SaResult> isLogin4() {\n        return userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\")\n                .flatMap(userId -> SaReactorHolder.sync(() -> {\n                    StpUtil.login(userId);\n                    return SaResult.data(StpUtil.getTokenInfo());\n                }));\n    }\n\n    // API测试：切换线程、复杂嵌套调用 \t--- http://localhost:8081/test/isLogin5\n    @GetMapping(\"isLogin5\")\n    public Mono<SaResult> isLogin5() {\n        System.out.println(\"线程id-----\" + Thread.currentThread().getId());\n        // 要点：在流里调用 Sa-Token API 之前，必须用 SaReactorHolder.sync( () -> {} ) 进行包裹\n        return Mono.delay(Duration.ofSeconds(1))\n                .doOnNext(r -> System.out.println(\"线程id-----\" + Thread.currentThread().getId()))\n                .map(r -> SaReactorHolder.sync(() -> userService.isLogin()))\n                .map(r -> userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\"))\n                .map(r -> SaReactorHolder.sync(() -> userService.isLogin()))\n                .flatMap(isLogin -> {\n                    System.out.println(\"是否登录 \" + isLogin);\n                    return SaReactorHolder.sync(() -> {\n                        System.out.println(\"是否登录 \" + StpUtil.isLogin());\n                        return SaResult.data(StpUtil.getTokenInfo());\n                    });\n                });\n    }\n\n    // API测试：使用上下文无关的API \t--- http://localhost:8081/test/isLogin6\n    @GetMapping(\"isLogin6\")\n    public SaResult isLogin6(@CookieValue(\"satoken\") String satoken) {\n        System.out.println(\"token 为：\" + satoken);\n        System.out.println(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n        return SaResult.ok(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n    }\n\n    // 测试   浏览器访问： http://localhost:8081/test/test\n    @GetMapping(\"test\")\n    public SaResult test() {\n        System.out.println(\"线程id------- \" + Thread.currentThread().getId());\n        return SaResult.ok();\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/java/com/pj/test/UserService.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Service;\nimport reactor.core.publisher.Mono;\n\n/**\n * 模拟 Service 方法\n * @author click33\n * @since 2025/4/6\n */\n@Service\npublic class UserService {\n\n    public boolean isLogin() {\n        System.out.println(\"UserService 里调用 API 测试，是否登录：\" + StpUtil.isLogin());\n        return StpUtil.isLogin();\n    }\n\n    public Mono<Long> findUserIdByNamePwd(String name, String pwd) {\n        // ...\n        return Mono.just(10001L);\n    }\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-loveqq-boot/src/main/resources/application.yml",
    "content": "# 端口\nk:\n    server:\n        port: 8081\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>com.pj</groupId>\n\t<artifactId>sa-token-demo-oauth2-client</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t</parent>\n\n\t<!-- 指定一些属性 -->\n\t<properties> \n\t\t<java.version>1.8</java.version>\n\t\t<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>\n\t\t<!-- 定义 Sa-Token 版本号 -->\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\t\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t    <groupId>cn.dev33</groupId>\n\t\t    <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- thymeleaf 视图引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-thymeleaf</artifactId>\n\t\t</dependency>\n        \n        <!-- OkHttps网络请求库： http://okhttps.ejlchina.com/ -->\n        <dependency>\n\t\t\t<groupId>com.ejlchina</groupId>\n\t\t\t<artifactId>okhttps</artifactId>\n\t\t\t<version>3.1.1</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- 热刷新 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n\t\t\n\t\t<!-- ConfigurationProperties -->\n        <dependency>\n        \t<groupId>org.springframework.boot</groupId>\n        \t<artifactId>spring-boot-configuration-processor</artifactId>\n        \t<optional>true</optional>\n        </dependency>\n        \n\t</dependencies>\n\n\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/SaOAuth2ClientApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * 启动：Sa-OAuth2 ClientServer端 \n * @author click33 \n */\n@SpringBootApplication \npublic class SaOAuth2ClientApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaOAuth2ClientApplication.class, args);\n\t\tSystem.out.println(\"\\nSa-Token-OAuth Client端启动成功\\n\\n\" + str);\n\t}\n\n\tstatic String str = \"-------------------- Sa-Token-OAuth2 示例 --------------------\\n\\n\" + \n\t\t\t\"首先在host文件 (C:\\\\windows\\\\system32\\\\drivers\\\\etc\\\\hosts) 添加以下内容: \\r\\n\" + \n\t\t\t\"\t127.0.0.1 sa-oauth-server.com \\r\\n\" + \n\t\t\t\"\t127.0.0.1 sa-oauth-client.com \\r\\n\" + \n\t\t\t\"再从浏览器访问：\\r\\n\" + \n\t\t\t\"\thttp://sa-oauth-client.com:8002\";\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java",
    "content": "package com.pj.oauth2;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.ejlchina.okhttps.OkHttps;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.pj.utils.SoMap;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.ModelAndView;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * Sa-OAuth2 Client端 控制器 \n * @author click33 \n */\n@RestController\npublic class SaOAuthClientController {\n\n\t// 相关参数配置 \n\tprivate final String clientId = \"1001\";\t\t\t\t\t\t\t\t// 应用id\n\tprivate final String clientSecret = \"aaaa-bbbb-cccc-dddd-eeee\";\t\t// 应用秘钥\n\tprivate final String serverUrl = \"http://sa-oauth-server.com:8000\";\t// 服务端接口\n\n\t// 进入首页 \n\t@RequestMapping(\"/\")\n\tpublic Object index(HttpServletRequest request) {\n\t\trequest.setAttribute(\"uid\", StpUtil.getLoginIdDefaultNull());\n\t\treturn new ModelAndView(\"index.html\");\n\t}\n\t\n\t// 根据Code码进行登录，获取 Access-Token 和 openid  \n\t@RequestMapping(\"/codeLogin\")\n\tpublic SaResult codeLogin(String code) throws JsonProcessingException {\n\t\t// 调用Server端接口，获取 Access-Token 以及其他信息 \n\t\tString str = OkHttps.sync(serverUrl + \"/oauth2/token\")\n\t\t\t\t.addBodyPara(\"grant_type\", \"authorization_code\")\n\t\t\t\t.addBodyPara(\"code\", code)\n\t\t\t\t.addBodyPara(\"client_id\", clientId)\n\t\t\t\t.addBodyPara(\"client_secret\", clientSecret)\n\t\t\t\t.post()\n\t\t\t\t.getBody()\n\t\t\t\t.toString();\n\t\tSoMap so = SoMap.getSoMap().setJsonString(str);\n\t\tSystem.out.println(\"返回结果: \" + new ObjectMapper().writeValueAsString(so));\n\t\t\n\t\t// code不等于200  代表请求失败 \n\t\tif(so.getInt(\"code\") != 200) {\n\t\t\treturn SaResult.error(so.getString(\"msg\"));\n\t\t}\n\n\t\t// 根据openid获取其对应的userId\n\t\tlong uid = getUserIdByOpenid(so.getString(\"openid\"));\n\t\tso.set(\"uid\", uid);\n\t\t\n\t\t// 返回相关参数 \n\t\tStpUtil.login(uid);\n\t\treturn SaResult.data(so);\n\t}\n\t\n\t// 根据 Refresh-Token 去刷新 Access-Token \n\t@RequestMapping(\"/refresh\")\n\tpublic SaResult refresh(String refreshToken) throws JsonProcessingException {\n\t\t// 调用Server端接口，通过 Refresh-Token 刷新出一个新的 Access-Token \n\t\tString str = OkHttps.sync(serverUrl + \"/oauth2/refresh\")\n\t\t\t\t.addBodyPara(\"grant_type\", \"refresh_token\")\n\t\t\t\t.addBodyPara(\"client_id\", clientId)\n\t\t\t\t.addBodyPara(\"client_secret\", clientSecret)\n\t\t\t\t.addBodyPara(\"refresh_token\", refreshToken)\n\t\t\t\t.post()\n\t\t\t\t.getBody()\n\t\t\t\t.toString();\n\t\tSoMap so = SoMap.getSoMap().setJsonString(str);\n\t\tSystem.out.println(\"返回结果: \" + new ObjectMapper().writeValueAsString(so));\n\t\t\n\t\t// code不等于200  代表请求失败 \n\t\tif(so.getInt(\"code\") != 200) {\n\t\t\treturn SaResult.error(so.getString(\"msg\"));\n\t\t}\n\n\t\t// 返回相关参数\n\t\treturn SaResult.data(so);\n\t}\n\t\n\t// 模式三：密码式-授权登录\n\t@RequestMapping(\"/passwordLogin\")\n\tpublic SaResult passwordLogin(String username, String password) throws JsonProcessingException {\n\t\t// 模式三：密码式-授权登录\n\t\tString str = OkHttps.sync(serverUrl + \"/oauth2/token\")\n\t\t\t\t.addBodyPara(\"grant_type\", \"password\")\n\t\t\t\t.addBodyPara(\"client_id\", clientId)\n\t\t\t\t.addBodyPara(\"client_secret\", clientSecret)\n\t\t\t\t.addBodyPara(\"username\", username)\n\t\t\t\t.addBodyPara(\"password\", password)\n\t\t\t\t.post()\n\t\t\t\t.getBody()\n\t\t\t\t.toString();\n\t\tSoMap so = SoMap.getSoMap().setJsonString(str);\n\t\tSystem.out.println(\"返回结果: \" + new ObjectMapper().writeValueAsString(so));\n\t\t\n\t\t// code不等于200  代表请求失败 \n\t\tif(so.getInt(\"code\") != 200) {\n\t\t\treturn SaResult.error(so.getString(\"msg\"));\n\t\t}\n\n\t\t// 根据openid获取其对应的userId\n\t\tlong uid = getUserIdByOpenid(so.getString(\"openid\"));\n\t\tso.set(\"uid\", uid);\n\t\t\n\t\t// 返回相关参数 \n\t\tStpUtil.login(uid);\n\t\treturn SaResult.data(so);\n\t}\n\t\n\t// 模式四：获取应用的 Client-Token \n\t@RequestMapping(\"/clientToken\")\n\tpublic SaResult clientToken() throws JsonProcessingException {\n\t\t// 调用Server端接口\n\t\tString str = OkHttps.sync(serverUrl + \"/oauth2/client_token\")\n\t\t\t\t.addBodyPara(\"grant_type\", \"client_credentials\")\n\t\t\t\t.addBodyPara(\"client_id\", clientId)\n\t\t\t\t.addBodyPara(\"client_secret\", clientSecret)\n\t\t\t\t.post()\n\t\t\t\t.getBody()\n\t\t\t\t.toString();\n\t\tSoMap so = SoMap.getSoMap().setJsonString(str);\n\t\tSystem.out.println(\"返回结果: \" + new ObjectMapper().writeValueAsString(so));\n\t\t\n\t\t// code不等于200  代表请求失败 \n\t\tif(so.getInt(\"code\") != 200) {\n\t\t\treturn SaResult.error(so.getString(\"msg\"));\n\t\t}\n\n\t\t// 返回相关参数\n\t\treturn SaResult.data(so);\n\t}\n\t\n\t// 注销登录 \n\t@RequestMapping(\"/logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\n\t// 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息 \n\t@RequestMapping(\"/getUserinfo\")\n\tpublic SaResult getUserinfo(String accessToken) throws JsonProcessingException {\n\t\t// 调用Server端接口，查询开放的资源 \n\t\tString str = OkHttps.sync(serverUrl + \"/oauth2/userinfo\")\n\t\t\t\t.addBodyPara(\"access_token\", accessToken)\n\t\t\t\t.post()\n\t\t\t\t.getBody()\n\t\t\t\t.toString();\n\t\tSoMap so = SoMap.getSoMap().setJsonString(str);\n\t\tSystem.out.println(\"返回结果: \" + new ObjectMapper().writeValueAsString(so));\n\t\t\n\t\t// code不等于200  代表请求失败 \n\t\tif(so.getInt(\"code\") != 200) {\n\t\t\treturn SaResult.error(so.getString(\"msg\"));\n\t\t}\n\n\t\t// 返回相关参数 (data=获取到的资源 )\n\t\treturn SaResult.data(so);\n\t}\n\t\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\n\t\n\t// ------------ 模拟方法 ------------------ \n\t// 模拟方法：根据openid获取userId \n\tprivate long getUserIdByOpenid(String openid) {\n\t\t// 此方法仅做模拟，实际开发要根据具体业务逻辑来获取userId\n\t\treturn 10001;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/utils/SoMap.java",
    "content": "package com.pj.utils;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Date;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport javax.servlet.http.HttpServletRequest;\n\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\n/**\n * Map< String, Object> 是最常用的一种Map类型，但是它写着麻烦 \n * <p>所以特封装此类，继承Map，进行一些扩展，可以让Map更灵活使用 \n * <p>最新：2020-12-10 新增部分构造方法\n * @author click33\n */\npublic class SoMap extends LinkedHashMap<String, Object> {\n\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic SoMap() {\n\t}\n\t\n\t/** 以下元素会在isNull函数中被判定为Null， */\n\tpublic static final Object[] NULL_ELEMENT_ARRAY = {null, \"\"};\n\tpublic static final List<Object> NULL_ELEMENT_LIST;\n\n\tstatic {\n\t\tNULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);\n\t}\n\n\t// ============================= 读值 =============================\n\n\t/** 获取一个值 */\n\t@Override\n\tpublic Object get(Object key) {\n\t\tif(\"this\".equals(key)) {\n\t\t\treturn this;\n\t\t}\n\t\treturn super.get(key);\n\t}\n\n\t/** 如果为空，则返回默认值 */\n\tpublic Object get(Object key, Object defaultValue) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn value;\n\t}\n\t\n\t/** 转为String并返回 */\n\tpublic String getString(String key) {\n\t\tObject value = get(key);\n\t\tif(value == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn String.valueOf(value);\n\t}\n\n\t/** 如果为空，则返回默认值 */\n\tpublic String getString(String key, String defaultValue) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn String.valueOf(value);\n\t}\n\n\t/** 转为int并返回 */\n\tpublic int getInt(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Integer.valueOf(String.valueOf(value));\n\t}\n\t/** 转为int并返回，同时指定默认值 */\n\tpublic int getInt(String key, int defaultValue) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Integer.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为long并返回 */\n\tpublic long getLong(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Long.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为double并返回 */\n\tpublic double getDouble(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn 0.0;\n\t\t}\n\t\treturn Double.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为boolean并返回 */\n\tpublic boolean getBoolean(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn Boolean.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为Date并返回，根据自定义格式 */\n\tpublic Date getDateByFormat(String key, String format) {\n\t\ttry {\n\t\t\treturn new SimpleDateFormat(format).parse(getString(key));\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\t/** 转为Date并返回，根据格式： yyyy-MM-dd */\n\tpublic Date getDate(String key) {\n\t\treturn getDateByFormat(key, \"yyyy-MM-dd\");\n\t}\n\n\t/** 转为Date并返回，根据格式： yyyy-MM-dd HH:mm:ss */\n\tpublic Date getDateTime(String key) {\n\t\treturn getDateByFormat(key, \"yyyy-MM-dd HH:mm:ss\");\n\t}\n\n\t/** 转为Map并返回 */\n\t@SuppressWarnings({ \"unchecked\", \"rawtypes\" })\n\tpublic SoMap getMap(String key) {\n\t\tObject value = get(key);\n\t\tif(value == null) {\n\t\t\treturn SoMap.getSoMap();\n\t\t}\n\t\tif(value instanceof Map) {\n\t\t\treturn SoMap.getSoMap((Map)value);\n\t\t}\n\t\tif(value instanceof String) {\n\t\t\treturn SoMap.getSoMap().setJsonString((String)value);\n\t\t}\n\t\tthrow new RuntimeException(\"值无法转化为SoMap: \" + value);\n\t}\n\n\t/** 获取集合(必须原先就是个集合，否则会创建个新集合并返回) */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic List<Object> getList(String key) {\n\t\tObject value = get(key);\n\t\tList<Object> list = null;\n\t\tif(value == null || value.equals(\"\")) {\n\t\t\tlist = new ArrayList<Object>();\n\t\t}\n\t\telse if(value instanceof List) {\n\t\t\tlist = (List<Object>)value;\n\t\t} else {\n\t\t\tlist = new ArrayList<Object>();\n\t\t\tlist.add(value);\n\t\t}\n\t\treturn list;\n\t}\n\n\t/** 获取集合 (指定泛型类型) */\n\tpublic <T> List<T> getList(String key, Class<T> cs) {\n\t\tList<Object> list = getList(key);\n\t\tList<T> list2 = new ArrayList<T>();\n\t\tfor (Object obj : list) {\n\t\t\tT objC = getValueByClass(obj, cs);\n\t\t\tlist2.add(objC);\n\t\t}\n\t\treturn list2;\n\t}\n\n\t/** 获取集合(逗号分隔式)，(指定类型) */\n\tpublic <T> List<T> getListByComma(String key, Class<T> cs) {\n\t\tString listStr = getString(key);\n\t\tif(listStr == null || listStr.equals(\"\")) {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\t\t// 开始转化\n\t\tString [] arr = listStr.split(\",\");\n\t\tList<T> list = new ArrayList<T>();\n\t\tfor (String str : arr) {\n\t\t\tif(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) {\n\t\t\t\tstr = str.trim();\n\t\t\t}\n\t\t\tT objC = getValueByClass(str, cs);\n\t\t\tlist.add(objC);\n\t\t}\n\t\treturn list;\n\t}\n\n\n\t/** 根据指定类型从map中取值，返回实体对象 */\n\tpublic <T> T getModel(Class<T> cs) {\n\t\ttry {\n\t\t\treturn getModelByObject(cs.newInstance());\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\t\n\t/** 从map中取值，塞到一个对象中 */\n\tpublic <T> T getModelByObject(T obj) {\n\t\t// 获取类型 \n\t\tClass<?> cs = obj.getClass();\n\t\t// 循环复制  \n\t\tfor (Field field : cs.getDeclaredFields()) {\n\t\t\ttry {\n\t\t\t\t// 获取对象 \n\t\t\t\tObject value = this.get(field.getName());\t\n\t\t\t\tif(value == null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfield.setAccessible(true);\t\n\t\t\t\tObject valueConvert = getValueByClass(value, field.getType());\n\t\t\t\tfield.set(obj, valueConvert);\n\t\t\t} catch (IllegalArgumentException | IllegalAccessException e) {\n\t\t\t\tthrow new RuntimeException(\"属性取值出错：\" + field.getName(), e);\n\t\t\t}\n\t\t}\n\t\treturn obj;\n\t}\n\n\t\n\n\t/**\n\t * 将指定值转化为指定类型并返回\n\t * @param obj\n\t * @param cs\n\t * @param <T>\n\t * @return\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getValueByClass(Object obj, Class<T> cs) {\n\t\tString obj2 = String.valueOf(obj);\n\t\tObject obj3 = null;\n\t\tif (cs.equals(String.class)) {\n\t\t\tobj3 = obj2;\n\t\t} else if (cs.equals(int.class) || cs.equals(Integer.class)) {\n\t\t\tobj3 = Integer.valueOf(obj2);\n\t\t} else if (cs.equals(long.class) || cs.equals(Long.class)) {\n\t\t\tobj3 = Long.valueOf(obj2);\n\t\t} else if (cs.equals(short.class) || cs.equals(Short.class)) {\n\t\t\tobj3 = Short.valueOf(obj2);\n\t\t} else if (cs.equals(byte.class) || cs.equals(Byte.class)) {\n\t\t\tobj3 = Byte.valueOf(obj2);\n\t\t} else if (cs.equals(float.class) || cs.equals(Float.class)) {\n\t\t\tobj3 = Float.valueOf(obj2);\n\t\t} else if (cs.equals(double.class) || cs.equals(Double.class)) {\n\t\t\tobj3 = Double.valueOf(obj2);\n\t\t} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {\n\t\t\tobj3 = Boolean.valueOf(obj2);\n\t\t} else {\n\t\t\tobj3 = (T)obj;\n\t\t}\n\t\treturn (T)obj3;\n\t}\n\n\t\n\t// ============================= 写值 =============================\n\n\t/**\n\t * 给指定key添加一个默认值（只有在这个key原来无值的情况先才会set进去）\n\t */\n\tpublic void setDefaultValue(String key, Object defaultValue) {\n\t\tif(isNull(key)) {\n\t\t\tset(key, defaultValue);\n\t\t}\n\t}\n\n\t/** set一个值，连缀风格 */\n\tpublic SoMap set(String key, Object value) {\n\t\t// 防止敏感key \n\t\tif(key.toLowerCase().equals(\"this\")) {\t\t\n\t\t\treturn this;\n\t\t}\n\t\tput(key, value);\n\t\treturn this;\n\t}\n\n\t/** 将一个Map塞进SoMap */\n\tpublic SoMap setMap(Map<String, ?> map) {\n\t\tif(map != null) {\n\t\t\tfor (String key : map.keySet()) {\n\t\t\t\tthis.set(key, map.get(key));\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/** 将一个对象解析塞进SoMap */\n\tpublic SoMap setModel(Object model) {\n\t\tif(model == null) {\n\t\t\treturn this;\n\t\t}\n\t\tField[] fields = model.getClass().getDeclaredFields();\n\t    for (Field field : fields) {\n\t        try{\n\t            field.setAccessible(true);\n\t            boolean isStatic = Modifier.isStatic(field.getModifiers());\n\t            if(!isStatic) {\n\t\t            this.set(field.getName(), field.get(model));\n\t            }\n\t        }catch (Exception e){\n\t        \tthrow new RuntimeException(e);\n\t        }\n\t    }\n\t\treturn this;\n\t}\n\n\t/** 将json字符串解析后塞进SoMap */\n\tpublic SoMap setJsonString(String jsonString) {\n\t\ttry {\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tMap<String, Object> map = new ObjectMapper().readValue(jsonString, Map.class);\n\t\t\treturn this.setMap(map);\n\t\t} catch (JsonProcessingException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\t\n\t// ============================= 删值 =============================\n\n\t/** delete一个值，连缀风格 */\n\tpublic SoMap delete(String key) {\n\t\tremove(key);\n\t\treturn this;\n\t}\n\n\t/** 清理所有value为null的字段 */\n\tpublic SoMap clearNull() {\n\t\tIterator<String> iterator = this.keySet().iterator();\n\t\twhile(iterator.hasNext()) {\n\t\t\tString key = iterator.next();\n\t\t\tif(this.isNull(key)) {\n\t\t\t\titerator.remove();\n\t\t\t\tthis.remove(key);\n\t\t\t}\n\n\t\t}\n\t\treturn this;\n\t}\n\t/** 清理指定key */\n\tpublic SoMap clearIn(String ...keys) {\n\t\tList<String> keys2 = Arrays.asList(keys);\n\t\tIterator<String> iterator = this.keySet().iterator();\n\t\twhile(iterator.hasNext()) {\n\t\t\tString key = iterator.next();\n\t\t\tif(keys2.contains(key) == true) {\n\t\t\t\titerator.remove();\n\t\t\t\tthis.remove(key);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\t/** 清理掉不在列表中的key */\n\tpublic SoMap clearNotIn(String ...keys) {\n\t\tList<String> keys2 = Arrays.asList(keys);\n\t\tIterator<String> iterator = this.keySet().iterator();\n\t\twhile(iterator.hasNext()) {\n\t\t\tString key = iterator.next();\n\t\t\tif(keys2.contains(key) == false) {\n\t\t\t\titerator.remove();\n\t\t\t\tthis.remove(key);\n\t\t\t}\n\n\t\t}\n\t\treturn this;\n\t}\n\t/** 清理掉所有key */\n\tpublic SoMap clearAll() {\n\t\tclear();\n\t\treturn this;\n\t}\n\t\n\n\t// ============================= 快速构建 ============================= \n\n\t/** 构建一个SoMap并返回 */\n\tpublic static SoMap getSoMap() {\n\t\treturn new SoMap();\n\t}\n\t/** 构建一个SoMap并返回 */\n\tpublic static SoMap getSoMap(String key, Object value) {\n\t\treturn new SoMap().set(key, value);\n\t}\n\t/** 构建一个SoMap并返回 */\n\tpublic static SoMap getSoMap(Map<String, ?> map) {\n\t\treturn new SoMap().setMap(map);\n\t}\n\n\t/** 将一个对象集合解析成为SoMap */\n\tpublic static SoMap getSoMapByModel(Object model) {\n\t\treturn SoMap.getSoMap().setModel(model);\n\t}\n\t\n\t/** 将一个对象集合解析成为SoMap集合 */\n\tpublic static List<SoMap> getSoMapByList(List<?> list) {\n\t\tList<SoMap> listMap = new ArrayList<SoMap>();\n\t\tfor (Object model : list) {\n\t\t\tlistMap.add(getSoMapByModel(model));\n\t\t}\n\t\treturn listMap;\n\t}\n\t\n\t/** 克隆指定key，返回一个新的SoMap */\n\tpublic SoMap cloneKeys(String... keys) {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : keys) {\n\t\t\tso.set(key, this.get(key));\n\t\t}\n\t\treturn so;\n\t}\n\t/** 克隆所有key，返回一个新的SoMap */\n\tpublic SoMap cloneSoMap() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(key, this.get(key));\n\t\t}\n\t\treturn so;\n\t}\n\n\t/** 将所有key转为大写 */\n\tpublic SoMap toUpperCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(key.toUpperCase(), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key转为小写 */\n\tpublic SoMap toLowerCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(key.toLowerCase(), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key中下划线转为中划线模式 (kebab-case风格) */\n\tpublic SoMap toKebabCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(wordEachKebabCase(key), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key中下划线转为小驼峰模式 */\n\tpublic SoMap toHumpCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(wordEachBigFs(key), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key中小驼峰转为下划线模式 */\n\tpublic SoMap humpToLineCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(wordHumpToLine(key), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t\n\t\n\t\n\t\n\t// ============================= 辅助方法 =============================\n\n\n\t/** 指定key是否为null，判定标准为 NULL_ELEMENT_ARRAY 中的元素  */\n\tpublic boolean isNull(String key) {\n\t\treturn valueIsNull(get(key));\n\t}\n\n\t/** 指定key列表中是否包含value为null的元素，只要有一个为null，就会返回true */\n\tpublic boolean isContainNull(String ...keys) {\n\t\tfor (String key : keys) {\n\t\t\tif(this.isNull(key)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t/** 与isNull()相反 */\n\tpublic boolean isNotNull(String key) {\n\t\treturn !isNull(key);\n\t}\n\t/** 指定key的value是否为null，作用同isNotNull() */\n\tpublic boolean has(String key) {\n\t\treturn !isNull(key);\n\t}\n\t\n\t/** 指定value在此SoMap的判断标准中是否为null */\n\tpublic boolean valueIsNull(Object value) {\n\t\treturn NULL_ELEMENT_LIST.contains(value);\n\t}\n\t\n\t/** 验证指定key不为空，为空则抛出异常 */\n\tpublic SoMap checkNull(String ...keys) {\n\t\tfor (String key : keys) {\n\t\t\tif(this.isNull(key)) {\n\t\t\t\tthrow new RuntimeException(\"参数\" + key + \"不能为空\");\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\tstatic Pattern patternNumber = Pattern.compile(\"[0-9]*\");\n\t/** 指定key是否为数字 */\n\tpublic boolean isNumber(String key) {\n\t\tString value = getString(key);\n\t\tif(value == null) {\n\t\t\treturn false;\n\t\t}\n\t    return patternNumber.matcher(value).matches();   \n\t}\n\n\t\n\t\n\t\n\t/**\n\t * 转为JSON字符串\n\t */\n\tpublic String toJsonString() {\n\t\ttry {\n//\t\t\tSoMap so = SoMap.getSoMap(this);\n\t\t\treturn new ObjectMapper().writeValueAsString(this);\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n//\t/**\n//\t * 转为JSON字符串, 带格式的 \n//\t */\n//\tpublic String toJsonFormatString() {\n//\t\ttry {\n//\t\t\treturn JSON.toJSONString(this, true); \n//\t\t} catch (Exception e) {\n//\t\t\tthrow new RuntimeException(e);\n//\t\t}\n//\t}\n\n\t// ============================= web辅助 =============================\n\n\n\t/**\n\t * 返回当前request请求的的所有参数 \n\t * @return\n\t */\n\tpublic static SoMap getRequestSoMap() {\n\t\t// 大善人SpringMVC提供的封装 \n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n\t\tif(servletRequestAttributes == null) {\n\t\t\tthrow new RuntimeException(\"当前线程非JavaWeb环境\");\n\t\t}\n\t\t// 当前request\n\t\tHttpServletRequest request = servletRequestAttributes.getRequest(); \n\t\tif (request.getAttribute(\"currentSoMap\") == null || request.getAttribute(\"currentSoMap\") instanceof SoMap == false ) {\n\t\t\tinitRequestSoMap(request);\n\t\t}\n\t\treturn (SoMap)request.getAttribute(\"currentSoMap\");\n\t}\n\n\t/** 初始化当前request的 SoMap */\n\tprivate static void initRequestSoMap(HttpServletRequest request) {\n\t\tSoMap soMap = new SoMap();\n\t\tMap<String, String[]> parameterMap = request.getParameterMap();\t// 获取所有参数 \n\t\tfor (String key : parameterMap.keySet()) {\n\t\t\ttry {\n\t\t\t\tString[] values = parameterMap.get(key); // 获得values \n\t\t\t\tif(values.length == 1) {\n\t\t\t\t\tsoMap.set(key, values[0]);\n\t\t\t\t} else {\n\t\t\t\t\tList<String> list = new ArrayList<String>();\n\t\t\t\t\tfor (String v : values) {\n\t\t\t\t\t\tlist.add(v);\n\t\t\t\t\t}\n\t\t\t\t\tsoMap.set(key, list);\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new RuntimeException(e);\n\t\t\t}\n\t\t}\n\t\trequest.setAttribute(\"currentSoMap\", soMap);\n\t}\n\t\n\t/**\n\t * 验证返回当前线程是否为JavaWeb环境 \n\t * @return\n\t */\n\tpublic static boolean isJavaWeb() {\n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装 \n\t\tif(servletRequestAttributes == null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\t\n\n\n\t// ============================= 常见key （以下key经常用，所以封装以下，方便写代码） =============================\n\n\t/** get 当前页  */\n\tpublic int getKeyPageNo() {\n\t\tint pageNo = getInt(\"pageNo\", 1);\n\t\tif(pageNo <= 0) {\n\t\t\tpageNo = 1;\n\t\t}\n\t\treturn pageNo;\n\t}\n\t/** get 页大小  */\n\tpublic int getKeyPageSize() {\n\t\tint pageSize = getInt(\"pageSize\", 10);\n\t\tif(pageSize <= 0 || pageSize > 1000) {\n\t\t\tpageSize = 10;\n\t\t}\n\t\treturn pageSize;\n\t}\n\n\t/** get 排序方式 */\n\tpublic int getKeySortType() {\n\t\treturn getInt(\"sortType\");\n\t}\n\n\n\n\n\t\n\n\t// ============================= 工具方法 =============================\n\t\n\n\t/**\n\t * 将一个一维集合转换为树形集合 \n\t * @param list         集合\n\t * @param idKey        id标识key\n\t * @param parentIdKey  父id标识key\n\t * @param childListKey 子节点标识key\n\t * @return 转换后的tree集合 \n\t */\n\tpublic static List<SoMap> listToTree(List<SoMap> list, String idKey, String parentIdKey, String childListKey) {\n\t\t// 声明新的集合，存储tree形数据 \n\t\tList<SoMap> newTreeList = new ArrayList<SoMap>();\n\t\t// 声明hash-Map，方便查找数据 \n\t\tSoMap hash = new SoMap();\n\t\t// 将数组转为Object的形式，key为数组中的id \n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tSoMap json = (SoMap) list.get(i);\n\t\t\thash.put(json.getString(idKey), json);\n\t\t}\n\t\t// 遍历结果集\n\t\tfor (int j = 0; j < list.size(); j++) {\n\t\t\t// 单条记录\n\t\t\tSoMap aVal = (SoMap) list.get(j);\n\t\t\t// 在hash中取出key为单条记录中pid的值\n\t\t\tSoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, \"\").toString());\n\t\t\t// 如果记录的pid存在，则说明它有父节点，将她添加到孩子节点的集合中\n\t\t\tif (hashVp != null) {\n\t\t\t\t// 检查是否有child属性，有则添加，没有则新建 \n\t\t\t\tif (hashVp.get(childListKey) != null) {\n\t\t\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\t\t\tList<SoMap> ch = (List<SoMap>) hashVp.get(childListKey);\n\t\t\t\t\tch.add(aVal);\n\t\t\t\t\thashVp.put(childListKey, ch);\n\t\t\t\t} else {\n\t\t\t\t\tList<SoMap> ch = new ArrayList<SoMap>();\n\t\t\t\t\tch.add(aVal);\n\t\t\t\t\thashVp.put(childListKey, ch);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnewTreeList.add(aVal);\n\t\t\t}\n\t\t}\n\t\treturn newTreeList;\n\t}\n\t\n\t\n\n\t/** 指定字符串的字符串下划线转大写模式 */\n\tprivate static String wordEachBig(String str){\n\t\tString newStr = \"\";\n\t\tfor (String s : str.split(\"_\")) {\n\t\t\tnewStr += wordFirstBig(s);\n\t\t}\n\t\treturn newStr;\n\t}\n\t/** 返回下划线转小驼峰形式 */\n\tprivate static String wordEachBigFs(String str){\n\t\treturn wordFirstSmall(wordEachBig(str));\n\t}\n\n\t/** 将指定单词首字母大写 */\n\tprivate static String wordFirstBig(String str) {\n\t\treturn str.substring(0, 1).toUpperCase() + str.substring(1, str.length());\n\t}\n\n\t/** 将指定单词首字母小写 */\n\tprivate static String wordFirstSmall(String str) {\n\t\treturn str.substring(0, 1).toLowerCase() + str.substring(1, str.length());\n\t}\n\n\t/** 下划线转中划线 */\n\tprivate static String wordEachKebabCase(String str) {\n\t\treturn str.replaceAll(\"_\", \"-\");\n\t}\n\n\t/** 驼峰转下划线  */\n\tprivate static String wordHumpToLine(String str) {\n\t\treturn str.replaceAll(\"[A-Z]\", \"_$0\").toLowerCase();\n\t}\n\t\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/application.yml",
    "content": "server:\n    port: 8002\n\n# sa-token配置\nsa-token: \n    # token名称 (同时也是cookie名称)\n    token-name: satoken-client\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-OAuth2-Client端-测试页</title>\n\t\t<style type=\"text/css\">\n\t\t\tbody{background-color: #D0D9E0;}\n\t\t\t*{margin: 0px; padding: 0px;}\n\t\t\t.login-box{max-width: 1000px; margin: 30px auto; padding: 1em;}\n\t\t\t.info{line-height: 30px;}\n\t\t\t.btn-box{margin-top: 10px; margin-bottom: 15px;}\n\t\t\t.btn-box a{margin-right: 10px;}\n\t\t\t.btn-box a:hover{text-decoration:underline !important;}\n\t\t\t.login-box input{line-height: 25px; margin-bottom: 10px; padding-left: 5px;}\n\t\t\t.login-box button{padding: 5px 15px; margin-top: 20px; cursor: pointer; }\n\t\t\t.login-box a{text-decoration: none;}\n\t\t\t.pst{color: #666; margin-top: 15px;}\n\t\t\t.ps{color: #666; margin-left: 10px;}\n\t\t\t.login-box code{display: block; background-color: #F5F2F0; border: 1px #ccc solid; color: #600; padding: 15px; margin-top: 5px; border-radius: 2px; }\n\t\t\t.info b,.info span{color: green;}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t<h2>Sa-OAuth2-Client端-测试页</h2> <br>\n\t\t\t<div class=\"info\">\n\t\t\t\t<div>当前账号id： \n\t\t\t\t\t<b class=\"uid\" th:utext=\"${uid}\"></b>\n\t\t\t\t</div>\n\t\t\t\t<div>当前Openid： <span class=\"openid\"></span></div>\n\t\t\t\t<div>当前Access-Token： <span class=\"access_token\"></span></div>\n\t\t\t\t<div>当前Refresh-Token： <span class=\"refresh_token\"></span></div>\n\t\t\t\t<div>当前令牌包含Scope： <span class=\"scope\"></span></div>\n\t\t\t\t<div>当前Client-Token： <span class=\"client_token\"></span></div>\n\t\t\t</div>\n\t\t\t<div class=\"btn-box\">\n\t\t\t\t<a href=\"javascript:logout();\">注销</a>\n\t\t\t\t<a href=\"/\">回到首页</a>\n\t\t\t</div>\n\t\t\t<hr><br>\n\t\t\t\n\t\t\t<h3>模式一：授权码（Authorization Code）</h3>\n\t\t\t<p class=\"pst\">授权码：OAuth2.0标准授权流程，先 (重定向) 获取Code授权码，再 (Rest API) 获取 Access-Token 和 Openid </p> \n\t\t\t\n\t\t\t<a href=\"http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/\">\n\t\t\t\t<button>点我开始授权登录（静默授权）</button> \n\t\t\t</a>\n\t\t\t<span class=\"ps\">当请求链接不包含 scope 权限，或请求的 scope 近期已授权时，将无需用户手动确认，做到静默授权</span>\n\t\t\t<code>http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/</code>\n\t\t\t\n\t\t\t<a href=\"http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=openid,userinfo\">\n\t\t\t\t<button>授权登录（显式授权）</button> \n\t\t\t</a>\n\t\t\t<span class=\"ps\">当请求链接包含具体的 scope 权限时，将需要用户手动确认，此时 OAuth-Server 会返回更多的数据</span>\n\t\t\t<code>http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=openid,userinfo</code>\n\t\t\t\n\t\t\t<button onclick=\"refreshToken()\">刷新令牌</button> \n\t\t\t<span class=\"ps\">我们可以拿着 Refresh-Token 去刷新我们的 Access-Token，每次刷新后旧Token将作废</span>\n\t\t\t<code>http://sa-oauth-server.com:8000/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value}</code>\n\t\t\t\n\t\t\t<button onclick=\"getUserinfo()\">获取账号信息</button> \n\t\t\t<span class=\"ps\">使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) </span>\n\t\t\t<code>http://sa-oauth-server.com:8000/oauth2/userinfo?access_token={value}</code>\n\t\t\t\n\t\t\t<br>\n\t\t\t<h3>模式二：隐藏式（Implicit）</h3>\n\t\t\t<a href=\"http://sa-oauth-server.com:8000/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo\">\n\t\t\t\t<button>隐藏式</button> \n\t\t\t</a>\n\t\t\t<span class=\"ps\">越过授权码的步骤，直接返回token到前端页面（ 格式：http//:domain.com#token=xxxx-xxxx ）</span>\n\t\t\t<code>http://sa-oauth-server.com:8000/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo</code>\n\t\t\t\n\t\t\t<br>\n\t\t\t<h3>模式三：密码式（Password）</h3>\n\t\t\t<p class=\"pst\">在下面输入Server端的用户名和密码，使用密码式进行 OAuth2 授权登录</p>\n\t\t\t账号：<input name=\"username\">\n\t\t\t密码：<input name=\"password\">\n\t\t\t<button onclick=\"passwordLogin()\">登录</button> \n\t\t\t<code>http://sa-oauth-server.com:8000/oauth2/token?grant_type=password&client_id={value}&client_secret={value}&username={value}&password={value}</code>\n\t\t\t\n\t\t\t<br>\n\t\t\t<h3>模式四：凭证式（Client Credentials）</h3>\n\t\t\t<p class=\"pst\">以上三种模式获取的都是用户的 Access-Token，代表用户对第三方应用的授权，在OAuth2.0中还有一种针对 Client级别的授权，\n\t\t\t\t即：Client-Token，代表应用自身的资源授权</p>\n\t\t\t<p class=\"pst\">Client-Token具有延迟作废特性，即：在每次获取最新Client-Token的时候，旧Client-Token不会立即过期，而是作为Lower-Client-Token再次\n\t\t\t\t储存起来，资源请求方只要携带其中之一便可通过Token校验，这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”，\n\t\t\t\t保证了服务的高可用</p>\n\t\t\t\t\n\t\t\t<button onclick=\"getClientToken()\">获取应用Client-Token</button> \n\t\t\t<code>http://sa-oauth-server.com:8000/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value}</code>\n\t\t\t\n\t\t\t<br><br>\n\t\t\t<span>更多资料请参考 Sa-Token 官方文档地址：</span>\n\t\t\t<a href=\"https://sa-token.cc/\">https://sa-token.cc/</a>\n\t\t\t\n\t\t\t<div style=\"height: 200px;\"></div>\n\t\t</div>\n\t\t<script src=\"https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js\"></script>\n\t\t<script src=\"https://www.layuicdn.com/layer-v3.1.1/layer.js\"></script>\n\t\t<script>window.jQuery || alert('当前页面CDN服务商已宕机，请将所有js包更换为本地依赖')</script>\n\t\t<script type=\"text/javascript\">\n\t\t\n\t\t\t// 根据code授权码进行登录 \n\t\t\tfunction doLogin(code) {\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/codeLogin?code=' + code,\n\t\t\t\t\tdataType: 'json', \n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tconsole.log('返回：', res);\n\t\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\t\tsetInfo(res.data);\n\t\t\t\t\t\t\tlayer.msg('登录成功！');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlayer.msg(res.msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\tvar code = getParam('code');\n\t\t\tif(code) {\n\t\t\t\tdoLogin(code);\n\t\t\t}\n\t\t\t\n\t\t\t// 根据 Refresh-Token 去刷新 Access-Token \n\t\t\tfunction refreshToken() {\n\t\t\t\tvar refreshToken = $('.refresh_token').text();\n\t\t\t\tif(refreshToken == '') {\n\t\t\t\t\treturn layer.alert('您还没有获取 Refresh-Token ，请先授权登录');\n\t\t\t\t}\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/refresh?refreshToken=' + refreshToken,\n\t\t\t\t\tdataType: 'json', \n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tconsole.log('返回：', res);\n\t\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\t\tsetInfo(res.data);\n\t\t\t\t\t\t\tlayer.msg('登录成功！');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlayer.msg(res.msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t// 模式三：密码式-授权登录\n\t\t\tfunction passwordLogin() {\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/passwordLogin',\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tusername: $('[name=username]').val(),\n\t\t\t\t\t\tpassword: $('[name=password]').val()\n\t\t\t\t\t},\n\t\t\t\t\tdataType: 'json', \n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tconsole.log('返回：', res);\n\t\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\t\tsetInfo(res.data);\n\t\t\t\t\t\t\tlayer.msg('登录成功！');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlayer.msg(res.msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t// 模式四：获取应用的 Client-Token\n\t\t\tfunction getClientToken () {\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/clientToken',\n\t\t\t\t\tdataType: 'json', \n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tconsole.log('返回：', res);\n\t\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\t\tsetInfo(res.data);\n\t\t\t\t\t\t\tlayer.msg('获取成功！');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlayer.msg(res.msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t// 使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 \n\t\t\tfunction getUserinfo() {\n\t\t\t\tvar accessToken = $('.access_token').text();\n\t\t\t\tif(accessToken == '') {\n\t\t\t\t\treturn layer.alert('您还没有获取 Access-Token ，请先授权登录');\n\t\t\t\t}\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/getUserinfo',\n\t\t\t\t\tdata: {accessToken: accessToken},\n\t\t\t\t\tdataType: 'json', \n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\t\tlayer.alert(JSON.stringify(res.data));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlayer.alert(res.msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t// 注销 \n\t\t\tfunction logout() {\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/logout',\n\t\t\t\t\tdataType: 'json', \n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tlocation.href = '/';\n\t\t\t\t\t},\n\t\t\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\t\t\n\t\t\t\n\t\t\t// 写入数据\n\t\t\tfunction setInfo(info) {\n\t\t\t\tconsole.log('info', info);\n\t\t\t\tfor (var key in info) {\n\t\t\t\t\t$('.' + key).text(info[key]);\n\t\t\t\t}\n\t\t\t\tif($('.uid').text() == '') {\n\t\t\t\t\t$('.uid').html('<b style=\"color: #E00;\">未登录</b>')\n\t\t\t\t}\n\t\t\t}\n\t\t\tsetInfo({});\n\t\t\t\n\t\t\t// 从url中查询到指定名称的参数值 \n\t\t\tfunction getParam(name, defaultValue){\n\t\t\t\tvar query = window.location.search.substring(1);\n\t\t\t\tvar vars = query.split(\"&\");\n\t\t\t\tfor (var i=0;i<vars.length;i++) {\n\t\t\t\t\tvar pair = vars[i].split(\"=\");\n\t\t\t\t\tif(pair[0] == name){return pair[1];}\n\t\t\t\t}\n\t\t\t\treturn(defaultValue == undefined ? null : defaultValue);\n\t\t\t}\n\t\t\t\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-Token-OAuth2 Client 端 - 测试页</title>\n\t\t<style type=\"text/css\">\n\t\t\tbody{background-color: #F0F9EB;}\n\t\t\t*{margin: 0px; padding: 0px;}\n\t\t\t.login-box{max-width: 1000px; margin: 30px auto; padding: 1em;}\n\t\t\t/* 全局配置盒子 */\n\t\t\t.in-cfg-box{line-height: 30px; padding-top: 10px;}\n\t\t\t.in-cfg-box .in-cfg-div{display: flex;}\n\t\t\t.in-cfg-box .in-cfg-div>span{width: 280px;}\n\t\t\t.in-cfg-box .in-cfg-div>input{flex: 1;}\n\t\t\t\n\t\t\t.login-box textarea{width: calc(100% - 1em); padding: 0.5em; min-height: 4em; box-sizing: border-box;}\n\t\t\t.login-box button{padding: 5px 15px; margin-top: 5px; margin-bottom: 5px; cursor: pointer; }\n\t\t\t.login-box select{ height: 30px; cursor: pointer; }\n\t\t\t\n\t\t\t.login-box h4{margin-top: 20px;}\n\t\t\t\n\t\t\t.btn-box{margin-top: 10px; margin-bottom: 15px;}\n\t\t\t.btn-box a{margin-right: 10px;}\n\t\t\t.btn-box a:hover{text-decoration:underline !important;}\n\t\t\t.login-box input{line-height: 25px; margin-bottom: 10px; padding-left: 5px;}\n\t\t\t.login-box a{text-decoration: none;}\n\t\t\t.pst{color: #666; margin-top: 15px;}\n\t\t\t.ps{color: #666; margin-bottom: 5px;}\n\t\t\t\n\t\t\t.oauth2-server-urls-box{ padding: 0.5em 0; padding-bottom: 0; margin-bottom: 0.5em; background-color: #ddd;}\n\t\t\t.oauth2-server-urls-box .in-cfg-div>span{text-indent: 0.5em;}\n\t\t\t\n\t\t\t/* loading框样式 */\n\t\t\t.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n\t\t\t.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n\t\t\t.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }\n\t\t\t\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t<h2>Sa-Token-OAuth2 Client 端 测试页 </h2> \n\t\t\t<p class=\"pst\">注：为方便测试，此处将应用秘钥等敏感信息采用前端填写式，真实项目应该改为后端配置。</p> \n\t\t\t<br><hr><br>\n\t\t\t<h3>配置信息</h3>\n\t\t\t<div class=\"in-cfg-box\">\n\t\t\t\t<div class=\"in-cfg-div\"><span>OAuth2 Server 主机地址：</span><input class=\"in-cfg\" name=\"oauth2_server_url\" /></div>\n\t\t\t\t<div class=\"oauth2-server-urls-box\">\n\t\t\t\t\t<div class=\"in-cfg-div\"><span>OAuth2 Server 授权页地址：</span><input class=\"in-cfg\" name=\"oauth2_server_auth_url\" /></div>\n\t\t\t\t\t<div class=\"in-cfg-div\"><span>OAuth2 Server 获取 token 地址：</span><input class=\"in-cfg\" name=\"oauth2_server_token_url\" /></div>\n\t\t\t\t\t<div class=\"in-cfg-div\"><span>OAuth2 Server 刷新 token 地址：</span><input class=\"in-cfg\" name=\"oauth2_server_refresh_token_url\" /></div>\n\t\t\t\t\t<div class=\"in-cfg-div\"><span>OAuth2 Server 获取 userinfo 地址：</span><input class=\"in-cfg\" name=\"oauth2_server_userinfo_url\" /></div>\n\t\t\t\t\t<div class=\"in-cfg-div\" style=\"margin-top: -5px;\">&nbsp;<button onclick=\"autoSplicingUrl();\">根据主机 URL 一键拼接授权页等地址</button></div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"in-cfg-div\"><span>Client Id：</span><input class=\"in-cfg\" name=\"client_id\" /></div>\n\t\t\t\t<div class=\"in-cfg-div\"><span>Client Secret：</span><input class=\"in-cfg\" name=\"client_secret\" /></div>\n\t\t\t\t<div class=\"in-cfg-div\"><span>重定向授权地址：</span><input class=\"in-cfg\" name=\"redirect_uri\" /></div>\n\t\t\t\t<div class=\"in-cfg-div\"><span>请求 Scope (多个用逗号/空格隔开)：</span><input class=\"in-cfg\" name=\"scope\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"btn-box\">\n\t\t\t\t<button onclick=\"clearLocalCfg()\">清空配置</button>\n\t\t\t\t<button onclick=\"clearLocalCfg(); readLocalCfg();\">恢复默认配置</button>\n\t\t\t\t<button onclick=\"autoFillGiteeUrl();\">一键填写 Gitee 参数样例</button>\n\t\t\t\t<button onclick=\"autoFillSaSsoMaxUrl();\">一键填写 Sa-Sso-Max 参数样例</button>\n\t\t\t\t<a href=\"javascript: location.href = location.href.split('?')[0].split('#')[0];\">回到首页</a>\n\t\t\t</div>\n\t\t\t\n\t\t\t<hr><br>\n\t\t\t<h3>模式一：授权码（Authorization Code）</h3>\n\t\t\t<p class=\"pst\">授权码：OAuth2.0标准授权流程，先 (重定向) 获取Code授权码，再 (Rest API) 获取 Access-Token 和 Openid 等信息</p> \n\t\t\t\n\t\t\t<h4>1、获取授权码</h4>\n\t\t\t<button onclick=\"buildAuthorizationCodeUrl()\">构建授权地址</button>\n\t\t\t<textarea class=\"auth-code-url\"></textarea>\n\t\t\t<button onclick=\"jumpAuthCodeUrl()\">→ 访问上述授权地址</button> <br>\n\t\t\t<span>从 URL 上读取到的 code 为：<span class=\"show-url-code\" style=\"color: green;\"></span></span> \n\t\t\t\n\t\t\t<h4>2、获取 Access-Token </h4>\n\t\t\t<button onclick=\"buildCodeTakeTokenUrl()\">构建 code 换 Access-Token 接口地址</button>\n\t\t\t<textarea class=\"code-take-token-url\"></textarea>\n\t\t\t<button onclick=\"ajaxCodeToAccessToken()\">→ 请求上述地址，获取 Access-Token 数据</button> 请求结果显示如下：\n\t\t\t<textarea class=\"code-take-token-result\"></textarea> \n\t\t\t\n\t\t\t<h4>3、刷新 Access-Token </h4>\n\t\t\t<button onclick=\"buildRefreshTokenUrl()\">构建刷新 Access-Token 接口地址 </button>\n\t\t\t请先填写 Refresh-Token 值：<input name=\"refresh-token-input\" style=\"width: 500px;\">\n\t\t\t<textarea class=\"refresh-token-url\"></textarea>\n\t\t\t<p class=\"ps\">我们可以拿着 Refresh-Token 去刷新我们的 Access-Token，每次刷新后旧Token将作废</p>\n\t\t\t<button onclick=\"ajaxRefreshToken()\">→ 请求上述地址，刷新 Access-Token </button> 请求结果显示如下：\n\t\t\t<textarea class=\"refresh-token-result\"></textarea> \n\t\t\t\n\t\t\t<h4>4、获取 Userinfo </h4>\n\t\t\t<button onclick=\"buildUserinfoUrl()\">构建刷新 Userinfo 接口地址 </button>\n\t\t\t请先填写 Access-Token 值：<input name=\"access-token-input\" style=\"width: 500px;\">\n\t\t\t<textarea class=\"userinfo-url\"></textarea>\n\t\t\t<p class=\"ps\">使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) </p>\n\t\t\t<button onclick=\"ajaxUserinfoUrl()\">→ 请求上述地址，获取 Userinfo 信息 </button> \n\t\t\t（\n\t\t\t请求 Method：\n\t\t\t<select name=\"userinfo-ajax-method\">\n\t\t\t\t<option value=\"GET\">GET</option>\n\t\t\t\t<option value=\"POST\">POST</option>\n\t\t\t\t<option value=\"PUT\">PUT</option>\n\t\t\t\t<option value=\"DELETE\">DELETE</option>\n\t\t\t\t<option value=\"HEAD\">HEAD</option>\n\t\t\t\t<option value=\"OPTIONS\">OPTIONS</option>\n\t\t\t</select>\n\t\t\t）\n\t\t\t请求结果显示如下：\n\t\t\t<textarea class=\"userinfo-result\"></textarea>\n\t\t\t\n\t\t\t<h4>5、回收 Access-Token </h4>\n\t\t\t<button onclick=\"buildRevokeTokenUrl()\">构建回收 Access-Token 接口地址 </button>\n\t\t\t<!-- 请先填写 Access-Token 值：<input name=\"access-token-input\" style=\"width: 500px;\"> -->\n\t\t\t<textarea class=\"revoke-token-url\"></textarea>\n\t\t\t<p class=\"ps\">回收后，该 Access-Token 将无法再使用（点击上面的 Userinfo 接口试一试）</p>\n\t\t\t<button onclick=\"ajaxRevokeTokenUrl()\">→ 请求上述地址，回收 Access-Token </button> 请求结果显示如下：\n\t\t\t<textarea class=\"revoke-token-result\"></textarea>\n\t\t\t\n\t\t\t\n\t\t\t<br><br>\n\t\t\t<h3>模式二：隐藏式（Implicit）</h3>\n\t\t\t<p class=\"pst\">越过授权码的步骤，直接返回token到前端页面（ 格式：http://domain.com#token=xxxx-xxxx ）</p>\n\t\t\t\n\t\t\t<button onclick=\"buildImplicitUrl()\">构建授权地址</button>\n\t\t\t<textarea class=\"implicit-url\"></textarea>\n\t\t\t<button onclick=\"jumpImplicitUrl()\">→ 访问上述授权地址</button> <br>\n\t\t\t<span>从 URL 上读取到的 Access-Token 为：<span class=\"show-url-access-token\" style=\"color: green;\"></span></span> \n\t\t\t\n\t\t\t\n\t\t\t<br><br>\n\t\t\t<h3>模式三：密码式（Password）</h3>\n\t\t\t<p class=\"pst\">注解在 OAuth2-Client 端，输入用户名和密码获取 Access-Token，此模式只适用于高度信任的客户端</p>\n\t\t\t\n\t\t\t<button onclick=\"buildPasswordUrl()\">构建授权地址</button>\n\t\t\t&emsp;账号：<input class=\"in-cfg\" name=\"username\">\n\t\t\t&emsp;密码：<input class=\"in-cfg\" name=\"password\">\n\t\t\t<textarea class=\"password-url\"></textarea>\n\t\t\t<button onclick=\"ajaxPasswordUrl()\">→ 请求上述地址，获取 Access-Token 数据</button> 请求结果显示如下：\n\t\t\t<textarea class=\"password-result\"></textarea> \n\t\t\t\n\t\t\t<br><br>\n\t\t\t<h3>模式四：凭证式（Client Credentials）</h3>\n\t\t\t<p class=\"pst\">以上三种模式获取的都是用户的 Access-Token，代表用户对第三方应用的授权，在OAuth2.0中还有一种针对 Client级别的授权，\n\t\t\t\t即：Client-Token，代表应用自身的资源授权</p>\n\t\t\t\n\t\t\t<button onclick=\"buildClientTokenUrl()\">构建 Client-Token 授权地址</button>\n\t\t\t<textarea class=\"client-token-url\"></textarea>\n\t\t\t<button onclick=\"ajaxClientTokenUrl()\">→ 请求上述地址，获取 Access-Token 数据</button> 请求结果显示如下：\n\t\t\t<textarea class=\"client-token-result\"></textarea> \n\t\t\t\n\t\t\t\n\t\t\t<br><br>\n\t\t\t<span>更多资料请参考 Sa-Token 官方文档地址：</span>\n\t\t\t<a href=\"https://sa-token.cc/\">https://sa-token.cc/</a>\n\t\t\t\n\t\t\t<div style=\"height: 200px;\"></div>\n\t\t</div>\n\t\t<script src=\"https://unpkg.com/jquery@3.4.1/dist/jquery.min.js\"></script>\n\t\t<script src=\"https://www.layuicdn.com/layer-v3.1.1/layer.js\"></script>\n\t\t<script>window.jQuery || alert('当前页面CDN服务商已宕机，请将所有js包更换为本地依赖')</script>\n\t\t<!-- 配置缓存读取 -->\n\t\t<script>\n\t\t\t// 缓存前缀 \n\t\t\tvar prefix = \"IN_CFG_\";\n\t\t\tfunction getLocalCfg(key) {\n\t\t\t\treturn localStorage.getItem(prefix + key);\n\t\t\t}\n\t\t\tfunction setLocalCfg(key, value) {\n\t\t\t\tlocalStorage.setItem(prefix + key, value);\n\t\t\t}\n\t\t\t\n\t\t\t// 全局配置变动时，存储到本地 \n\t\t\t$('.in-cfg').bind('input propertychange', function(){\n\t\t\t\tvar name = $(this).attr('name');\n\t\t\t\tvar value = $(this).val();\n\t\t\t\tsetLocalCfg(name, value);\n\t\t\t})\n\t\t\t\n\t\t\t// 默认配置 \n\t\t\tvar defaultCfg = {\n\t\t\t\toauth2_server_url: 'http://sa-oauth-server.com:8000',  // OAuth2 服务端主机地址 \n\t\t\t\toauth2_server_auth_url: 'http://sa-oauth-server.com:8000/oauth2/authorize', // OAuth2 授权页地址 \n\t\t\t\toauth2_server_token_url: 'http://sa-oauth-server.com:8000/oauth2/token', // OAuth2 获取 token 地址 \n\t\t\t\toauth2_server_refresh_token_url: 'http://sa-oauth-server.com:8000/oauth2/refresh', // OAuth2 刷新 token 地址 \n\t\t\t\toauth2_server_userinfo_url: 'http://sa-oauth-server.com:8000/oauth2/userinfo', // OAuth2 获取 userinfo 地址 \n\t\t\t\tclient_id: '1001',\n\t\t\t\tclient_secret: 'aaaa-bbbb-cccc-dddd-eeee',\n\t\t\t\tredirect_uri: location.href.split('?')[0].split('#')[0],\n\t\t\t\tscope: 'userinfo,userid,openid,unionid,oidc',\n\t\t\t\tusername: 'sa',\n\t\t\t\tpassword: '123456'\n\t\t\t}\n\t\t\t\n\t\t\t// 打开页面时，加载本地缓存数据，本地缓存无数据时加载默认配置 \n\t\t\tfunction readLocalCfg() {\n\t\t\t\t$('.in-cfg').each(function(){\n\t\t\t\t\tvar name = $(this).attr('name');\n\t\t\t\t\tvar value = getLocalCfg(name) || defaultCfg[name];\n\t\t\t\t\t$(this).val(value);\n\t\t\t\t})\n\t\t\t}\n\t\t\treadLocalCfg();\n\t\t\t\n\t\t\t// 清空配置 \n\t\t\tfunction clearLocalCfg() {\n\t\t\t\t$('.in-cfg').each(function(){\n\t\t\t\t\t$(this).val('');\n\t\t\t\t\tsetLocalCfg($(this).attr('name'), '');\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 将所有配置保存到本地缓存 \n\t\t\tfunction saveAllCfgToLocal() {\n\t\t\t\t$('.in-cfg').each(function(){\n\t\t\t\t\tsetLocalCfg($(this).attr('name'), $(this).val());\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 根据主机 URL 一键拼接授权页等地址  \n\t\t\tfunction autoSplicingUrl() {\n\t\t\t\tvar oauth2_server_url = $('[name=oauth2_server_url]').val();\n\t\t\t\tif(!oauth2_server_url) {\n\t\t\t\t\treturn layer.alert('请先配置 OAuth2 Server 主机地址！')\n\t\t\t\t}\n\t\t\t\t$('[name=oauth2_server_auth_url]').val(oauth2_server_url + '/oauth2/authorize');\n\t\t\t\t$('[name=oauth2_server_token_url]').val(oauth2_server_url + '/oauth2/token');\n\t\t\t\t$('[name=oauth2_server_refresh_token_url]').val(oauth2_server_url + '/oauth2/refresh');\n\t\t\t\t$('[name=oauth2_server_userinfo_url]').val(oauth2_server_url + '/oauth2/userinfo');\n\t\t\t\tsaveAllCfgToLocal();\n\t\t\t\tlayer.msg('已自动拼接：OAuth2 Server 授权页地址、获取 token 地址、刷新 token 地址、获取 userinfo 地址');\n\t\t\t}\n\t\t\t\n\t\t\t// 一键填写 Gitee 参数样例\n\t\t\tfunction autoFillGiteeUrl() {\n\t\t\t\t$('[name=oauth2_server_url]').val('https://gitee.com')\n\t\t\t\t$('[name=oauth2_server_auth_url]').val('https://gitee.com/oauth/authorize');\n\t\t\t\t$('[name=oauth2_server_token_url]').val('https://gitee.com/oauth/token');\n\t\t\t\t$('[name=oauth2_server_refresh_token_url]').val('https://gitee.com/oauth/token');\n\t\t\t\t$('[name=oauth2_server_userinfo_url]').val('https://gitee.com/api/v5/user');\n\t\t\t\t$('[name=client_id]').val('<待填写>');\n\t\t\t\t$('[name=client_secret]').val('<待填写>');\n\t\t\t\t$('[name=redirect_uri]').val(defaultCfg.redirect_uri);\n\t\t\t\t$('[name=scope]').val('user_info');\n\t\t\t\tsaveAllCfgToLocal();\n\t\t\t\tlayer.msg('填写成功');\n\t\t\t}\n\t\t\t\n\t\t\t// 一键填写 Sa-Sso-Max 参数样例，参考：http://sa-pro.dev33.cn/\n\t\t\tfunction autoFillSaSsoMaxUrl() {\n\t\t\t\t$('[name=oauth2_server_url]').val('http://sspx-server.dev33.cn')\n\t\t\t\t$('[name=oauth2_server_auth_url]').val('http://sspx-center.dev33.cn/oauth2/authorize');\n\t\t\t\t$('[name=oauth2_server_token_url]').val('http://sspx-server.dev33.cn/oauth2/token');\n\t\t\t\t$('[name=oauth2_server_refresh_token_url]').val('http://sspx-server.dev33.cn/oauth2/refresh');\n\t\t\t\t$('[name=oauth2_server_userinfo_url]').val('http://sspx-server.dev33.cn/oauth2/userinfo');\n\t\t\t\t$('[name=client_id]').val('100001');\n\t\t\t\t$('[name=client_secret]').val('CQ0Nf1LmaYq7Ads8EdmKMtEnZmTVIicAEl2trBi0zVKufmOVY5G5Tu2epfu4');\n\t\t\t\t$('[name=redirect_uri]').val(defaultCfg.redirect_uri);\n\t\t\t\t$('[name=scope]').val('userinfo,openid,unionid');\n\t\t\t\tsaveAllCfgToLocal();\n\t\t\t\tlayer.msg('填写成功');\n\t\t\t}\n\t\t\t\n\t\t</script>\n\t\t<!-- 工具方法 -->\n\t\t<script>\n\t\t\t// 从url中查询到指定名称的参数值 \n\t\t\tfunction getParam(name, defaultValue){\n\t\t\t\tvar query = window.location.search.substring(1);\n\t\t\t\tvar vars = query.split(\"&\");\n\t\t\t\tfor (var i=0;i<vars.length;i++) {\n\t\t\t\t\tvar pair = vars[i].split(\"=\");\n\t\t\t\t\tif(pair[0] == name){return pair[1];}\n\t\t\t\t}\n\t\t\t\treturn(defaultValue == undefined ? null : defaultValue);\n\t\t\t}\n\t\t\t\n\t\t\t// 从url中查询到指定名称的锚参数值 \n\t\t\tfunction getSharpParam(name, defaultValue){\n\t\t\t\tvar query = window.location.hash.substring(1);\n\t\t\t\tvar vars = query.split(\"&\");\n\t\t\t\tfor (var i=0;i<vars.length;i++) {\n\t\t\t\t\tvar pair = vars[i].split(\"=\");\n\t\t\t\t\tif(pair[0] == name){return pair[1];}\n\t\t\t\t}\n\t\t\t\treturn(defaultValue == undefined ? null : defaultValue);\n\t\t\t}\n\t\t\t\t\n\t\t\tvar sa = {};\n\t\t\t\n\t\t\t// 打开loading\n\t\t\tsa.loading = function(msg) {\n\t\t\t\tif(window.layer) {\n\t\t\t\t\tlayer.closeAll();\t// 开始前先把所有弹窗关了\n\t\t\t\t\tlayer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });\n\t\t\t\t}\n\t\t\t};\n\t\t\t\n\t\t\t// 隐藏loading\n\t\t\tsa.hideLoading = function() {\n\t\t\t\tif(window.layer) {\n\t\t\t\t\tlayer.closeAll();\n\t\t\t\t}\n\t\t\t};\n\t\t\t\n\t\t\t// 封装一下Ajax\n\t\t\tsa.ajax = function(url, data, successFn, cfg) {\n\t\t\t\tcfg = cfg || {};\n\t\t\t\tsa.loading(\"正在努力加载...\");\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t$.ajax({\n\t\t\t\t\t\turl: url,\n\t\t\t\t\t\ttype: cfg.method || \"post\",\n\t\t\t\t\t\tdata: data,\n\t\t\t\t\t\tdataType: 'json',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t\t\t\t\t'satoken': localStorage.getItem('satoken')\n\t\t\t\t\t\t},\n\t\t\t\t\t\tsuccess: function(res){\n\t\t\t\t\t\t\tconsole.log('返回数据：', res);\n\t\t\t\t\t\t\tsa.hideLoading();\n\t\t\t\t\t\t\tsuccessFn(res);\n\t\t\t\t\t\t},\n\t\t\t\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\t\t\t\tsa.hideLoading();\n\t\t\t\t\t\t\tif(xhr.status == 0){\n\t\t\t\t\t\t\t\treturn layer.alert('无法连接到服务器，请检查网络');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}, 400);\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\t// 从url中查询到指定名称的参数值\n\t\t\tfunction getParam(name, defaultValue){\n\t\t\t\tvar query = window.location.search.substring(1);\n\t\t\t\tvar vars = query.split(\"&\");\n\t\t\t\tfor (var i=0;i<vars.length;i++) {\n\t\t\t\t\tvar pair = vars[i].split(\"=\");\n\t\t\t\t\tif(pair[0] == name){return pair[1];}\n\t\t\t\t}\n\t\t\t\treturn(defaultValue == undefined ? null : defaultValue);\n\t\t\t}\n\n\t\t\t// 监听 textarea 内容变化时，高度随之变化 \n\t\t\t$('textarea').keyup(function(){\n\t\t\t\tvar textarea = this;\n\t\t\t\ttextarea.style.height = 'auto'; // 去除之前的高度限制 \n\t\t\t\ttextarea.style.height = textarea.scrollHeight + 14 + 'px'; \n\t\t\t})\n\t\t\t\n\t\t\t// 重设指定 textarea 的高度\n\t\t\tfunction resizeTextarea(selected){\n\t\t\t\t$(selected).each(function(){\n\t\t\t\t\tvar textarea = this;\n\t\t\t\t\ttextarea.style.height = 'auto'; // 去除之前的高度限制 \n\t\t\t\t\ttextarea.style.height = textarea.scrollHeight + 14 + 'px'; \n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 重设所有 textarea 的高度 \n\t\t\tfunction resizeAllTextarea(){\n\t\t\t\t$('textarea').each(function(){\n\t\t\t\t\tvar textarea = this;\n\t\t\t\t\ttextarea.style.height = 'auto'; // 去除之前的高度限制 \n\t\t\t\t\ttextarea.style.height = textarea.scrollHeight + 14 + 'px'; \n\t\t\t\t})\n\t\t\t}\n\n\n\t\t\t\n\t\t</script>\n\t\t\n\t\t<script type=\"text/javascript\">\n\t\t\t\n\t\t\t// --------------------- 模式一 ---------------------\n\t\t\t\n\t\t\t// 构建授权码地址 \n\t\t\tfunction buildAuthorizationCodeUrl() {\n\t\t\t\tvar url = $('[name=oauth2_server_auth_url]').val()\n\t\t\t\t\t+ '?response_type=code'\n\t\t\t\t\t+ '&client_id=' + $('[name=client_id]').val()\n\t\t\t\t\t+ '&redirect_uri=' + $('[name=redirect_uri]').val()\n\t\t\t\t\t+ '&scope=' + $('[name=scope]').val()\n\t\t\t\t$('.auth-code-url').val(url);\n\t\t\t\tresizeTextarea('.auth-code-url');\n\t\t\t}\n\t\t\t\n\t\t\t// 跳转到授权码授权地址 \n\t\t\tfunction jumpAuthCodeUrl() {\n\t\t\t\tvar url = $('.auth-code-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tlocation.href = url;\n\t\t\t}\n\t\t\t\n\t\t\t// 默认尝试读取一下 code \n\t\t\tvar code = getParam('code');\n\t\t\tif(code) {\n\t\t\t\t$('.show-url-code').text(code);\n\t\t\t}\n\t\t\t\n\t\t\t// 构建 code 换 token 地址 \n\t\t\tfunction buildCodeTakeTokenUrl() {\n\t\t\t\tvar code = getParam('code');\n\t\t\t\tif(!code) {\n\t\t\t\t\treturn layer.msg('未能获取到 code 参数，请先点击上方的授权地址获取 code ');\n\t\t\t\t}\n\t\t\t\t// var url = $('[name=oauth2_server_url]').val() + '/oauth2/token'\n\t\t\t\tvar url = $('[name=oauth2_server_token_url]').val()\n\t\t\t\t\t+ '?grant_type=authorization_code'\n\t\t\t\t\t+ '&client_id=' + $('[name=client_id]').val()\n\t\t\t\t\t+ '&client_secret=' + $('[name=client_secret]').val()\n\t\t\t\t\t+ '&redirect_uri=' + $('[name=redirect_uri]').val()\n\t\t\t\t\t+ '&code=' + code\n\t\t\t\t$('.code-take-token-url').val(url);\n\t\t\t\tresizeTextarea('.code-take-token-url');\n\t\t\t}\n\t\t\t\n\t\t\t// code 换 Access-Token\n\t\t\tfunction ajaxCodeToAccessToken() {\n\t\t\t\tvar url = $('.code-take-token-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tsa.ajax(url, {}, function(res){\n\t\t\t\t\tif(res.access_token) {\n\t\t\t\t\t\t$('[name=access-token-input]').val(res.access_token);\n\t\t\t\t\t}\n\t\t\t\t\tif(res.refresh_token) {\n\t\t\t\t\t\t$('[name=refresh-token-input]').val(res.refresh_token);\n\t\t\t\t\t}\n\t\t\t\t\tvar jsonStr = JSON.stringify(res, null, '\\t');\n\t\t\t\t\t$('.code-take-token-result').val(jsonStr);\n\t\t\t\t\tresizeTextarea('.code-take-token-result');\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// --------- 刷新令牌\n\t\t\t\n\t\t\t// 构建 Tefresh-Token 刷新 Access-Token 地址 \n\t\t\tfunction buildRefreshTokenUrl() {\n\t\t\t\tvar refresh_token = $('[name=refresh-token-input]').val();\n\t\t\t\tif(!refresh_token) {\n\t\t\t\t\treturn layer.msg('未能获取到 refresh_token 参数，请先点击上方的授权地址获取 refresh_token ');\n\t\t\t\t}\n\t\t\t\t// var url = $('[name=oauth2_server_url]').val() + '/oauth2/refresh'\n\t\t\t\tvar url = $('[name=oauth2_server_refresh_token_url]').val()\n\t\t\t\t\t+ '?grant_type=refresh_token'\n\t\t\t\t\t+ '&client_id=' + $('[name=client_id]').val()\n\t\t\t\t\t+ '&client_secret=' + $('[name=client_secret]').val()\n\t\t\t\t\t+ '&refresh_token=' + refresh_token\n\t\t\t\t$('.refresh-token-url').val(url);\n\t\t\t\tresizeTextarea('.refresh-token-url');\n\t\t\t}\n\t\t\t\n\t\t\t// 使用 Tefresh-Token 刷新 Access-Token\n\t\t\tfunction ajaxRefreshToken() {\n\t\t\t\tvar url = $('.refresh-token-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tsa.ajax(url, {}, function(res){\n\t\t\t\t\tif(res.access_token) {\n\t\t\t\t\t\t$('[name=access-token-input]').val(res.access_token);\n\t\t\t\t\t}\n\t\t\t\t\tif(res.refresh_token) {\n\t\t\t\t\t\t$('[name=refresh-token-input]').val(res.refresh_token);\n\t\t\t\t\t}\n\t\t\t\t\tvar jsonStr = JSON.stringify(res, null, '\\t');\n\t\t\t\t\t$('.refresh-token-result').val(jsonStr);\n\t\t\t\t\tresizeTextarea('.refresh-token-result');\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// --------- 用户资料\n\t\t\t\n\t\t\t// 构建 Access-Token 获取 Userinfo 地址\n\t\t\tfunction buildUserinfoUrl() {\n\t\t\t\tvar access_token = $('[name=access-token-input]').val();\n\t\t\t\tif(!access_token) {\n\t\t\t\t\treturn layer.msg('未能获取到 access_token 参数，请先点击上方的授权地址获取 access_token ');\n\t\t\t\t}\n\t\t\t\t// var url = $('[name=oauth2_server_url]').val() + '/oauth2/userinfo'\n\t\t\t\tvar url = $('[name=oauth2_server_userinfo_url]').val()\n\t\t\t\t\t+ '?access_token=' + access_token\n\t\t\t\t$('.userinfo-url').val(url);\n\t\t\t\tresizeTextarea('.userinfo-url');\n\t\t\t}\n\t\t\t\n\t\t\t// 使用 Access-Token 获取 Userinfo\n\t\t\tfunction ajaxUserinfoUrl() {\n\t\t\t\tvar url = $('.userinfo-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tvar method = $('[name=userinfo-ajax-method]').val();\n\t\t\t\tsa.ajax(url, {}, function(res){\n\t\t\t\t\tvar jsonStr = JSON.stringify(res, null, '\\t');\n\t\t\t\t\t$('.userinfo-result').val(jsonStr);\n\t\t\t\t\tresizeTextarea('.userinfo-result');\n\t\t\t\t}, {method: method})\n\t\t\t}\n\t\t\t\n\t\t\t// --------- 回收令牌\n\t\t\t\n\t\t\t// 构建回收 Access-Token 地址\n\t\t\tfunction buildRevokeTokenUrl() {\n\t\t\t\tvar access_token = $('[name=access-token-input]').val();\n\t\t\t\tif(!access_token) {\n\t\t\t\t\treturn layer.msg('未能获取到 access_token 参数，请先点击上方的授权地址获取 access_token ');\n\t\t\t\t}\n\t\t\t\tvar url = $('[name=oauth2_server_url]').val() + '/oauth2/revoke'\n\t\t\t\t\t+ '?client_id=' + $('[name=client_id]').val()\n\t\t\t\t\t+ '&client_secret=' + $('[name=client_secret]').val()\n\t\t\t\t\t+ '&access_token=' + access_token\n\t\t\t\t$('.revoke-token-url').val(url);\n\t\t\t\tresizeTextarea('.revoke-token-url');\n\t\t\t}\n\t\t\t\n\t\t\t// 回收 Access-Token  \n\t\t\tfunction ajaxRevokeTokenUrl() {\n\t\t\t\tvar url = $('.revoke-token-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tsa.ajax(url, {}, function(res){\n\t\t\t\t\tvar jsonStr = JSON.stringify(res, null, '\\t');\n\t\t\t\t\t$('.revoke-token-result').val(jsonStr);\n\t\t\t\t\tresizeTextarea('.revoke-token-result');\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\t// --------------------- 模式二 ---------------------\n\t\t\t\n\t\t\t// 构建隐藏式 Implicit 地址 \n\t\t\tfunction buildImplicitUrl() {\n\t\t\t\tvar url = $('[name=oauth2_server_auth_url]').val()\n\t\t\t\t\t+ '?response_type=token'\n\t\t\t\t\t+ '&client_id=' + $('[name=client_id]').val()\n\t\t\t\t\t+ '&redirect_uri=' + $('[name=redirect_uri]').val()\n\t\t\t\t\t+ '&scope=' + $('[name=scope]').val()\n\t\t\t\t$('.implicit-url').val(url);\n\t\t\t\tresizeTextarea('.implicit-url');\n\t\t\t}\n\t\t\t\n\t\t\t// 跳转到 Implicit 授权地址 \n\t\t\tfunction jumpImplicitUrl() {\n\t\t\t\tvar url = $('.implicit-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tlocation.href = url;\n\t\t\t}\n\t\t\t\n\t\t\t// 默认尝试读取一下 Access-Token \n\t\t\tvar accessToken = getSharpParam('token');\n\t\t\tif(accessToken) {\n\t\t\t\t$('.show-url-access-token').text(accessToken);\n\t\t\t\t$('[name=access-token-input]').val(accessToken);\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\t// --------------------- 模式三 ---------------------\n\t\t\t\n\t\t\t// 构建密码 Password 授权地址 \n\t\t\tfunction buildPasswordUrl() {\n\t\t\t\tvar url = $('[name=oauth2_server_url]').val() + '/oauth2/token'\n\t\t\t\t\t+ '?grant_type=password'\n\t\t\t\t\t+ '&client_id=' + $('[name=client_id]').val()\n\t\t\t\t\t+ '&client_secret=' + $('[name=client_secret]').val()\n\t\t\t\t\t+ '&username=' + $('[name=username]').val()\n\t\t\t\t\t+ '&password=' + $('[name=password]').val()\n\t\t\t\t\t+ '&scope=' + $('[name=scope]').val()\n\t\t\t\t$('.password-url').val(url);\n\t\t\t\tresizeTextarea('.password-url');\n\t\t\t}\n\t\t\t\n\t\t\t// 请求密码式 Password 授权地址 \n\t\t\tfunction ajaxPasswordUrl() {\n\t\t\t\tvar url = $('.password-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tsa.ajax(url, {}, function(res){\n\t\t\t\t\tif(res.access_token) {\n\t\t\t\t\t\t$('[name=access-token-input]').val(res.access_token);\n\t\t\t\t\t}\n\t\t\t\t\tif(res.refresh_token) {\n\t\t\t\t\t\t$('[name=refresh-token-input]').val(res.refresh_token);\n\t\t\t\t\t}\n\t\t\t\t\tvar jsonStr = JSON.stringify(res, null, '\\t');\n\t\t\t\t\t$('.password-result').val(jsonStr);\n\t\t\t\t\tresizeTextarea('.password-result');\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\t// --------------------- 模式四 ---------------------\n\t\t\t\n\t\t\t// 构建密码 Client-Token 授权地址 \n\t\t\tfunction buildClientTokenUrl() {\n\t\t\t\tvar url = $('[name=oauth2_server_url]').val() + '/oauth2/client_token'\n\t\t\t\t\t+ '?grant_type=client_credentials'\n\t\t\t\t\t+ '&client_id=' + $('[name=client_id]').val()\n\t\t\t\t\t+ '&client_secret=' + $('[name=client_secret]').val()\n\t\t\t\t\t+ '&scope=' + $('[name=scope]').val()\n\t\t\t\t$('.client-token-url').val(url);\n\t\t\t\tresizeTextarea('.client-token-url');\n\t\t\t}\n\t\t\t\n\t\t\t// 请求 Client-Token 授权地址 \n\t\t\tfunction ajaxClientTokenUrl() {\n\t\t\t\tvar url = $('.client-token-url').val();\n\t\t\t\tif(!url) {\n\t\t\t\t\treturn layer.msg('请先构建地址');\n\t\t\t\t}\n\t\t\t\tsa.ajax(url, {}, function(res){\n\t\t\t\t\tvar jsonStr = JSON.stringify(res, null, '\\t');\n\t\t\t\t\t$('.client-token-result').val(jsonStr);\n\t\t\t\t\tresizeTextarea('.client-token-result');\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>com.pj</groupId>\n\t<artifactId>sa-token-demo-oauth2-server</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t</parent>\n\n\t<!-- 指定一些属性 -->\n\t<properties> \n\t\t<java.version>1.8</java.version>\n\t\t<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>\n\t\t<!-- 定义 Sa-Token 版本号 -->\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\t\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t    <groupId>cn.dev33</groupId>\n\t\t    <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token-OAuth2.0 模块 -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-oauth2</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token整合Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- thymeleaf 视图引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-thymeleaf</artifactId>\n\t\t</dependency>\n\n\t\t<!-- sa-token-jwt 签发 OIDC id_token 令牌 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jwt</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 热刷新 -->\n\t\t<!--<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>-->\n        \n\t\t<!-- ConfigurationProperties -->\n        <dependency>\n        \t<groupId>org.springframework.boot</groupId>\n        \t<artifactId>spring-boot-configuration-processor</artifactId>\n        \t<optional>true</optional>\n        </dependency>\n        \n\t</dependencies>\n\n\n\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/SaOAuth2ServerApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * 启动：Sa-OAuth2 Server端\n * @author click33\n */\n@SpringBootApplication\npublic class SaOAuth2ServerApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaOAuth2ServerApplication.class, args);\n\t\tSystem.out.println(\"\\nSa-Token-OAuth2 Server端启动成功，配置如下：\");\n\t\tSystem.out.println(SaOAuth2Manager.getServerConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/mock/SaClientMockDao.java",
    "content": "package com.pj.mock;\n\nimport cn.dev33.satoken.oauth2.consts.GrantType;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport org.springframework.stereotype.Component;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * SaClientModel 模拟查询操作\n *\n * @author click33\n * @since 2024/11/15\n */\n@Component\npublic class SaClientMockDao {\n\n    public List<SaClientModel> list;\n\n    /**\n     * 构造方法，添加三个模拟应用\n     */\n    public void init(){\n        list = new ArrayList<>();\n\n        // 模拟应用1\n        SaClientModel client1 = new SaClientModel()\n                .setClientId(\"1001\")    // client id\n                .setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")    // client 秘钥\n                .addAllowRedirectUris(\"*\")    // 所有允许授权的 url\n                .addContractScopes(\"openid\", \"unionid\", \"userid\", \"userinfo\", \"oidc\")    // 所有签约的权限\n                .setSubjectId(\"1000001\")   // 主体 id (可选)\n                .addAllowGrantTypes(     // 所有允许的授权模式\n                        GrantType.authorization_code, // 授权码式\n                        GrantType.implicit,  // 隐藏式\n                        GrantType.refresh_token,  // 刷新令牌\n                        GrantType.password,  // 密码式\n                        GrantType.client_credentials,  // 客户端模式\n                        \"phone_code\"  // 自定义授权模式 手机号验证码登录\n                );\n        list.add(client1);\n\n        // 模拟应用2\n        SaClientModel client2 = new SaClientModel()\n                .setClientId(\"1002\")\n                .setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")\n                .addAllowRedirectUris(\"*\")\n                .addContractScopes(\"openid\", \"unionid\", \"userid\", \"userinfo\", \"oidc\")\n                .setSubjectId(\"1000001\")   // 主体 id (可选)\n                .addAllowGrantTypes(\n                        GrantType.authorization_code,\n                        GrantType.implicit,\n                        GrantType.refresh_token,\n                        GrantType.password,\n                        GrantType.client_credentials\n                );\n        list.add(client2);\n\n        // 模拟应用3\n        SaClientModel client3 = new SaClientModel()\n                .setClientId(\"1003\")\n                .setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")\n                .addAllowRedirectUris(\"*\")\n                .addContractScopes(\"openid\", \"unionid\", \"userid\", \"userinfo\", \"oidc\")\n                .addAllowGrantTypes(\n                        GrantType.authorization_code,\n                        GrantType.implicit,\n                        GrantType.refresh_token,\n                        GrantType.password,\n                        GrantType.client_credentials\n                );\n        list.add(client3);\n    }\n\n    /**\n     * 根据应用 id 查找对应的应用，找不到则返回 null\n     * @param clientId 应用 id\n     * @return 应用对象\n     */\n    public SaClientModel getClientModel(String clientId) {\n        if(list == null) {\n            init();\n        }\n        return list.stream()\n                .filter(e -> e.getClientId().equals(clientId))\n                .findFirst()\n                .orElse(null);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java",
    "content": "package com.pj.oauth2;\n\nimport cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport com.pj.mock.SaClientMockDao;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Component;\n\n/**\n * Sa-Token OAuth2：自定义数据加载器\n *\n * @author click33\n */\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\n\t@Autowired\n\tSaClientMockDao saClientMockDao;\n\n\t// 根据 clientId 获取 Client 信息\n\t@Override\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\t// 此为模拟数据，真实环境需要从数据库查询\n\t\treturn saClientMockDao.getClientModel(clientId);\n\t}\n\t\n\t// 根据 clientId 和 loginId 获取 openid\n\t@Override\n\tpublic String getOpenid(String clientId, Object loginId) {\n\t\t// 此处使用框架默认算法生成 openid，真实项目建议改为从数据库查询\n\t\treturn SaOAuth2DataLoader.super.getOpenid(clientId, loginId);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ResourcesController.java",
    "content": "package com.pj.oauth2;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Util;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token OAuth2 Resources 端 Controller\n *\n * <p> Resources 端：OAuth2 资源端，允许 Client 端根据 Access-Token 置换相关资源 </p>\n *\n * <p> 在 OAuth2 中，认证端和资源端：\n *  1、可以在一个 Controller 中，也可以在不同的 Controller 中\n *  2、可以在同一个项目中，也可以在不同的项目中（在不同项目中时需要两端连同一个 Redis ）\n * </p>\n *\n * @author click33\n * @since 2024/12/6\n */\n@RestController\npublic class SaOAuth2ResourcesController {\n\n    // 示例：获取 userinfo 信息：昵称、头像、性别等等\n    @RequestMapping(\"/oauth2/userinfo\")\n    public SaResult userinfo() {\n        // 获取 Access-Token 对应的账号id\n        String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());\n        Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);\n        System.out.println(\"-------- 此Access-Token对应的账号id: \" + loginId);\n\n        // 校验 Access-Token 是否具有权限: userinfo\n        SaOAuth2Util.checkAccessTokenScope(accessToken, \"userinfo\");\n\n        // 模拟账号信息 （真实环境需要查询数据库获取信息）\n        Map<String, Object> map = new LinkedHashMap<>();\n        // map.put(\"userId\", loginId);  一般原则下，oauth2-server 不能把 userId 返回给 oauth2-client\n        map.put(\"nickname\", \"林小林\");\n        map.put(\"avatar\", \"http://xxx.com/1.jpg\");\n        map.put(\"age\", \"18\");\n        map.put(\"sex\", \"男\");\n        map.put(\"address\", \"山东省 青岛市 城阳区\");\n        return SaResult.ok().setMap(map);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java",
    "content": "package com.pj.oauth2;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.ModelAndView;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token-OAuth2 Server 认证端 Controller\n *\n * @author click33\n */\n@RestController\npublic class SaOAuth2ServerController {\n\n\t// OAuth2-Server 端：处理所有 OAuth2 相关请求\n\t@RequestMapping(\"/oauth2/*\")\n\tpublic Object request() {\n\t\tSystem.out.println(\"------- 进入请求: \" + SaHolder.getRequest().getUrl());\n\t\treturn SaOAuth2ServerProcessor.instance.dister();\n\t}\n\n\t// Sa-Token OAuth2 定制化配置\n\t@Autowired\n\tpublic void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t\t// 未登录的视图\n\t\tSaOAuth2Strategy.instance.notLoginView = ()->{\n\t\t\treturn new ModelAndView(\"login.html\");\n\t\t};\n\n\t\t// 登录处理函数\n\t\tSaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> {\n\t\t\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\tStpUtil.login(10001);\n\t\t\t\treturn SaResult.ok().set(\"satoken\", StpUtil.getTokenValue());\n\t\t\t}\n\t\t\treturn SaResult.error(\"账号名或密码错误\");\n\t\t};\n\n\t\t// 授权确认视图\n\t\tSaOAuth2Strategy.instance.confirmView = (clientId, scopes)->{\n\t\t\tMap<String, Object> map = new HashMap<>();\n\t\t\tmap.put(\"clientId\", clientId);\n\t\t\tmap.put(\"scope\", scopes);\n\t\t\treturn new ModelAndView(\"confirm.html\", map);\n\t\t};\n\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/CustomPasswordGrantTypeHandler.java",
    "content": "//package com.pj.oauth2.custom_grant_type;\n//\n//import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\n//import cn.dev33.satoken.oauth2.granttype.handler.PasswordGrantTypeHandler;\n//import cn.dev33.satoken.oauth2.granttype.handler.model.PasswordAuthResult;\n//import org.springframework.stereotype.Component;\n//\n///**\n// * 自定义 Password Grant_Type 授权模式处理器认证过程\n// *\n// * @author click33\n// * @since 2025/5/11\n// */\n//@Component\n//public class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler {\n//\n//    @Override\n//    public PasswordAuthResult loginByUsernamePassword(String username, String password) {\n//        if(\"sa\".equals(username) && \"123456\".equals(password)) {\n//            long userId = 10001;\n//            return new PasswordAuthResult(userId);\n//        } else {\n//            throw new SaOAuth2Exception(\"无效账号密码\");\n//        }\n//    }\n//\n//}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneCodeGrantTypeHandler.java",
    "content": "//package com.pj.oauth2.custom_grant_type;\n//\n//import cn.dev33.satoken.SaManager;\n//import cn.dev33.satoken.context.model.SaRequest;\n//import cn.dev33.satoken.oauth2.SaOAuth2Manager;\n//import cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\n//import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\n//import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\n//import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface;\n//import org.springframework.stereotype.Component;\n//\n//import java.util.List;\n//\n///**\n// * 自定义 phone_code 授权模式处理器\n// *\n// * @author click33\n// * @since 2024/8/23\n// */\n//@Component\n//public class PhoneCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {\n//\n//    @Override\n//    public String getHandlerGrantType() {\n//        return \"phone_code\";\n//    }\n//\n//    @Override\n//    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {\n//\n//        // 获取前端提交的参数\n//        String phone = req.getParamNotNull(\"phone\");\n//        String code = req.getParamNotNull(\"code\");\n//        String realCode = SaManager.getSaTokenDao().get(\"phone_code:\" + phone);\n//\n//        // 1、校验验证码是否正确\n//        if(!code.equals(realCode)) {\n//            throw new SaOAuth2Exception(\"验证码错误\");\n//        }\n//\n//        // 2、校验通过，删除验证码\n//        SaManager.getSaTokenDao().delete(\"phone_code:\" + phone);\n//\n//        // 3、登录\n//        long userId = 10001; // 模拟 userId，真实项目应该根据手机号从数据库查询\n//\n//        // 4、构建 ra 对象\n//        RequestAuthModel ra = new RequestAuthModel();\n//        ra.clientId = clientId;\n//        ra.loginId = userId;\n//        ra.scopes = scopes;\n//\n//        // 5、生成 Access-Token\n//        AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = \"phone_code\");\n//        return at;\n//    }\n//}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_grant_type/PhoneLoginController.java",
    "content": "//package com.pj.oauth2.custom_grant_type;\n//\n//import cn.dev33.satoken.SaManager;\n//import cn.dev33.satoken.util.SaFoxUtil;\n//import cn.dev33.satoken.util.SaResult;\n//import org.springframework.web.bind.annotation.RequestMapping;\n//import org.springframework.web.bind.annotation.RestController;\n//\n///**\n// * 自定义手机登录接口\n// *\n// * @author click33\n// * @since 2024/8/23\n// */\n//@RestController\n//public class PhoneLoginController {\n//\n//    @RequestMapping(\"/oauth2/sendPhoneCode\")\n//    public SaResult sendCode(String phone) {\n//        String code = SaFoxUtil.getRandomNumber(100000, 999999) + \"\";\n//        SaManager.getSaTokenDao().set(\"phone_code:\" + phone, code, 60 * 5);\n//        System.out.println(\"手机号：\" + phone + \"，验证码：\" + code + \"，已发送成功\");\n//        return SaResult.ok(\"验证码发送成功\");\n//    }\n//\n//}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/CustomOidcScopeHandler.java",
    "content": "//package com.pj.oauth2.custom_scope;\n//\n//import cn.dev33.satoken.oauth2.data.model.oidc.IdTokenModel;\n//import cn.dev33.satoken.oauth2.scope.handler.OidcScopeHandler;\n//import org.springframework.stereotype.Component;\n//\n///**\n// * 扩展 OIDC 权限处理器，返回更多字段\n// *\n// * @author click33\n// * @since 2024/8/24\n// */\n//@Component\n//public class CustomOidcScopeHandler extends OidcScopeHandler {\n//\n//    @Override\n//    public IdTokenModel workExtraData(IdTokenModel idToken) {\n//        Object userId = idToken.sub;\n//        System.out.println(\"----- 为 idToken 追加扩展字段 ----- \");\n//\n//        idToken.extraData.put(\"uid\", userId); // 用户id\n//        idToken.extraData.put(\"nickname\", \"linXiaoLin\"); // 昵称\n//        idToken.extraData.put(\"picture\", \"https://sa-token.cc/logo.png\"); // 头像\n//        idToken.extraData.put(\"email\", \"456456@xx.com\"); // 邮箱\n//        idToken.extraData.put(\"phone_number\", \"13144556677\"); // 手机号\n//\n//        // 更多字段 ...\n//        // 可参考：https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\n//\n//        return idToken;\n//    }\n//\n//}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom_scope/UserinfoScopeHandler.java",
    "content": "//package com.pj.oauth2.custom_scope;\n//\n//import cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\n//import cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\n//import cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface;\n//import org.springframework.stereotype.Component;\n//\n//import java.util.LinkedHashMap;\n//import java.util.Map;\n//\n///**\n// * 自定义 userinfo scope 处理器\n// * @author click33\n// * @since 2024/8/20\n// */\n//@Component\n//public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface {\n//\n//    // 指示当前处理器所要处理的 scope\n//    @Override\n//    public String getHandlerScope() {\n//        return \"userinfo\";\n//    }\n//\n//    // 当构建的 AccessToken 具有此权限时，所需要执行的方法\n//    @Override\n//    public void workAccessToken(AccessTokenModel at) {\n//        System.out.println(\"--------- userinfo 权限，加工 AccessTokenModel --------- \");\n//        // 模拟账号信息 （真实环境需要查询数据库获取信息）\n//        Map<String, Object> map = new LinkedHashMap<String, Object>();\n//        map.put(\"userId\", \"10008\");\n//        map.put(\"nickname\", \"shengzhang_\");\n//        map.put(\"avatar\", \"http://xxx.com/1.jpg\");\n//        map.put(\"age\", \"18\");\n//        map.put(\"sex\", \"男\");\n//        map.put(\"address\", \"山东省 青岛市 城阳区\");\n//        at.extraData.put(\"userinfo\", map);\n//    }\n//\n//    // 当构建的 ClientToken 具有此权限时，所需要执行的方法\n//    @Override\n//    public void workClientToken(ClientTokenModel ct) {\n//    }\n//\n//    // 当使用 RefreshToken 刷新 AccessToken 时，是否重新执行 workAccessToken 构建方法\n//    // 在一些实时性较高的数据中需要指定为 true\n//    @Override\n//    public boolean refreshAccessTokenIsWork() {\n//        return true;\n//    }\n//\n//}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/h5/SaOAuth2ServerH5Controller.java",
    "content": "package com.pj.oauth2.h5;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Template;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口)\n */\n@RestController\npublic class SaOAuth2ServerH5Controller {\n\n    /**\n     * 获取最终授权重定向地址，形如：http://xxx.com/xxx?code=xxxxx\n     *\n     * <p> 情况1：客户端未登录，返回 code=401，提示用户登录 <p/>\n     * <p> 情况2：请求的 scope 需要客户端手动确认授权，返回 code=411，提示用户手动确认 <p/>\n     * <p> 情况3：已登录且请求的 scope 已确认授权，返回 code=200，redirect_uri=最终重定向 url 地址(携带code码参数) <p/>\n     *\n     * @return /\n     */\n    @PostMapping(\"/oauth2/getRedirectUri\")\n    public Object getRedirectUri() {\n\n        // 获取变量\n        SaRequest req = SaHolder.getRequest();\n        SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();\n        SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();\n        SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n        String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type);\n\n        // 1、先判断是否开启了指定的授权模式\n        SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg);\n\n        // 2、如果尚未登录, 则先去登录\n        long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L);\n        if(loginId == 0L) {\n            return SaResult.get(401, \"need login\", null);\n        }\n\n        // 3、构建请求 Model\n        RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);\n\n        // 4、开发者自定义的授权前置检查\n        SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId);\n\n        // 5、校验：重定向域名是否合法\n        oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri);\n\n        // 6、校验：此次申请的Scope，该Client是否已经签约\n        oauth2Template.checkContractScope(ra.clientId, ra.scopes);\n\n        // 7、判断：如果此次申请的Scope，该用户尚未授权，则转到授权页面\n        boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);\n        if(isNeedCarefulConfirm) {\n            SaClientModel cm = oauth2Template.checkClientModel(ra.clientId);\n            if( ! cm.getIsAutoConfirm()) {\n                // code=411，需要用户手动确认授权\n                return SaResult.get(411, \"need confirm\", null);\n            }\n        }\n\n        // 8、判断授权类型，重定向到不同地址\n        // \t\t如果是 授权码式，则：开始重定向授权，下放code\n        if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) {\n            CodeModel codeModel = dataGenerate.generateCode(ra);\n            String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);\n            return SaResult.ok().set(\"redirect_uri\", redirectUri);\n        }\n\n        // \t\t如果是 隐藏式，则：开始重定向授权，下放 token\n        if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) {\n            AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null);\n            String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);\n            return SaResult.ok().set(\"redirect_uri\", redirectUri);\n        }\n\n        // 默认返回\n        throw new SaOAuth2Exception(\"无效 response_type: \" + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/GlobalExceptionHandler.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\n\t/**\n\t * 注册 [Sa-Token 全局过滤器]\n\t */\n\t@Bean\n\tpublic SaServletFilter getSaServletFilter() {\n\t\treturn new SaServletFilter()\n\n\t\t\t\t// 指定 [拦截路由] 与 [放行路由]\n\t\t\t\t.addInclude(\"/**\").addExclude(\"/favicon.ico\")\n\n\t\t\t\t// 认证函数: 每次请求执行\n\t\t\t\t.setAuth(obj -> {\n\t\t\t\t\tSaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n\t\t\t\t\t// ...\n\t\t\t\t})\n\n\t\t\t\t// 异常处理函数：每次认证函数发生异常时执行此函数\n\t\t\t\t.setError(e -> {\n\t\t\t\t\treturn SaResult.error(e.getMessage());\n\t\t\t\t})\n\n\t\t\t\t// 前置函数：在每次认证函数之前执行\n\t\t\t\t.setBeforeAuth(obj -> {\n\t\t\t\t\tSaHolder.getResponse()\n\n\t\t\t\t\t\t\t// ---------- 设置跨域响应头 ----------\n\t\t\t\t\t\t\t// 允许指定域访问跨域资源\n\t\t\t\t\t\t\t.setHeader(\"Access-Control-Allow-Origin\", \"*\")\n\t\t\t\t\t\t\t// 允许所有请求方式\n\t\t\t\t\t\t\t.setHeader(\"Access-Control-Allow-Methods\", \"*\")\n\t\t\t\t\t\t\t// 允许的header参数\n\t\t\t\t\t\t\t.setHeader(\"Access-Control-Allow-Headers\", \"*\")\n\t\t\t\t\t\t\t// 有效时间\n\t\t\t\t\t\t\t.setHeader(\"Access-Control-Max-Age\", \"3600\")\n\t\t\t\t\t;\n\n\t\t\t\t\t// 如果是预检请求，则立即返回到前端\n\t\t\t\t\tSaRouter.match(SaHttpMethod.OPTIONS)\n\t\t\t\t\t\t\t.free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n\t\t\t\t\t\t\t.back();\n\t\t\t\t})\n\t\t\t\t;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/Test2Controller.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Util;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 测试 OAuth2 相关 token 增删查\n *\n * @author click33\n * @since 2024/8/25\n */\n@RestController\n@RequestMapping(\"/test\")\npublic class Test2Controller {\n\n    // 测试：查询全部 Access-Token   --- http://localhost:8000/test/getAccessTokenValueList?clientId=1001&loginId=10001\n    @RequestMapping(\"/getAccessTokenValueList\")\n    public SaResult getAccessTokenValueList(String clientId, long loginId) {\n        List<String> accessTokenValueList = SaOAuth2Util.getAccessTokenValueList(clientId, loginId);\n        return SaResult.data(accessTokenValueList);\n    }\n\n    // 测试：查询全部 Access-Token, 带过期时间   --- http://localhost:8000/test/getAccessTokenIndexMap?clientId=1001&loginId=10001\n    @RequestMapping(\"/getAccessTokenIndexMap\")\n    public SaResult getAccessTokenIndexMap(String clientId, long loginId) {\n        Map<String, Long> accessTokenIndexMap = SaOAuth2Manager.getDao().getAccessTokenIndexMap_FromAdjustAfter(clientId, loginId);\n        return SaResult.data(accessTokenIndexMap);\n    }\n\n    // 测试：回收指定 Access-Token   --- http://localhost:8000/test/revokeAccessToken?access_token=xxxxxxxxxx\n    @RequestMapping(\"/revokeAccessToken\")\n    public SaResult revokeAccessToken(String access_token) {\n        SaOAuth2Util.revokeAccessToken(access_token);\n        return SaResult.ok();\n    }\n\n    // 测试：回收全部 Access-Token   --- http://localhost:8000/test/revokeAccessTokenByIndex?clientId=1001&loginId=10001\n    @RequestMapping(\"/revokeAccessTokenByIndex\")\n    public SaResult revokeAccessTokenByIndex(String clientId, long loginId) {\n        SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId);\n        return SaResult.ok();\n    }\n\n\n    // 测试：查询全部 Refresh-Token   --- http://localhost:8000/test/getRefreshTokenValueList?clientId=1001&loginId=10001\n    @RequestMapping(\"/getRefreshTokenValueList\")\n    public SaResult getRefreshTokenValueList(String clientId, long loginId) {\n        List<String> refreshTokenValueList = SaOAuth2Util.getRefreshTokenValueList(clientId, loginId);\n        return SaResult.data(refreshTokenValueList);\n    }\n\n    // 测试：查询全部 Refresh-Token, 带过期时间   --- http://localhost:8000/test/getRefreshTokenIndexMap?clientId=1001&loginId=10001\n    @RequestMapping(\"/getRefreshTokenIndexMap\")\n    public SaResult getRefreshTokenIndexMap(String clientId, long loginId) {\n        Map<String, Long> refreshTokenIndexMap = SaOAuth2Manager.getDao().getRefreshTokenIndexMap_FromAdjustAfter(clientId, loginId);\n        return SaResult.data(refreshTokenIndexMap);\n    }\n\n    // 测试：回收指定 Refresh-Token   --- http://localhost:8000/test/revokeRefreshToken?refresh_token=xxxxxxxxxx\n    @RequestMapping(\"/revokeRefreshToken\")\n    public SaResult revokeRefreshToken(String refresh_token) {\n        SaOAuth2Util.revokeRefreshToken(refresh_token);\n        return SaResult.ok();\n    }\n\n    // 测试：回收全部 Refresh-Token   --- http://localhost:8000/test/revokeRefreshTokenByIndex?clientId=1001&loginId=10001\n    @RequestMapping(\"/revokeRefreshTokenByIndex\")\n    public SaResult revokeRefreshTokenByIndex(String clientId, long loginId) {\n        SaOAuth2Util.revokeRefreshTokenByIndex(clientId, loginId);\n        return SaResult.ok();\n    }\n\n\n    // 测试：查询全部 Client-Token   --- http://localhost:8000/test/getClientTokenValueList?clientId=1001\n    @RequestMapping(\"/getClientTokenValueList\")\n    public SaResult getClientTokenValueList(String clientId) {\n        List<String> clientTokenValueList = SaOAuth2Util.getClientTokenValueList(clientId);\n        return SaResult.data(clientTokenValueList);\n    }\n\n    // 测试：查询全部 Client-Token, 带过期时间   --- http://localhost:8000/test/getClientTokenIndexMap?clientId=1001&loginId=10001\n    @RequestMapping(\"/getClientTokenIndexMap\")\n    public SaResult getClientTokenIndexMap(String clientId, long loginId) {\n        Map<String, Long> rlientTokenIndexMap = SaOAuth2Manager.getDao().getClientTokenIndexMap_FromAdjustAfter(clientId, loginId);\n        return SaResult.data(rlientTokenIndexMap);\n    }\n\n    // 测试：回收指定 Client-Token   --- http://localhost:8000/test/revokeClientToken?client_token=xxxxxxxxxxx\n    @RequestMapping(\"/revokeClientToken\")\n    public SaResult revokeClientToken(String client_token) {\n        SaOAuth2Util.revokeClientToken(client_token);\n        return SaResult.ok();\n    }\n\n    // 测试：回收全部 Client-Token   --- http://localhost:8000/test/revokeClientTokenByIndex?clientId=1001\n    @RequestMapping(\"/revokeClientTokenByIndex\")\n    public SaResult revokeClientTokenByIndex(String clientId) {\n        SaOAuth2Util.revokeClientTokenByIndex(clientId);\n        return SaResult.ok();\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.oauth2.annotation.SaCheckAccessToken;\nimport cn.dev33.satoken.oauth2.annotation.SaCheckClientIdSecret;\nimport cn.dev33.satoken.oauth2.annotation.SaCheckClientToken;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * OAuth2 相关注解测试 Controller\n *\n * @author click33\n * @since 2024/8/25\n */\n@RestController\n@RequestMapping(\"/test\")\npublic class TestController {\n\n    // 测试：携带有效的 access_token 才可以进入请求\n    // 你可以在请求参数中携带 access_token 参数，或者从请求头以 Authorization: bearer xxx 的形式携带\n    @SaCheckAccessToken\n    @RequestMapping(\"/checkAccessToken\")\n    public SaResult checkAccessToken() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 access_token ，并且具备指定 scope 才可以进入请求\n    @SaCheckAccessToken(scope = \"userinfo\")\n    @RequestMapping(\"/checkAccessTokenScope\")\n    public SaResult checkAccessTokenScope() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 access_token ，并且具备指定 scope 列表才可以进入请求\n    @SaCheckAccessToken(scope = {\"openid\", \"userinfo\"})\n    @RequestMapping(\"/checkAccessTokenScopeList\")\n    public SaResult checkAccessTokenScopeList() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 client_token 才可以进入请求\n    // 你可以在请求参数中携带 client_token 参数，或者从请求头以 Authorization: bearer xxx 的形式携带\n    @SaCheckClientToken\n    @RequestMapping(\"/checkClientToken\")\n    public SaResult checkClientToken() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 client_token ，并且具备指定 scope 才可以进入请求\n    @SaCheckClientToken(scope = \"userinfo\")\n    @RequestMapping(\"/checkClientTokenScope\")\n    public SaResult checkClientTokenScope() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 client_token ，并且具备指定 scope 列表才可以进入请求\n    @SaCheckClientToken(scope = {\"openid\", \"userinfo\"})\n    @RequestMapping(\"/checkClientTokenScopeList\")\n    public SaResult checkClientTokenScopeList() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 client_id 和 client_secret 信息，才可以进入请求\n    // 你可以在请求参数中携带 client_id 和 client_secret 参数，或者从请求头以 Authorization: Basic base64(client_id:client_secret) 的形式携带\n    @SaCheckClientIdSecret\n    @RequestMapping(\"/checkClientIdSecret\")\n    public SaResult checkClientIdSecret() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml",
    "content": "server:\n    port: 8000\n\n# sa-token配置\nsa-token:\n    # token名称 (同时也是 Cookie 名称)\n    token-name: satoken\n    # 是否打印操作日志\n    is-log: true\n    # jwt 秘钥\n    jwt-secret-key: saxsaxsaxsax\n    # OAuth2.0 配置\n    oauth2-server:\n        # 是否全局开启授权码模式\n        enable-authorization-code: true\n        # 是否全局开启 Implicit 模式\n        enable-implicit: true\n        # 是否全局开启密码模式\n        enable-password: true\n        # 是否全局开启客户端模式\n        enable-client-credentials: true\n        # 定义哪些 scope 是高级权限，多个用逗号隔开\n        # higher-scope: openid,userid\n        # 定义哪些 scope 是低级权限，多个用逗号隔开\n        # lower-scope: userinfo\n\nspring:\n    # redis配置\n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        # password:\n        # 连接超时时间（毫秒）\n        timeout: 1000ms\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/confirm.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-OAuth2-认证中心-确认授权页</title>\n\t\t<style type=\"text/css\">\n\t\t\tbody{background-color: #F5F5D5;}\n\t\t\t*{margin: 0px; padding: 0px;}\n\t\t\t.login-box{width: 400px; margin: 20vh auto; padding: 70px; border: 1px #000 solid;}\n\t\t\t.login-box button{padding: 5px 15px; cursor: pointer; }\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t<h2>Sa-OAuth2-认证中心-确认授权页</h2> <br>\n\t\t\t<div>\n\t\t\t\t<div><b>应用ID：</b><span th:utext=\"${clientId}\"></span></div>\n\t\t\t\t<div><b>请求授权：</b><span th:utext=\"${scope}\"></span></div>\n\t\t\t\t<br><div>------------- 是否同意授权 -------------</div><br>\n\t\t\t\t<div>\n\t\t\t\t\t<button onclick=\"yes()\">同意</button>\n\t\t\t\t\t<button onclick=\"no()\">拒绝</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<script src=\"https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js\"></script>\n\t\t<script src=\"https://www.layuicdn.com/layer-v3.1.0/layer.js\"></script>\n\t\t<script>window.jQuery || alert('当前页面CDN服务商已宕机，请将所有js包更换为本地依赖')</script>\n\t\t<script type=\"text/javascript\">\n\t\t\t\n\t\t\t// 同意授权\n\t\t\tfunction yes() {\n\t\t\t\tconsole.log('-----------');\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/oauth2/doConfirm',\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tclient_id: getParam('client_id'),\n\t\t\t\t\t\tscope: getParam('scope'),\n\t\t\t\t\t\t// 以下四个参数必须一起出现\n\t\t\t\t\t\tbuild_redirect_uri: true,\n\t\t\t\t\t\tresponse_type: getParam('response_type'),\n\t\t\t\t\t\tredirect_uri: getParam('redirect_uri'),\n\t\t\t\t\t\tstate: getParam('state'),\n\t\t\t\t\t},\n\t\t\t\t\tdataType: 'json',\n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tconsole.log('res：', res);\n\t\t\t\t\t\tif(res.code === 200) {\n\t\t\t\t\t\t\tlayer.msg('授权成功！');\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tif (res.redirect_uri) {\n\t\t\t\t\t\t\t\t\tlocation.href = res.redirect_uri;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlocation.reload();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}, 800);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// 重定向至授权失败URL \n\t\t\t\t\t\t\tlayer.alert('授权失败:' + res.msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\terror: function(e) {\n\t\t\t\t\t\tconsole.log('error');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t// 拒绝授权\n\t\t\tfunction no() {\n\t\t\t\tvar url = joinParam(getParam('redirect_uri'), \"handle=refuse&msg=用户拒绝了授权\");\n\t\t\t\tlocation.href = url;\n\t\t\t}\n\t\t\t\n\t\t\t// 从url中查询到指定名称的参数值 \n\t\t\tfunction getParam(name, defaultValue){\n\t\t\t\tvar query = window.location.search.substring(1);\n\t\t\t\tvar vars = query.split(\"&\");\n\t\t\t\tfor (var i=0;i<vars.length;i++) {\n\t\t\t\t\tvar pair = vars[i].split(\"=\");\n\t\t\t\t\tif(pair[0] == name){return pair[1];}\n\t\t\t\t}\n\t\t\t\treturn(defaultValue == undefined ? null : defaultValue);\n\t\t\t}\n\t\t\t\n\t\t\t// 在url上拼接上kv参数并返回 \n\t\t\tfunction joinParam(url, parameStr) {\n\t\t\t\tif(parameStr == null || parameStr.length == 0) {\n\t\t\t\t\treturn url;\n\t\t\t\t}\n\t\t\t\tvar index = url.indexOf('?');\n\t\t\t\t// ? 不存在\n\t\t\t\tif(index == -1) {\n\t\t\t\t\treturn url + '?' + parameStr;\n\t\t\t\t}\n\t\t\t\t// ? 是最后一位\n\t\t\t\tif(index == url.length - 1) {\n\t\t\t\t\treturn url + parameStr;\n\t\t\t\t}\n\t\t\t\t// ? 是其中一位\n\t\t\t\tif(index > -1 && index < url.length - 1) {\n\t\t\t\t\t// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &\n\t\t\t\t\tif(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) {\n\t\t\t\t\t\treturn url + '&' + parameStr;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn url + parameStr;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t\t\t\t\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/templates/login.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-OAuth2-认证中心-登录页</title>\n\t\t<style type=\"text/css\">\n\t\t\tbody{background-color: #F5F5D5;}\n\t\t\t*{margin: 0px; padding: 0px;}\n\t\t\t.login-box{width: 400px; margin: 20vh auto;}\n\t\t\t.login-box input{line-height: 25px; margin-bottom: 10px;}\n\t\t\t.login-box button{padding: 5px 15px; cursor: pointer; }\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t<h2>Sa-OAuth2-认证中心-登录页</h2> <br>\n\t\t\t账号：<input name=\"name\" /> <br>\n\t\t\t密码：<input name=\"pwd\" type=\"password\" /> <br>\n\t\t\t<button onclick=\"doLogin()\">登录</button>\n\t\t\t<span style=\"color: #666;\">（测试账号： sa / 123456）</span>\n\t\t</div>\n\t\t<script src=\"https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js\"></script>\n\t\t<script src=\"https://www.layuicdn.com/layer-v3.1.0/layer.js\"></script>\n\t\t<script>window.jQuery || alert('当前页面CDN服务商已宕机，请将所有js包更换为本地依赖')</script>\n\t\t<script type=\"text/javascript\">\n\t\t\t\n\t\t\t// 登录方法 \n\t\t\tfunction doLogin() {\n\t\t\t\tconsole.log('-----------');\n\t\t\t\t$.ajax({\n\t\t\t\t\turl: '/oauth2/doLogin',\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tname: $('[name=name]').val(),\n\t\t\t\t\t\tpwd: $('[name=pwd]').val()\n\t\t\t\t\t},\n\t\t\t\t\tdataType: 'json', \n\t\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t\tif(res.code == 200) {\n\t\t\t\t\t\t\tlayer.msg('登录成功！');\n\t\t\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\t\t\tlocation.reload(true);\n\t\t\t\t\t\t\t}, 800);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlayer.alert(res.msg);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\terror: function(e) {\n\t\t\t\t\t\tconsole.log('error');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css",
    "content": "*{margin: 0; padding: 0;}\nbody{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}\n::-webkit-input-placeholder{color: #ccc;}\n\n/* 视图盒子 */\n.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}\n/* 背景 EAEFF3 */\n.bg-1{height: 100%; background: #E4B17F;}\n\n/* 内容盒子 */\n.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}\n.content-box{display: none;}\n.region-default{display: block;}\n.message-box{width: 100%;; text-align: center;}\n\n/* 登录盒子 */\n/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */\n.login-box{width: 450px; margin: auto; max-width: 90%; height: 100%;}\n.login-box{display: flex; align-items: center; text-align: center;}\n\n/* 表单 */\n.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}\n.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}\n.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}\n\n/* 输入框 */\n.from-item{border: 0px #000 solid; margin-bottom: 15px;}\n.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;}\n.s-input{font-size: 12px;}\n.s-input:focus{border-color: #409eff}\n\n/* 登录按钮 */\n.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}\n.s-btn:hover{background-color: #50aEFF;}\n\n/* 重置按钮 */\n.reset-box{text-align: left; font-size: 12px;}\n.reset-box a{text-decoration: none;}\n.reset-box a:hover{text-decoration: underline;}\n\n/* 确认授权按钮 */\n.confirm-btn{text-indent: 0; cursor: pointer; background-color: #409EFF; border: 1px #409EFF solid; color: #FFF; padding: 5px 15px;}\n.confirm-btn:hover{background-color: #50aEFF;}\n\n/* loading框样式 */\n.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js",
    "content": "// OAuth-Server 后端 接口地址 \nvar baseUrl = \"http://sa-oauth-server.com:8000\";\n\n\n// ----------------------------------- 相关事件 -----------------------------------\n\n// 显示默认区域 \nfunction showDefaultRegion(){\n\t$('.content-box').hide();\n\t$('.region-default').show();\n}\n// 显示登录框区域 \nfunction showLoginRegion(){\n\t$('.content-box').hide();\n\t$('.region-login').show();\n}\n// 显示确认授权框区域 \nfunction showConfirmRegion(){\n\t$('.content-box').hide();\n\t$('.region-confirm').show(); \n\t$('.show-clientId').text(getParam('client_id'));\n\t$('.show-scope').text(getParam('scope'));\n}\n\n// 检查当前是否已经登录，如果已登录则直接开始跳转，如果未登录则等待用户输入账号密码 \nfunction tryJump(){\n\tvar data = location.search.substr(1);\n\tsa.ajax(\"/oauth2/getRedirectUri\", data, function(res) {\n\t\t// 情况1：客户端未登录，返回 code=401，提示用户登录\n\t\tif(res.code === 401) {\n\t\t\tshowLoginRegion();\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 情况2：请求的 scope 需要客户端手动确认授权，返回 code=411，提示用户手动确认 \n\t\tif(res.code === 411) {\n\t\t\tshowConfirmRegion();\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 情况3：已登录且请求的 scope 已确认授权，返回 code=200，data=最终重定向 url 地址(携带code码参数)\n\t\tif(res.code == 200) {\n\t\t\tconsole.log('跳转：', res.redirect_uri);\n\t\t\tlocation.href = res.redirect_uri;\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tconsole.log('未知状态码，', res.code, res);\n\t\tlayer.alert('错误：' + JSON.stringify(res))\n\t})\n}\n\n// 登录事件 \nfunction doLogin() {\n\t// 开始登录\n\tvar data = {\n\t\tname: $('[name=name]').val(),\n\t\tpwd: $('[name=pwd]').val()\n\t};\n\tsa.ajax(\"/oauth2/doLogin\", data, function(res) {\n\t\tif(res.code == 200) {\n\t\t\tlocalStorage.setItem('satoken', res.satoken);\n\t\t\tlayer.msg('登录成功', {anim: 0, icon: 6 }); \n\t\t\tsetTimeout(function() {\n\t\t\t\tlocation.reload();\n\t\t\t}, 800);\n\t\t} else {\n\t\t\tlayer.msg(res.msg, {anim: 6, icon: 2 }); \n\t\t}\n\t})\n}\n\n// 确认授权事件 \nfunction yes() {\n\tvar data = location.search.substr(1) + '&build_redirect_uri=true';\n\tsa.ajax(\"/oauth2/doConfirm\", data, function(res) {\n\t\tif(res.code == 200) {\n\t\t\tlayer.msg('确认授权成功，即将跳转...', {anim: 0, icon: 6 }); \n\t\t\tsetTimeout(function() {\n\t\t\t\tconsole.log('跳转：', res.redirect_uri);\n\t\t\t\tlocation.href = res.redirect_uri;\n\t\t\t}, 800);\n\t\t} else {\n\t\t\tlayer.msg(res.msg, {anim: 6, icon: 2 }); \n\t\t}\n\t})\n}\n\n// 拒绝授权事件 \nfunction no() {\n\tvar url = joinParam(getParam('redirect_uri'), \"handle=refuse&msg=用户拒绝了授权\");\n\tlocation.href = url;\n}\n\n// 页面加载完毕后触发 \nwindow.onload = function() {\n\ttryJump();\n\t\n\t// 绑定回车事件\n\t$('[name=name],[name=pwd]').bind('keypress', function(event){\n\t\tif(event.keyCode == \"13\") {\n\t\t\t$('.login-btn').click();\n\t\t}\n\t});\n\t\n\t// 输入框获取焦点\n\t$(\"[name=name]\").focus();\n}\n\n\n\n// ----------------------------------- 工具函数封装 -----------------------------------\n\n// sa \nvar sa = {};\n\n// 打开loading\nsa.loading = function(msg) {\n\tlayer.closeAll();\t// 开始前先把所有弹窗关了\n\treturn layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});\n};\n\n// 隐藏loading\nsa.hideLoading = function() {\n\tlayer.closeAll();\n};\n\n// 封装一下Ajax\nsa.ajax = function(url, data, successFn) {\n\tsa.loading(\"加载中...\");\n\t$.ajax({\n\t\turl: baseUrl + url,\n\t\ttype: \"post\", \n\t\tdata: data,\n\t\tdataType: 'json',\n\t\theaders: {\n\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t'satoken': localStorage.getItem('satoken')\n\t\t},\n\t\tsuccess: function(res){\n\t\t\tsa.hideLoading();\n\t\t\tconsole.log('返回数据：', res);\n\t\t\tsuccessFn(res);\n\t\t},\n\t\terror: function(xhr, type, errorThrown){\n\t\t\tsa.hideLoading();\n\t\t\tif(xhr.status == 0){\n\t\t\t\treturn alert('无法连接到服务器，请检查网络');\n\t\t\t}\n\t\t\treturn alert(\"异常：\" + JSON.stringify(xhr));\n\t\t}\n\t});\n}\n\n// 从url中查询到指定名称的参数值 \nfunction getParam(name, defaultValue){\n\tvar query = window.location.search.substring(1);\n\tvar vars = query.split(\"&\");\n\tfor (var i=0;i<vars.length;i++) {\n\t\tvar pair = vars[i].split(\"=\");\n\t\tif(pair[0] == name){return pair[1] + (pair[2] ? '=' + pair[2] : '');}\n\t}\n\treturn(defaultValue == undefined ? null : defaultValue);\n}\n\n// 在url上拼接上kv参数并返回 \nfunction joinParam(url, parameStr) {\n\tif(parameStr == null || parameStr.length == 0) {\n\t\treturn url;\n\t}\n\tvar index = url.indexOf('?');\n\t// ? 不存在\n\tif(index == -1) {\n\t\treturn url + '?' + parameStr;\n\t}\n\t// ? 是最后一位\n\tif(index == url.length - 1) {\n\t\treturn url + parameStr;\n\t}\n\t// ? 是其中一位\n\tif(index > -1 && index < url.length - 1) {\n\t\t// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &\n\t\tif(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) {\n\t\t\treturn url + '&' + parameStr;\n\t\t} else {\n\t\t\treturn url + parameStr;\n\t\t}\n\t}\n}\n\n// 打印信息 \nvar str = \"This page is provided by Sa-Token, Please refer to: \" + \"https://sa-token.cc/\";\nconsole.log(str);\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<title>Sa-OAuth2-Server 认证中心（前后端分离版）</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t\t<link rel=\"stylesheet\" href=\"./login.css\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\">\n\t\t\t<div class=\"bg-1\"></div>\n\t\t\t\n\t\t\t<!-- \n\t\t\t\t将页面分为三块区域：\n\t\t\t\t\t- 未登录时显示区域2：登录框。\n\t\t\t\t\t- 已登录但请求的 scope 尚未手动确认授权，显示区域3：确认授权框。\n\t\t\t\t\t- 默认显示区域1：提示文字。\n\t\t\t -->\n\t\t\t\n\t\t\t<!-- 区域1：默认显示 -->\n\t\t\t<div class=\"content-box region-default\">\n\t\t\t\t<div class=\"login-box\">\n\t\t\t\t\t<div class=\"message-box\">\n\t\t\t\t\t\t加载中...\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t\n\t\t\t<!-- 区域2：登录框 -->\n\t\t\t<div class=\"content-box region-login\">\n\t\t\t\t<div class=\"login-box\">\n\t\t\t\t\t<div class=\"from-box\">\n\t\t\t\t\t\t<h2 class=\"from-title\">Sa-OAuth2-Server 认证中心（前后端分离版）</h2>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"name\" placeholder=\"请输入账号\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"pwd\" type=\"password\" placeholder=\"请输入密码\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<button class=\"s-input s-btn login-btn\" onclick=\"doLogin()\">登录</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item reset-box\">\n\t\t\t\t\t\t\t<a href=\"javascript: location.reload();\" >刷新</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t\n\t\t\t<!-- 区域3：确认授权框 -->\n\t\t\t<div class=\"content-box region-confirm\">\n\t\t\t\t<div class=\"login-box\">\n\t\t\t\t\t<div class=\"from-box\">\n\t\t\t\t\t\t<h2 class=\"from-title\">Sa-OAuth2-Server 认证中心（前后端分离版）</h2><br>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div><b>应用ID：</b><span class=\"show-clientId\"></span></div>\n\t\t\t\t\t\t\t<div><b>请求授权：</b><span class=\"show-scope\"></span></div>\n\t\t\t\t\t\t\t<br><br><div>------------- 是否同意授权 -------------</div><br>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<button class=\"confirm-btn\" onclick=\"yes()\">同意</button>\n\t\t\t\t\t\t\t\t<button class=\"confirm-btn\" onclick=\"no()\">拒绝</button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div style=\"height: 10px;\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t\n\t\t\t<!-- 底部 版权 -->\n\t\t\t<div style=\"position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;\">\n\t\t\t\tThis page is provided by Sa-Token-OAuth2  \n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- scripts -->\n\t\t<script src=\"https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js\"></script>\n\t\t<script src=\"https://www.layuicdn.com/layer-v3.1.1/layer.js\"></script>\n\t\t<script src=\"./login.js\"></script>\n\t\t\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-quick-login</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>2.6.0</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n        <!-- quick-login -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-quick-login</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t\t<!-- 热更新插件 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n    <build>\n        <plugins>\n          \t<plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaQuicikStartup.java",
    "content": "package com.pj;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.quick.SaQuickManager;\n\n/**\n * springboot启动之后 \n * @author click33\n *\n */\n@Component\npublic class SaQuicikStartup implements CommandLineRunner {\n\n\t@Value(\"${spring.application.name:sa-quick}\")\n    private String applicationName;\n\t\n\t@Value(\"${server.port:8080}\")\n    private String port;\n\n    @Value(\"${server.servlet.context-path:}\")\n    private String path;\n\n//    @Value(\"${spring.profiles.active:}\")\n//    private String active;\n   \n    @Override\n    public void run(String... args) throws Exception {\n         String str = \"\\n------------- \" + applicationName + \" 启动成功 (\" + getNow() + \") -------------\\n\" + \n                 \"    - home: \" + \"http://localhost:\" + port + path + \"\\n\" +\n                 \"    - name: \" + SaQuickManager.getConfig().getName() + \"\\n\"+\n                 \"    - pwd : \" + SaQuickManager.getConfig().getPwd() + \"\\n\";\n         System.out.println(str);\n    }\n\n    \n\t\n\t/**\n\t * 返回系统当前时间的YYYY-MM-dd hh:mm:ss 字符串格式\n\t */\n\tprivate static String getNow(){\n\t\treturn new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(new Date());\n\t}\n    \n\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/SaTokenQuickDemoApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaTokenQuickDemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenQuickDemoApplication.class, args);\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\npublic class TestController {\n\n\t// 浏览器访问测试： http://localhost:8081\n\t@RequestMapping({\"/\"})\n\tpublic String index() {\n\t\tString str = \"<br />\"\n//\t\t\t\t+ \"<h1 style='text-align: center;'>Welcome to the system</h1>\"\n\t\t\t\t+ \"<h1 style='text-align: center;'>资源页 （登录后才可进入本页面） </h1>\"\n\t\t\t\t+ \"<hr/>\"\n\t\t\t\t+ \"<p style='text-align: center;'> Sa-Token \" + SaTokenConsts.VERSION_NO + \" </p>\";\n\t\treturn str; \n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# Sa-Token-Quick-Login 配置\nsa: \n    # 登录账号\n    name: sa\n    # 登录密码\n    pwd: 123456\n    # 是否自动随机生成账号密码 (此项为true时, name与pwd失效)\n    auto: false\n    # 是否开启全局认证(关闭后将不再强行拦截) \n    auth: true\n    # 登录页标题\n    title: Sa-Token 登录\n    # 是否显示底部版权信息 \n    copr: true\n    # 指定拦截路径 \n    # include: /**\n    # 指定排除路径\n    # exclude: /1.jpg\n    # 将本地磁盘的某个路径作为静态资源开放 \n    # dir: file:E:\\static\n    \n    \n# 静态文件路径映射 \nspring: \n    resources: \n        static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/, ${sa.dir:}\n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login-sb3/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-quick-login-sb3</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>3.4.3</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot3-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n        <!-- quick-login -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-quick-login</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- 热更新插件 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n    <build>\n        <plugins>\n          \t<plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaQuicikStartup.java",
    "content": "package com.pj;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.quick.SaQuickManager;\n\n/**\n * springboot启动之后 \n * @author click33\n *\n */\n@Component\npublic class SaQuicikStartup implements CommandLineRunner {\n\n\t@Value(\"${spring.application.name:sa-quick}\")\n    private String applicationName;\n\t\n\t@Value(\"${server.port:8080}\")\n    private String port;\n\n    @Value(\"${server.servlet.context-path:}\")\n    private String path;\n\n//    @Value(\"${spring.profiles.active:}\")\n//    private String active;\n   \n    @Override\n    public void run(String... args) throws Exception {\n         String str = \"\\n------------- \" + applicationName + \" 启动成功 (\" + getNow() + \") -------------\\n\" + \n                 \"    - home: \" + \"http://localhost:\" + port + path + \"\\n\" +\n                 \"    - name: \" + SaQuickManager.getConfig().getName() + \"\\n\"+\n                 \"    - pwd : \" + SaQuickManager.getConfig().getPwd() + \"\\n\";\n         System.out.println(str);\n    }\n\n    \n\t\n\t/**\n\t * 返回系统当前时间的YYYY-MM-dd hh:mm:ss 字符串格式\n\t */\n\tprivate static String getNow(){\n\t\treturn new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(new Date());\n\t}\n    \n\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/SaTokenQuickSb3DemoApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaTokenQuickSb3DemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenQuickSb3DemoApplication.class, args);\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login-sb3/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\npublic class TestController {\n\n\t// 浏览器访问测试： http://localhost:8081\n\t@RequestMapping({\"/\"})\n\tpublic String index() {\n\t\tString str = \"<br />\"\n//\t\t\t\t+ \"<h1 style='text-align: center;'>Welcome to the system</h1>\"\n\t\t\t\t+ \"<h1 style='text-align: center;'>资源页 （登录后才可进入本页面） </h1>\"\n\t\t\t\t+ \"<hr/>\"\n\t\t\t\t+ \"<p style='text-align: center;'> Sa-Token \" + SaTokenConsts.VERSION_NO + \" </p>\";\n\t\treturn str; \n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-quick-login-sb3/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# Sa-Token-Quick-Login 配置\nsa: \n    # 登录账号\n    name: sa\n    # 登录密码\n    pwd: 123456\n    # 是否自动随机生成账号密码 (此项为true时, name与pwd失效)\n    auto: false\n    # 是否开启全局认证(关闭后将不再强行拦截) \n    auth: true\n    # 登录页标题\n    title: Sa-Token 登录\n    # 是否显示底部版权信息 \n    copr: true\n    # 指定拦截路径 \n    # include: /**\n    # 指定排除路径\n    # exclude: /1.jpg\n    # 将本地磁盘的某个路径作为静态资源开放 \n    # dir: file:E:\\static\n    \n    \n# 静态文件路径映射 \nspring: \n    resources: \n        static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/, ${sa.dir:}\n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/page_project/.gitignore",
    "content": "# vite创建项目时自动生成的git忽略配置文件\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/page_project/README.md",
    "content": "# Vue 3 + Vite\n\n[Node下载地址](https://nodejs.org/zh-cn/)\n\n安装最新版本Node环境, 然后执行如下命令开启开发服务:\n```\nnpm install\nnpm run dev\n```\n\n[cookie/sessionstorage/localstorage三者的区别](https://blog.csdn.net/weixin_45541388/article/details/125367823)\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/page_project/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>记住我模式Demo页面</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/page_project/package.json",
    "content": "{\n  \"name\": \"page_project\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.3.4\",\n    \"element-plus\": \"^2.2.33\",\n    \"qs\": \"^6.11.0\",\n    \"vue\": \"^3.2.45\",\n    \"vue-axios\": \"^3.5.2\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^4.0.0\",\n    \"vite\": \"^4.1.0\"\n  }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/page_project/src/App.vue",
    "content": "<template>\n    <div class=\"mainLayoutClass\">\n      <div class=\"loginClass\">\n        <div class=\"titleClass\">\n          账户登录\n        </div>\n        <div>\n          <el-input v-model=\"name\" placeholder=\"账号\" />\n        </div>\n        <div>\n          <el-input v-model=\"passwd\" placeholder=\"密码\" />\n        </div>\n        <div>\n          <span>\n            <el-switch v-model=\"rememberMe\" />\n          </span>\n          <span class=\"tipInfoClass\" @click=\"rememberMe = !rememberMe\">记住我</span>\n        </div>\n        <div>\n          <el-button type=\"primary\" style=\"width: 100%;\" @click=\"loginFun\">登录</el-button>\n        </div>\n      </div>\n      <div class=\"stateClass\">\n        <div class=\"titleClass\">当前登录状态:</div>\n        <div class=\"titleClass\">{{ loginState }}</div>\n        <div>\n          <el-button type=\"primary\" style=\"width: 100%;\" @click=\"checkLoginStateFun\">刷新登录状态</el-button>\n        </div>\n        <div>\n          <el-button type=\"danger\" style=\"width: 100%;\" @click=\"logoutFun\">退出</el-button>\n        </div>\n      </div>\n    </div>\n</template>\n\n<script>\n\nexport default {\n  mounted () {\n    if (localStorage.getItem('rememberMe') === 'true') {\n      this.rememberMe = true\n    }\n    this.checkLoginStateFun()\n  },\n  data () {\n    return {\n      name: 'zhang',\n      passwd: '123456',\n      rememberMe: false,\n      loginState: false\n    }\n  },\n  methods: {\n    loginFun () {\n      this.axios.post('/back/user/login', this.$f({\n        name: this.name,\n        pwd: this.passwd,\n        remember: this.rememberMe\n      })).then(res => {\n        if (res.status === 200) {\n          this.loginState = true\n          const { tokenName, tokenValue } = res.data\n          localStorage.setItem('tokenName', tokenName)\n          if (this.rememberMe) {\n            localStorage.setItem('tokenValue', tokenValue)\n          } else {\n            sessionStorage.setItem('tokenValue', tokenValue)\n          }\n        } else {\n          this.$message.error('网络异常')\n        }\n      }).catch(() => {\n        this.$message.error('无法访问后台服务')\n      })\n    },\n    checkLoginStateFun () {\n      let tokenName, tokenValue\n      tokenName = localStorage.getItem('tokenName')\n      if (this.rememberMe) {\n        tokenValue = localStorage.getItem('tokenValue')\n      } else {\n        tokenValue = sessionStorage.getItem('tokenValue')\n      }\n      const param = {}\n      param[tokenName] = tokenValue\n      this.axios.post('/back/user/state', this.$f(param))\n      .then(res => {\n        if (res.status === 200) {\n          this.loginState = res.data.data\n        } else {\n          this.$message.error('网络异常')\n        }\n      }).catch(() => {\n        this.$message.error('无法访问后台服务')\n      })\n    },\n    logoutFun () {\n      // ------------------------------------------------------------------------\n      // 重复的部分可以写到外部js统一封装或通过axios的拦截器添加token参数, 这里只做演示\n      let tokenName, tokenValue\n      tokenName = localStorage.getItem('tokenName')\n      if (this.rememberMe) {\n        tokenValue = localStorage.getItem('tokenValue')\n      } else {\n        tokenValue = sessionStorage.getItem('tokenValue')\n      }\n      const param = {}\n      param[tokenName] = tokenValue\n      // -------------------------------------------------------------------------\n      this.axios.post('/back/user/logout', this.$f(param))\n      .then(res => {\n        if (res.status === 200) {\n          this.loginState = res.data.data\n        } else {\n          this.$message.error('网络异常')\n        }\n      }).catch(() => {\n        this.$message.error('无法访问后台服务')\n      })\n    }\n  },\n  watch: {\n    rememberMe (newValue, oldValue) {\n      // 打开不同页面时使 记住我 的状态保持一致\n      localStorage.setItem('rememberMe', newValue)\n    }\n  }\n}\n</script>\n\n<style scoped>\n.mainLayoutClass{\n  padding: 0 25%;\n  padding-top: 20vh;\n  display: flex;\n  justify-content: space-between;\n  user-select: none;\n}\n.loginClass{\n  min-width: 300px;\n  height: 210px;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n}\n.titleClass{\n  font-size: 22px;\n  font-weight: bold;\n}\n.tipInfoClass{\n  cursor: pointer;\n}\n.stateClass{\n  min-width: 300px;\n  height: 200px;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n}\n</style>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/page_project/src/main.js",
    "content": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport axios from 'axios' // 请求发送接收工具\nimport VueAxios from 'vue-axios' // vue封装axios\nimport qs from 'qs' // axios请求参数类型封装\nimport ElementPlus from 'element-plus' // elementUI for vue3\nimport 'element-plus/dist/index.css' // 加载elementUI样式\nimport zhCn from 'element-plus/es/locale/lang/zh-cn' // 引入中文本地化组件\n\n\nconst app = createApp(App)\n\n// vue组件内通过 this.$f() 来调用\napp.config.globalProperties.$f = (params) => {\n  return qs.stringify(params)\n}\n\napp.use(VueAxios, axios)\n.use(ElementPlus, { locale: zhCn })\n.mount('#app')\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/page_project/vite.config.js",
    "content": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\n\n// 开启代理服务\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [vue()],\n  server: {\n    port: 5173,\n    host: true,\n    proxy: {\n      '^/back/.*$': {\n        target: 'http://localhost:80'\n      }\n    }\n  }\n})\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-demo-remember-me-server</artifactId>\n    <version>1.0-SNAPSHOT</version>\n\n    <!-- SpringBoot -->\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.5.14</version>\n        <relativePath/>\n    </parent>\n\n    <properties>\n        <sa-token.version>1.45.0</sa-token.version>\n    </properties>\n\n    <dependencies>\n        <!-- SpringBoot Web依赖 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n        <!-- Sa-Token 插件：整合redis (使用jackson序列化方式) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/RememberMeApplication.java",
    "content": "package cc.sa_token;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class RememberMeApplication {\n    public static void main(String[] args) {\n        SpringApplication.run(RememberMeApplication.class, args);\n    }\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/java/cc/sa_token/controller/UserLoginController.java",
    "content": "package cc.sa_token.controller;\n\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RequestMapping(\"/back/user\")\npublic class UserLoginController {\n\n    @RequestMapping(\"/login\")\n    public SaResult doLogin(String name, String pwd, Boolean remember) {\n        if(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n            StpUtil.login(10001, remember);\n            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();\n            return SaResult.ok()\n                    .set(\"tokenName\", tokenInfo.getTokenName())\n                    .set(\"tokenValue\", tokenInfo.getTokenValue());\n        } else {\n            return SaResult.error(\"登录失败\");\n        }\n    }\n\n    @RequestMapping(\"/state\")\n    public SaResult checkNowLoginState() {\n        return SaResult.ok().setData(StpUtil.isLogin());\n    }\n\n    @RequestMapping(\"/logout\")\n    public SaResult doLogout() {\n        StpUtil.logout();\n        return SaResult.ok().setData(StpUtil.isLogin());\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-remember-me/sa-token-demo-remember-me-server/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 80\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-solon</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- Solon -->\n\t<parent>\n\t\t<groupId>org.noear</groupId>\n\t\t<artifactId>solon-parent</artifactId>\n\t\t<version>3.2.1</version>\n\t\t<relativePath/>\n\t</parent>\n\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<java.version>17</java.version>\n\t\t<maven.compiler.source>17</maven.compiler.source>\n\t\t<maven.compiler.traget>17</maven.compiler.traget>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n\t    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- Solon 依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon-web</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon.logging.simple</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-solon-plugin</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- sa-token整合redis (使用jdk默认序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisx</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- sa-token json 序列化器组件：snack3 实现  -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-snack3</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- hutool工具类，用来生成雪花算法唯一id -->\n\t\t<!-- <dependency>\n\t\t     <groupId>cn.hutool</groupId>\n\t\t     <artifactId>hutool-all</artifactId>\n\t\t     <version>5.5.4</version>\n\t\t</dependency> -->\n\t\t\n\n\t</dependencies>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-compiler-plugin</artifactId>\n\t\t\t\t<version>3.8.1</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<compilerArgument>-parameters</compilerArgument>\n\t\t\t\t\t<source>1.8</source>\n\t\t\t\t\t<target>1.8</target>\n\t\t\t\t\t<encoding>UTF-8</encoding>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/SaTokenDemoApp.java",
    "content": "package com.pj;\n\n\nimport cn.dev33.satoken.SaManager;\nimport org.noear.solon.Solon;\nimport org.noear.solon.annotation.SolonMain;\n\n/**\n * sa-token整合 solon 示例\n * @author noear\n *\n */\n@SolonMain\npublic class SaTokenDemoApp {\n\n\tpublic static void main(String[] args) {\n\t\tSolon.start(SaTokenDemoApp.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaLogForSlf4j.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.log.SaLogForConsole;\nimport cn.dev33.satoken.util.StrFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * 将 Sa-Token log 信息转接到 slf4j 接口\n *\n * @author noear 2022/11/14 created\n */\n//@Component\npublic class SaLogForSlf4j extends SaLogForConsole implements SaLog {\n    static final Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class);\n\n    /**\n     * 打印日志到控制台\n     *\n     * @param level 日志等级\n     * @param str   字符串\n     * @param args  参数列表\n     */\n    public void println(int level, String str, Object... args) {\n        SaTokenConfig config = SaManager.getConfig();\n\n        if (config.getIsLog() && level >= config.getLogLevelInt()) {\n            switch (level) {\n                case trace:\n                    log.trace(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case debug:\n                    log.debug(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case info:\n                    log.info(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case warn:\n                    log.warn(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case error:\n                case fatal:\n                    log.error(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaLogForSolon.java",
    "content": "package com.pj.satoken;\n\nimport org.noear.solon.core.util.LogUtil;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.log.SaLogForConsole;\nimport cn.dev33.satoken.util.StrFormatter;\n\n/**\n * 将 Sa-Token log 信息转接到 Solon  \n * \n * @author click33\n * @since 2022-11-2\n */\n//@Component\npublic class SaLogForSolon extends SaLogForConsole implements SaLog {\n\n\t/**\n\t * 打印日志到控制台\n\t *\n\t * @param level 日志等级\n\t * @param str   字符串\n\t * @param args  参数列表\n\t */\n\tpublic void println(int level, String str, Object... args) {\n\t\tSaTokenConfig config = SaManager.getConfig();\n\n\t\tif (config.getIsLog() && level >= config.getLogLevelInt()) {\n\t\t\tswitch (level) {\n\t\t\t\tcase trace:\n\t\t\t\t\tLogUtil.global().trace(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase debug:\n\t\t\t\t\tLogUtil.global().debug(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase info:\n\t\t\t\t\tLogUtil.global().info(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase warn:\n\t\t\t\t\tLogUtil.global().warn(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase error:\n\t\t\t\tcase fatal:\n\t\t\t\t\tLogUtil.global().error(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisx;\nimport cn.dev33.satoken.solon.integration.SaTokenInterceptor;\nimport com.pj.util.AjaxJson;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n * @author noear\n */\n@Configuration\npublic class SaTokenConfigure {\n\n\t/**\n\t * 注册 [sa-token全局过滤器]\n\t */\n\t@Bean(index = -100)\n\tpublic SaTokenInterceptor tokenPathFilter() {\n\t\treturn new SaTokenInterceptor()\n\n\t\t\t\t// 指定 [拦截路由] 与 [放行路由]\n\t\t\t\t.addInclude(\"/**\").addExclude(\"/favicon.ico\")\n\n\t\t\t\t// 认证函数: 每次请求执行\n\t\t\t\t.setAuth(r -> {\n\t\t\t\t\t// System.out.println(\"---------- sa全局认证\");\n\n\t\t\t\t\t// SaRouter.match(\"/test/test\", () -> new Object());\n\t\t\t\t})\n\n\t\t\t\t// 异常处理函数：每次认证函数发生异常时执行此函数\n\t\t\t\t.setError(e -> {\n\t\t\t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\treturn AjaxJson.getError(e.getMessage());\n\t\t\t\t})\n\n\t\t\t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n\t\t\t\t.setBeforeAuth(r -> {\n\t\t\t\t\t// ---------- 设置一些安全响应头 ----------\n\t\t\t\t\tSaHolder.getResponse()\n\t\t\t\t\t\t\t// 服务器名称\n\t\t\t\t\t\t\t.setServer(\"sa-server\")\n\t\t\t\t\t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以\n\t\t\t\t\t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n\t\t\t\t\t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n\t\t\t\t\t\t\t.setHeader(\"X-Frame-Options\", \"1; mode=block\")\n\t\t\t\t\t\t\t// 禁用浏览器内容嗅探\n\t\t\t\t\t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n\t\t\t\t\t;\n\t\t\t\t});\n\t}\n\n\t//如果需要 redis dao，加这段代表\n\t@Bean\n\tpublic SaTokenDao saTokenDaoInit(@Inject(\"${sa-token-dao.redis}\") SaTokenDaoForRedisx saTokenDao) {\n\t\treturn saTokenDao;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport org.noear.solon.annotation.Component;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component    // 打开此注解，保证此类被 solon 扫描，即可完成 sa-token 的自定义权限验证扩展\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/CheckAccount.java",
    "content": "package com.pj.satoken.custom_annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 账号校验：在标注一个方法上时，要求前端必须提交相应的账号密码参数才能访问方法。\n *\n * @author click33\n *\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface CheckAccount {\n\n    /**\n     * 需要校验的账号\n     *\n     * @return /\n     */\n    String name();\n\n    /**\n     * 需要校验的密码\n     *\n     * @return /\n     */\n    String pwd();\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/satoken/custom_annotation/handler/CheckAccountHandler.java",
    "content": "package com.pj.satoken.custom_annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport com.pj.satoken.custom_annotation.CheckAccount;\nimport org.noear.solon.annotation.Component;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 CheckAccount 的处理器\n *\n * @author click33\n *\n */\n@Component\npublic class CheckAccountHandler implements SaAnnotationHandlerInterface<CheckAccount> {\n\n    // 指定这个处理器要处理哪个注解\n    @Override\n    public Class<CheckAccount> getHandlerAnnotationClass() {\n        return CheckAccount.class;\n    }\n\n    // 每次请求校验注解时，会执行的方法\n    @Override\n    public void checkMethod(CheckAccount at, AnnotatedElement method) {\n        // 获取前端请求提交的参数\n        String name = SaHolder.getRequest().getParamNotNull(\"name\");\n        String pwd = SaHolder.getRequest().getParamNotNull(\"pwd\");\n\n        // 与注解中指定的值相比较\n        if(name.equals(at.name()) && pwd.equals(at.pwd()) ) {\n            // 校验通过，什么也不做\n        } else {\n            // 校验不通过，则抛出异常\n            throw new SaTokenException(\"账号或密码错误，未通过校验\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/GlobalExceptionFilter.java",
    "content": "package com.pj.test;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.*;\n\nimport org.noear.solon.annotation.Component;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n\n/**\n * 全局异常处理\n *\n * @author noear\n */\n@Component\npublic class GlobalExceptionFilter implements Filter {\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\ttry {\n\t\t\tchain.doFilter(ctx);\n\t\t} catch (SaTokenException e) {\n\t\t\t// 不同异常返回不同状态码\n\t\t\tAjaxJson aj = null;\n\t\t\tif (e instanceof NotLoginException) {    // 如果是未登录异常\n\t\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t\t} else if (e instanceof NotRoleException) {        // 如果是角色异常\n\t\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t\t} else if (e instanceof NotPermissionException) {    // 如果是权限异常\n\t\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t\t} else if (e instanceof DisableServiceException) {    // 如果是被封禁异常\n\t\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\t\taj = AjaxJson.getNotJur(\"账号被封禁：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t\t} else {    // 普通异常, 输出：500 + 异常信息\n\t\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t\t}\n\n\t\t\tctx.render(aj);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\nimport com.pj.util.AjaxJson;\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 压力测试 \n * @author click33\n * @author noear\n */\n@Controller\n@Mapping(\"/s-test/\")\npublic class StressTestController {\n\n\t\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@Mapping(\"login\")\n\tpublic AjaxJson login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn AjaxJson.getSuccess();\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.session.SaSessionCustomUtil;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport com.pj.util.AjaxJson;\nimport com.pj.util.Ttime;\nimport org.noear.snack.ONode;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\nimport org.noear.solon.annotation.Param;\n\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 测试专用Controller \n * @author click33\n * @author noear\n */\n@Controller\n@Mapping(\"/test/\")\npublic class TestController {\n\n\t\n\t// 测试登录接口， 浏览器访问： http://localhost:8081/test/login\n\t@Mapping(\"login\")\n\tpublic AjaxJson login(@Param(defaultValue=\"10001\") String id) {\n\t\tSystem.out.println(\"======================= 进入方法，测试登录接口 ========================= \");\n\t\tSystem.out.println(\"当前会话的token：\" + StpUtil.getTokenValue());\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginIdDefaultNull());\n\t\t\n\t\tStpUtil.login(id);\t\t\t// 在当前会话登录此账号 \t\n\t\tSystem.out.println(\"登录成功\");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginId());\n//\t\tSystem.out.println(\"当前登录账号并转为int：\" + StpUtil.getLoginIdAsInt());\n\t\tSystem.out.println(\"当前登录设备：\" + StpUtil.getLoginDevice());\n//\t\tSystem.out.println(\"当前token信息：\" + StpUtil.getTokenInfo());\t\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试退出登录 ， 浏览器访问： http://localhost:8081/test/logout\n\t@Mapping(\"logout\")\n\tpublic AjaxJson logout() {\n\t\tStpUtil.logout();\n//\t\tStpUtil.logoutByLoginId(10001);\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试角色接口， 浏览器访问： http://localhost:8081/test/testRole\n\t@Mapping(\"testRole\")\n\tpublic AjaxJson testRole() {\n\t\tSystem.out.println(\"======================= 进入方法，测试角色接口 ========================= \");\n\t\t\n\t\tSystem.out.println(\"是否具有角色标识 user \" + StpUtil.hasRole(\"user\"));\n\t\tSystem.out.println(\"是否具有角色标识 admin \" + StpUtil.hasRole(\"admin\"));\n\t\t\n\t\tSystem.out.println(\"没有admin权限就抛出异常\");\n\t\tStpUtil.checkRole(\"admin\");\n\t\t\n\t\tSystem.out.println(\"在【admin、user】中只要拥有一个就不会抛出异常\");\n\t\tStpUtil.checkRoleOr(\"admin\", \"user\");\n\n\t\tSystem.out.println(\"在【admin、user】中必须全部拥有才不会抛出异常\");\n\t\tStpUtil.checkRoleAnd(\"admin\", \"user\");\n\n\t\tSystem.out.println(\"角色测试通过\");\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试权限接口， 浏览器访问： http://localhost:8081/test/testJur\n\t@Mapping(\"testJur\")\n\tpublic AjaxJson testJur() {\n\t\tSystem.out.println(\"======================= 进入方法，测试权限接口 ========================= \");\n\t\t\n\t\tSystem.out.println(\"是否具有权限101\" + StpUtil.hasPermission(\"101\"));\n\t\tSystem.out.println(\"是否具有权限user-add\" + StpUtil.hasPermission(\"user-add\"));\n\t\tSystem.out.println(\"是否具有权限article-get\" + StpUtil.hasPermission(\"article-get\"));\n\t\t\n\t\tSystem.out.println(\"没有user-add权限就抛出异常\");\n\t\tStpUtil.checkPermission(\"user-add\");\n\t\t\n\t\tSystem.out.println(\"在【101、102】中只要拥有一个就不会抛出异常\");\n\t\tStpUtil.checkPermissionOr(\"101\", \"102\");\n\n\t\tSystem.out.println(\"在【101、102】中必须全部拥有才不会抛出异常\");\n\t\tStpUtil.checkPermissionAnd(\"101\", \"102\");\n\n\t\tSystem.out.println(\"权限测试通过\");\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试会话session接口， 浏览器访问： http://localhost:8081/test/session \n\t@Mapping(\"session\")\n\tpublic AjaxJson session()  {\n\t\tSystem.out.println(\"======================= 进入方法，测试会话session接口 ========================= \");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tStpUtil.getSession().set(\"name\", new Date());\t// 写入一个值 \n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tSystem.out.println( ONode.stringify(StpUtil.getSession()));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试自定义session接口， 浏览器访问： http://localhost:8081/test/session2 \n\t@Mapping(\"session2\")\n\tpublic AjaxJson session2() {\n\t\tSystem.out.println(\"======================= 进入方法，测试自定义session接口 ========================= \");\n\t\t// 自定义session就是无需登录也可以使用 的session ：比如拿用户的手机号当做 key， 来获取 session \n\t\tSystem.out.println(\"自定义 session的id为：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").getId());\n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\tSaSessionCustomUtil.getSessionById(\"1895544896\").set(\"name\", \"张三\");\t// 写入值 \n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// ---------- \n\t// 测试token专属session， 浏览器访问： http://localhost:8081/test/getTokenSession \n\t@Mapping(\"getTokenSession\")\n\tpublic AjaxJson getTokenSession() {\n\t\tSystem.out.println(\"======================= 进入方法，测试会话session接口 ========================= \");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前token专属session: \" + StpUtil.getTokenSession().getId());\n\n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getTokenSession().get(\"name\"));\n\t\tStpUtil.getTokenSession().set(\"name\", \"张三\");\t// 写入一个值 \n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getTokenSession().get(\"name\"));\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 打印当前token信息， 浏览器访问： http://localhost:8081/test/tokenInfo\n\t@Mapping(\"tokenInfo\")\n\tpublic AjaxJson tokenInfo() {\n\t\tSystem.out.println(\"======================= 进入方法，打印当前token信息 ========================= \");\n\t\tSaTokenInfo tokenInfo = StpUtil.getTokenInfo();\n\t\tSystem.out.println(tokenInfo);\n\t\treturn AjaxJson.getSuccessData(tokenInfo);\n\t}\n\t\n\t// 测试注解式鉴权， 浏览器访问： http://localhost:8081/test/atCheck\n\t@SaCheckLogin\t\t\t\t\t\t// 注解式鉴权：当前会话必须登录才能通过 \n\t@SaCheckRole(\"super-admin\")\t\t\t// 注解式鉴权：当前会话必须具有指定角色标识才能通过 \n\t@SaCheckPermission(\"user-add\")\t\t// 注解式鉴权：当前会话必须具有指定权限才能通过 \n\t@Mapping(\"atCheck\")\n\tpublic AjaxJson atCheck() {\n\t\tSystem.out.println(\"======================= 进入方法，测试注解鉴权接口 ========================= \");\n\t\tSystem.out.println(\"只有通过注解鉴权，才能进入此方法\");\n//\t\tStpUtil.checkActiveTimeout();\n//\t\tStpUtil.updateLastActiveToNow();\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试注解式鉴权， 浏览器访问： http://localhost:8081/test/atJurOr\n\t@Mapping(\"atJurOr\")\n\t@SaCheckPermission(value = {\"user-add\", \"user-all\", \"user-delete\"}, mode = SaMode.OR)\t\t// 注解式鉴权：只要具有其中一个权限即可通过校验 \n\tpublic AjaxJson atJurOr() {\n\t\treturn AjaxJson.getSuccessData(\"用户信息\");\n\t}\n\t\n\t// [活动时间] 续签： http://localhost:8081/test/rene\n\t@Mapping(\"rene\")\n\tpublic AjaxJson rene() {\n\t\tStpUtil.checkActiveTimeout();\n\t\tStpUtil.updateLastActiveToNow();\n\t\treturn AjaxJson.getSuccess(\"续签成功\");\n\t}\n\t\n\t// 测试踢人下线   浏览器访问： http://localhost:8081/test/kickOut \n\t@Mapping(\"kickOut\")\n\tpublic AjaxJson kickOut() {\n\t\t// 先登录上 \n\t\tStpUtil.login(10001);\n\t\t// 踢下线 \n\t\tStpUtil.kickout(10001);\n\t\t// 再尝试获取\n\t\tStpUtil.getLoginId();\n\t\t// 返回 \n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试登录接口, 按照设备类型登录， 浏览器访问： http://localhost:8081/test/login2\n\t@Mapping(\"login2\")\n\tpublic AjaxJson login2(@Param(defaultValue=\"10001\") String id, @Param(defaultValue=\"PC\") String device) {\n\t\tStpUtil.login(id, device);\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试身份临时切换： http://localhost:8081/test/switchTo\n\t@Mapping(\"switchTo\")\n\tpublic AjaxJson switchTo() {\n\t\tSystem.out.println(\"当前会话身份：\" + StpUtil.getLoginIdDefaultNull());\n\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\tStpUtil.switchTo(10044, () -> {\n\t\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\t\tSystem.out.println(\"当前会话身份已被切换为：\" + StpUtil.getLoginId());\n\t\t});\t\t\n\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试会话治理   浏览器访问： http://localhost:8081/test/search\n\t@Mapping(\"search\")\n\tpublic AjaxJson search() {\n\t\tSystem.out.println(\"--------------\");\n\t\tTtime t = new Ttime().start();\n\t\tList<String> tokenValue = StpUtil.searchTokenValue(\"8feb8265f773\", 0, 10, true);\n\t\tfor (String v : tokenValue) {\n//\t\t\tSaSession session = StpUtil.getSessionBySessionId(sid);\n\t\t\tSystem.out.println(v);\n\t\t}\n\t\tSystem.out.println(\"用时：\" + t.end().toString());\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试指定设备类型登录   浏览器访问： http://localhost:8081/test/loginByDevice\n\t@Mapping(\"loginByDevice\")\n\tpublic AjaxJson loginByDevice() {\n\t\tSystem.out.println(\"--------------\");\n\t\tStpUtil.login(10001, \"PC\");\n\t\treturn AjaxJson.getSuccessData(\"登录成功\");\n\t}\n\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@Mapping(\"test\")\n\tpublic AjaxJson test() {\n\t\tSystem.out.println(\"进来了\");\n\t\treturn AjaxJson.getSuccess(\"访问成功\");\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@Mapping(\"test2\")\n\tpublic AjaxJson test2() {\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/test/UserController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\n/**\n * 登录测试\n * @author click33\n * @author noear\n */\n@Controller\n@Mapping(\"/user/\")\npublic class UserController {\n\n\t// 测试登录，浏览器访问： http://localhost:8081/user/doLogin?username=zhang&password=123456\n\t@Mapping(\"doLogin\")\n\tpublic String doLogin(String username, String password) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(username) && \"123456\".equals(password)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn \"登录成功\";\n\t\t}\n\t\treturn \"登录失败\";\n\t}\n\n\t// 查询登录状态，浏览器访问： http://localhost:8081/user/isLogin\n\t@Mapping(\"isLogin\")\n\tpublic String isLogin(String username, String password) {\n\t\treturn \"当前会话是否登录：\" + StpUtil.isLogin();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon/src/main/resources/app.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\n\nsa-token-dao: #名字可以随意取\n    redis:\n        server: \"localhost:6379\"\n#        password: 123456\n        db: 1\n        maxTotal: 200\n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-solon-redisson</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- Solon -->\n\t<parent>\n\t\t<groupId>org.noear</groupId>\n\t\t<artifactId>solon-parent</artifactId>\n\t\t<version>3.2.1</version>\n\t\t<relativePath/>\n\t</parent>\n\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n\t    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- Solon 依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon-web</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon.logging.simple</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>redisson-solon-plugin</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-solon-plugin</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t\n\t\t<!-- sa-token整合redis (使用jdk默认序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\n\t\t<!-- hutool工具类，用来生成雪花算法唯一id -->\n\t\t<!-- <dependency>\n\t\t     <groupId>cn.hutool</groupId>\n\t\t     <artifactId>hutool-all</artifactId>\n\t\t     <version>5.5.4</version>\n\t\t</dependency> -->\n\t\t\n\n\t</dependencies>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-compiler-plugin</artifactId>\n\t\t\t\t<version>3.8.1</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<compilerArgument>-parameters</compilerArgument>\n\t\t\t\t\t<source>1.8</source>\n\t\t\t\t\t<target>1.8</target>\n\t\t\t\t\t<encoding>UTF-8</encoding>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java",
    "content": "package com.pj;\n\n\nimport cn.dev33.satoken.SaManager;\nimport org.noear.solon.Solon;\nimport org.noear.solon.annotation.SolonMain;\n\n/**\n * sa-token整合 solon 示例\n * @author noear\n *\n */\n@SolonMain\npublic class SaTokenDemoApp {\n\n\tpublic static void main(String[] args) {\n\t\tSolon.start(SaTokenDemoApp.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.log.SaLogForConsole;\nimport cn.dev33.satoken.util.StrFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * 将 Sa-Token log 信息转接到 slf4j 接口\n *\n * @author noear 2022/11/14 created\n */\n//@Component\npublic class SaLogForSlf4j extends SaLogForConsole implements SaLog {\n    static final Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class);\n\n    /**\n     * 打印日志到控制台\n     *\n     * @param level 日志等级\n     * @param str   字符串\n     * @param args  参数列表\n     */\n    public void println(int level, String str, Object... args) {\n        SaTokenConfig config = SaManager.getConfig();\n\n        if (config.getIsLog() && level >= config.getLogLevelInt()) {\n            switch (level) {\n                case trace:\n                    log.trace(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case debug:\n                    log.debug(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case info:\n                    log.info(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case warn:\n                    log.warn(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n                case error:\n                case fatal:\n                    log.error(LOG_PREFIX + StrFormatter.format(str, args));\n                    break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java",
    "content": "package com.pj.satoken;\n\nimport org.noear.solon.core.util.LogUtil;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.log.SaLogForConsole;\nimport cn.dev33.satoken.util.StrFormatter;\n\n/**\n * 将 Sa-Token log 信息转接到 Solon  \n * \n * @author click33\n * @since 2022-11-2\n */\n//@Component\npublic class SaLogForSolon extends SaLogForConsole implements SaLog {\n\n\t/**\n\t * 打印日志到控制台\n\t *\n\t * @param level 日志等级\n\t * @param str   字符串\n\t * @param args  参数列表\n\t */\n\tpublic void println(int level, String str, Object... args) {\n\t\tSaTokenConfig config = SaManager.getConfig();\n\n\t\tif (config.getIsLog() && level >= config.getLogLevelInt()) {\n\t\t\tswitch (level) {\n\t\t\t\tcase trace:\n\t\t\t\t\tLogUtil.global().trace(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase debug:\n\t\t\t\t\tLogUtil.global().debug(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase info:\n\t\t\t\t\tLogUtil.global().info(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase warn:\n\t\t\t\t\tLogUtil.global().warn(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t\tcase error:\n\t\t\t\tcase fatal:\n\t\t\t\t\tLogUtil.global().error(LOG_PREFIX + StrFormatter.format(str, args));\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisson;\nimport cn.dev33.satoken.solon.integration.SaTokenInterceptor;\nimport com.pj.util.AjaxJson;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\nimport org.redisson.api.RedissonClient;\nimport org.redisson.solon.RedissonSupplier;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n * @author noear\n */\n@Configuration\npublic class SaTokenConfigure {\n\n\t/**\n\t * 注册 [sa-token全局过滤器]\n\t */\n\t@Bean(index = -100)\n\tpublic SaTokenInterceptor tokenPathFilter() {\n\t\treturn new SaTokenInterceptor()\n\n\t\t\t\t// 指定 [拦截路由] 与 [放行路由]\n\t\t\t\t.addInclude(\"/**\").addExclude(\"/favicon.ico\")\n\n\t\t\t\t// 认证函数: 每次请求执行\n\t\t\t\t.setAuth(r -> {\n\t\t\t\t\t// System.out.println(\"---------- sa全局认证\");\n\n\t\t\t\t\t// SaRouter.match(\"/test/test\", () -> new Object());\n\t\t\t\t})\n\n\t\t\t\t// 异常处理函数：每次认证函数发生异常时执行此函数\n\t\t\t\t.setError(e -> {\n\t\t\t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\treturn AjaxJson.getError(e.getMessage());\n\t\t\t\t})\n\n\t\t\t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n\t\t\t\t.setBeforeAuth(r -> {\n\t\t\t\t\t// ---------- 设置一些安全响应头 ----------\n\t\t\t\t\tSaHolder.getResponse()\n\t\t\t\t\t\t\t// 服务器名称\n\t\t\t\t\t\t\t.setServer(\"sa-server\")\n\t\t\t\t\t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以\n\t\t\t\t\t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n\t\t\t\t\t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n\t\t\t\t\t\t\t.setHeader(\"X-Frame-Options\", \"1; mode=block\")\n\t\t\t\t\t\t\t// 禁用浏览器内容嗅探\n\t\t\t\t\t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n\t\t\t\t\t;\n\t\t\t\t});\n\t}\n\n\t/**\n\t * 构造 RedissonClient\n\t * */\n\t@Bean\n\tpublic RedissonClient saTokenDaoInit(@Inject(\"${sa-token-dao}\") RedissonSupplier supplier) {\n\t\treturn supplier.get();\n\t}\n\n\t/**\n\t * 构建  SaTokenDao\n\t * */\n\t@Bean\n\tpublic SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {\n\t\treturn new SaTokenDaoForRedisson(redissonClient);\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport org.noear.solon.annotation.Component;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component    // 打开此注解，保证此类被 solon 扫描，即可完成 sa-token 的自定义权限验证扩展\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java",
    "content": "package com.pj.test;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.*;\n\nimport org.noear.solon.annotation.Component;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n\n/**\n * 全局异常处理\n *\n * @author noear\n */\n@Component\npublic class GlobalExceptionFilter implements Filter {\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\ttry {\n\t\t\tchain.doFilter(ctx);\n\t\t} catch (SaTokenException e) {\n\t\t\t// 不同异常返回不同状态码\n\t\t\tAjaxJson aj = null;\n\t\t\tif (e instanceof NotLoginException) {    // 如果是未登录异常\n\t\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t\t} else if (e instanceof NotRoleException) {        // 如果是角色异常\n\t\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t\t} else if (e instanceof NotPermissionException) {    // 如果是权限异常\n\t\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t\t} else if (e instanceof DisableServiceException) {    // 如果是被封禁异常\n\t\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\t\taj = AjaxJson.getNotJur(\"账号被封禁：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t\t} else {    // 普通异常, 输出：500 + 异常信息\n\t\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t\t}\n\n\t\t\tctx.render(aj);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java",
    "content": "package com.pj.test;\n\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.noear.solon.annotation.Param;\n\n/**\n * 测试: 同域单点登录\n * @author click33\n * @author noear\n */\n@Controller\n@Mapping(\"/sso/\")\npublic class SSOController {\n\n\t// 测试：进行登录\n\t@Mapping(\"doLogin\")\n\tpublic AjaxJson doLogin(@Param(defaultValue = \"10001\") String id) {\n\t\tSystem.out.println(\"---------------- 进行登录 \");\n\t\tStpUtil.login(id);\n\t\treturn AjaxJson.getSuccess(\"登录成功: \" + id);\n\t}\n\n\t// 测试：是否登录\n\t@Mapping(\"isLogin\")\n\tpublic AjaxJson isLogin() {\n\t\tSystem.out.println(\"---------------- 是否登录 \");\n\t\tboolean isLogin = StpUtil.isLogin();\n\t\treturn AjaxJson.getSuccess(\"是否登录: \" + isLogin);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\nimport com.pj.util.AjaxJson;\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 压力测试 \n * @author click33\n * @author noear\n */\n@Controller\n@Mapping(\"/s-test/\")\npublic class StressTestController {\n\n\t\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@Mapping(\"login\")\n\tpublic AjaxJson login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn AjaxJson.getSuccess();\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport java.util.Date;\nimport java.util.List;\n\nimport com.pj.util.AjaxJson;\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.session.SaSessionCustomUtil;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.noear.snack.ONode;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\nimport org.noear.solon.annotation.Param;\n\n/**\n * 测试专用Controller \n * @author click33\n * @author noear\n */\n@Controller\n@Mapping(\"/test/\")\npublic class TestController {\n\n\t\n\t// 测试登录接口， 浏览器访问： http://localhost:8081/test/login\n\t@Mapping(\"login\")\n\tpublic AjaxJson login(@Param(defaultValue=\"10001\") String id) {\n\t\tSystem.out.println(\"======================= 进入方法，测试登录接口 ========================= \");\n\t\tSystem.out.println(\"当前会话的token：\" + StpUtil.getTokenValue());\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginIdDefaultNull());\n\t\t\n\t\tStpUtil.login(id);\t\t\t// 在当前会话登录此账号 \t\n\t\tSystem.out.println(\"登录成功\");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginId());\n//\t\tSystem.out.println(\"当前登录账号并转为int：\" + StpUtil.getLoginIdAsInt());\n\t\tSystem.out.println(\"当前登录设备：\" + StpUtil.getLoginDevice());\n//\t\tSystem.out.println(\"当前token信息：\" + StpUtil.getTokenInfo());\t\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试退出登录 ， 浏览器访问： http://localhost:8081/test/logout\n\t@Mapping(\"logout\")\n\tpublic AjaxJson logout() {\n\t\tStpUtil.logout();\n//\t\tStpUtil.logoutByLoginId(10001);\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试角色接口， 浏览器访问： http://localhost:8081/test/testRole\n\t@Mapping(\"testRole\")\n\tpublic AjaxJson testRole() {\n\t\tSystem.out.println(\"======================= 进入方法，测试角色接口 ========================= \");\n\t\t\n\t\tSystem.out.println(\"是否具有角色标识 user \" + StpUtil.hasRole(\"user\"));\n\t\tSystem.out.println(\"是否具有角色标识 admin \" + StpUtil.hasRole(\"admin\"));\n\t\t\n\t\tSystem.out.println(\"没有admin权限就抛出异常\");\n\t\tStpUtil.checkRole(\"admin\");\n\t\t\n\t\tSystem.out.println(\"在【admin、user】中只要拥有一个就不会抛出异常\");\n\t\tStpUtil.checkRoleOr(\"admin\", \"user\");\n\n\t\tSystem.out.println(\"在【admin、user】中必须全部拥有才不会抛出异常\");\n\t\tStpUtil.checkRoleAnd(\"admin\", \"user\");\n\n\t\tSystem.out.println(\"角色测试通过\");\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试权限接口， 浏览器访问： http://localhost:8081/test/testJur\n\t@Mapping(\"testJur\")\n\tpublic AjaxJson testJur() {\n\t\tSystem.out.println(\"======================= 进入方法，测试权限接口 ========================= \");\n\t\t\n\t\tSystem.out.println(\"是否具有权限101\" + StpUtil.hasPermission(\"101\"));\n\t\tSystem.out.println(\"是否具有权限user-add\" + StpUtil.hasPermission(\"user-add\"));\n\t\tSystem.out.println(\"是否具有权限article-get\" + StpUtil.hasPermission(\"article-get\"));\n\t\t\n\t\tSystem.out.println(\"没有user-add权限就抛出异常\");\n\t\tStpUtil.checkPermission(\"user-add\");\n\t\t\n\t\tSystem.out.println(\"在【101、102】中只要拥有一个就不会抛出异常\");\n\t\tStpUtil.checkPermissionOr(\"101\", \"102\");\n\n\t\tSystem.out.println(\"在【101、102】中必须全部拥有才不会抛出异常\");\n\t\tStpUtil.checkPermissionAnd(\"101\", \"102\");\n\n\t\tSystem.out.println(\"权限测试通过\");\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试会话session接口， 浏览器访问： http://localhost:8081/test/session \n\t@Mapping(\"session\")\n\tpublic AjaxJson session()  {\n\t\tSystem.out.println(\"======================= 进入方法，测试会话session接口 ========================= \");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tStpUtil.getSession().set(\"name\", new Date());\t// 写入一个值 \n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tSystem.out.println( ONode.stringify(StpUtil.getSession()));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试自定义session接口， 浏览器访问： http://localhost:8081/test/session2 \n\t@Mapping(\"session2\")\n\tpublic AjaxJson session2() {\n\t\tSystem.out.println(\"======================= 进入方法，测试自定义session接口 ========================= \");\n\t\t// 自定义session就是无需登录也可以使用 的session ：比如拿用户的手机号当做 key， 来获取 session \n\t\tSystem.out.println(\"自定义 session的id为：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").getId());\n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\tSaSessionCustomUtil.getSessionById(\"1895544896\").set(\"name\", \"张三\");\t// 写入值 \n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// ---------- \n\t// 测试token专属session， 浏览器访问： http://localhost:8081/test/getTokenSession \n\t@Mapping(\"getTokenSession\")\n\tpublic AjaxJson getTokenSession() {\n\t\tSystem.out.println(\"======================= 进入方法，测试会话session接口 ========================= \");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前token专属session: \" + StpUtil.getTokenSession().getId());\n\n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getTokenSession().get(\"name\"));\n\t\tStpUtil.getTokenSession().set(\"name\", \"张三\");\t// 写入一个值 \n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getTokenSession().get(\"name\"));\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 打印当前token信息， 浏览器访问： http://localhost:8081/test/tokenInfo\n\t@Mapping(\"tokenInfo\")\n\tpublic AjaxJson tokenInfo() {\n\t\tSystem.out.println(\"======================= 进入方法，打印当前token信息 ========================= \");\n\t\tSaTokenInfo tokenInfo = StpUtil.getTokenInfo();\n\t\tSystem.out.println(tokenInfo);\n\t\treturn AjaxJson.getSuccessData(tokenInfo);\n\t}\n\t\n\t// 测试注解式鉴权， 浏览器访问： http://localhost:8081/test/atCheck\n\t@SaCheckLogin\t\t\t\t\t\t// 注解式鉴权：当前会话必须登录才能通过 \n\t@SaCheckRole(\"super-admin\")\t\t\t// 注解式鉴权：当前会话必须具有指定角色标识才能通过 \n\t@SaCheckPermission(\"user-add\")\t\t// 注解式鉴权：当前会话必须具有指定权限才能通过 \n\t@Mapping(\"atCheck\")\n\tpublic AjaxJson atCheck() {\n\t\tSystem.out.println(\"======================= 进入方法，测试注解鉴权接口 ========================= \");\n\t\tSystem.out.println(\"只有通过注解鉴权，才能进入此方法\");\n//\t\tStpUtil.checkActiveTimeout();\n//\t\tStpUtil.updateLastActiveToNow();\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试注解式鉴权， 浏览器访问： http://localhost:8081/test/atJurOr\n\t@Mapping(\"atJurOr\")\n\t@SaCheckPermission(value = {\"user-add\", \"user-all\", \"user-delete\"}, mode = SaMode.OR)\t\t// 注解式鉴权：只要具有其中一个权限即可通过校验 \n\tpublic AjaxJson atJurOr() {\n\t\treturn AjaxJson.getSuccessData(\"用户信息\");\n\t}\n\t\n\t// [活动时间] 续签： http://localhost:8081/test/rene\n\t@Mapping(\"rene\")\n\tpublic AjaxJson rene() {\n\t\tStpUtil.checkActiveTimeout();\n\t\tStpUtil.updateLastActiveToNow();\n\t\treturn AjaxJson.getSuccess(\"续签成功\");\n\t}\n\t\n\t// 测试踢人下线   浏览器访问： http://localhost:8081/test/kickOut \n\t@Mapping(\"kickOut\")\n\tpublic AjaxJson kickOut() {\n\t\t// 先登录上 \n\t\tStpUtil.login(10001);\n\t\t// 踢下线 \n\t\tStpUtil.kickout(10001);\n\t\t// 再尝试获取\n\t\tStpUtil.getLoginId();\n\t\t// 返回 \n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试登录接口, 按照设备类型登录， 浏览器访问： http://localhost:8081/test/login2\n\t@Mapping(\"login2\")\n\tpublic AjaxJson login2(@Param(defaultValue=\"10001\") String id, @Param(defaultValue=\"PC\") String device) {\n\t\tStpUtil.login(id, device);\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试身份临时切换： http://localhost:8081/test/switchTo\n\t@Mapping(\"switchTo\")\n\tpublic AjaxJson switchTo() {\n\t\tSystem.out.println(\"当前会话身份：\" + StpUtil.getLoginIdDefaultNull());\n\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\tStpUtil.switchTo(10044, () -> {\n\t\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\t\tSystem.out.println(\"当前会话身份已被切换为：\" + StpUtil.getLoginId());\n\t\t});\t\t\n\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试会话治理   浏览器访问： http://localhost:8081/test/search\n\t@Mapping(\"search\")\n\tpublic AjaxJson search() {\n\t\tSystem.out.println(\"--------------\");\n\t\tTtime t = new Ttime().start();\n\t\tList<String> tokenValue = StpUtil.searchTokenValue(\"8feb8265f773\", 0, 10, true);\n\t\tfor (String v : tokenValue) {\n//\t\t\tSaSession session = StpUtil.getSessionBySessionId(sid);\n\t\t\tSystem.out.println(v);\n\t\t}\n\t\tSystem.out.println(\"用时：\" + t.end().toString());\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试指定设备类型登录   浏览器访问： http://localhost:8081/test/loginByDevice\n\t@Mapping(\"loginByDevice\")\n\tpublic AjaxJson loginByDevice() {\n\t\tSystem.out.println(\"--------------\");\n\t\tStpUtil.login(10001, \"PC\");\n\t\treturn AjaxJson.getSuccessData(\"登录成功\");\n\t}\n\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@Mapping(\"test\")\n\tpublic AjaxJson test() {\n\t\tSystem.out.println(\"进来了\");\n\t\treturn AjaxJson.getSuccess(\"访问成功\");\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@Mapping(\"test2\")\n\tpublic AjaxJson test2() {\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\n/**\n * 登录测试\n * @author click33\n * @author noear\n */\n@Controller\n@Mapping(\"/user/\")\npublic class UserController {\n\n\t// 测试登录，浏览器访问： http://localhost:8081/user/doLogin?username=zhang&password=123456\n\t@Mapping(\"doLogin\")\n\tpublic String doLogin(String username, String password) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(username) && \"123456\".equals(password)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn \"登录成功\";\n\t\t}\n\t\treturn \"登录失败\";\n\t}\n\n\t// 查询登录状态，浏览器访问： http://localhost:8081/user/isLogin\n\t@Mapping(\"isLogin\")\n\tpublic String isLogin(String username, String password) {\n\t\treturn \"当前会话是否登录：\" + StpUtil.isLogin();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token:\n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志\n    is-log: true\n\n\n        \nsa-token-dao:\n    config: |\n        singleServerConfig:\n          password: \"123456\"\n          address: \"redis://localhost:6379\"\n          database: 0\n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-springboot</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token整合SpringAOP实现注解鉴权 -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-aop</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency> -->\n\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/SaTokenDemoApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * Sa-Token整合SpringBoot 示例 \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\n\n/**\n * 全局异常处理 \n */\n@ControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ResponseBody\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)\n\t\t\tthrows Exception {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace(); \n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} \n\t\telse if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} \n\t\telse if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} \n\t\telse if(e instanceof DisableServiceException) {\t// 如果是被封禁异常\n\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\taj = AjaxJson.getNotJur(\"账号被封禁：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t} \n\t\telse {\t// 普通异常, 输出：500 + 异常信息 \n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n        \t\t\t\n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\treturn AjaxJson.getError(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/AtController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpBasic;\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaCheckSafe;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 注解鉴权测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class AtController {\n\n\t// 登录认证，登录之后才可以进入方法  ---- http://localhost:8081/at/checkLogin \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 权限认证，具备user-add权限才可以进入方法  ---- http://localhost:8081/at/checkPermission \n\t@SaCheckPermission(\"user-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，同时具备所有权限才可以进入  ---- http://localhost:8081/at/checkPermissionAnd \n\t@SaCheckPermission({\"user-add\", \"user-delete\", \"user-update\"})\n\t@RequestMapping(\"checkPermissionAnd\")\n\tpublic SaResult checkPermissionAnd() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，只要具备其中一个就可以进入  ---- http://localhost:8081/at/checkPermissionOr \n\t@SaCheckPermission(value = {\"user-add\", \"user-delete\", \"user-update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermissionOr\")\n\tpublic SaResult checkPermissionOr() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色认证，只有具备admin角色才可以进入  ---- http://localhost:8081/at/checkRole \n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 完成二级认证  ---- http://localhost:8081/at/openSafe \n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(200); // 打开二级认证，有效期为200秒\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过二级认证后才可以进入  ---- http://localhost:8081/at/checkSafe \n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过Basic认证后才可以进入  ---- http://localhost:8081/at/checkBasic \n\t@SaCheckHttpBasic(account = \"sa:123456\")\n\t@RequestMapping(\"checkBasic\")\n\tpublic SaResult checkBasic() {\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.util.AjaxJson;\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 压力测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/s-test/\")\npublic class StressTestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@RequestMapping(\"login\")\n\tpublic AjaxJson login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport java.util.Date;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.pj.util.AjaxJson;\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.session.SaSessionCustomUtil;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试登录接口， 浏览器访问： http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic AjaxJson login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tSystem.out.println(\"======================= 进入方法，测试登录接口 ========================= \");\n\t\tSystem.out.println(\"当前会话的token：\" + StpUtil.getTokenValue());\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginIdDefaultNull());\n\t\t\n\t\tStpUtil.login(id);\t\t\t// 在当前会话登录此账号 \t\n\t\tSystem.out.println(\"登录成功\");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号：\" + StpUtil.getLoginId());\n//\t\tSystem.out.println(\"当前登录账号并转为int：\" + StpUtil.getLoginIdAsInt());\n\t\tSystem.out.println(\"当前登录设备：\" + StpUtil.getLoginDevice());\n//\t\tSystem.out.println(\"当前token信息：\" + StpUtil.getTokenInfo());\t\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试退出登录 ， 浏览器访问： http://localhost:8081/test/logout\n\t@RequestMapping(\"logout\")\n\tpublic AjaxJson logout() {\n\t\tStpUtil.logout();\n//\t\tStpUtil.logoutByLoginId(10001);\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试角色接口， 浏览器访问： http://localhost:8081/test/testRole\n\t@RequestMapping(\"testRole\")\n\tpublic AjaxJson testRole() {\n\t\tSystem.out.println(\"======================= 进入方法，测试角色接口 ========================= \");\n\t\t\n\t\tSystem.out.println(\"是否具有角色标识 user \" + StpUtil.hasRole(\"user\"));\n\t\tSystem.out.println(\"是否具有角色标识 admin \" + StpUtil.hasRole(\"admin\"));\n\t\t\n\t\tSystem.out.println(\"没有admin权限就抛出异常\");\n\t\tStpUtil.checkRole(\"admin\");\n\t\t\n\t\tSystem.out.println(\"在【admin、user】中只要拥有一个就不会抛出异常\");\n\t\tStpUtil.checkRoleOr(\"admin\", \"user\");\n\n\t\tSystem.out.println(\"在【admin、user】中必须全部拥有才不会抛出异常\");\n\t\tStpUtil.checkRoleAnd(\"admin\", \"user\");\n\n\t\tSystem.out.println(\"角色测试通过\");\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试权限接口， 浏览器访问： http://localhost:8081/test/testJur\n\t@RequestMapping(\"testJur\")\n\tpublic AjaxJson testJur() {\n\t\tSystem.out.println(\"======================= 进入方法，测试权限接口 ========================= \");\n\t\t\n\t\tSystem.out.println(\"是否具有权限101\" + StpUtil.hasPermission(\"101\"));\n\t\tSystem.out.println(\"是否具有权限user-add\" + StpUtil.hasPermission(\"user-add\"));\n\t\tSystem.out.println(\"是否具有权限article-get\" + StpUtil.hasPermission(\"article-get\"));\n\t\t\n\t\tSystem.out.println(\"没有user-add权限就抛出异常\");\n\t\tStpUtil.checkPermission(\"user-add\");\n\t\t\n\t\tSystem.out.println(\"在【101、102】中只要拥有一个就不会抛出异常\");\n\t\tStpUtil.checkPermissionOr(\"101\", \"102\");\n\n\t\tSystem.out.println(\"在【101、102】中必须全部拥有才不会抛出异常\");\n\t\tStpUtil.checkPermissionAnd(\"101\", \"102\");\n\n\t\tSystem.out.println(\"权限测试通过\");\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试会话session接口， 浏览器访问： http://localhost:8081/test/session \n\t@RequestMapping(\"session\")\n\tpublic AjaxJson session() throws JsonProcessingException {\n\t\tSystem.out.println(\"======================= 进入方法，测试会话session接口 ========================= \");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"当前登录账号session的id\" + StpUtil.getSession().getId());\n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tStpUtil.getSession().set(\"name\", new Date());\t// 写入一个值 \n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getSession().get(\"name\"));\n\t\tSystem.out.println( new ObjectMapper().writeValueAsString(StpUtil.getSession()));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试自定义session接口， 浏览器访问： http://localhost:8081/test/session2 \n\t@RequestMapping(\"session2\")\n\tpublic AjaxJson session2() {\n\t\tSystem.out.println(\"======================= 进入方法，测试自定义session接口 ========================= \");\n\t\t// 自定义session就是无需登录也可以使用 的session ：比如拿用户的手机号当做 key， 来获取 session \n\t\tSystem.out.println(\"自定义 session的id为：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").getId());\n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\tSaSessionCustomUtil.getSessionById(\"1895544896\").set(\"name\", \"张三\");\t// 写入值 \n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\tSystem.out.println(\"测试取值name：\" + SaSessionCustomUtil.getSessionById(\"1895544896\").get(\"name\"));\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// ---------- \n\t// 测试token专属session， 浏览器访问： http://localhost:8081/test/getTokenSession \n\t@RequestMapping(\"getTokenSession\")\n\tpublic AjaxJson getTokenSession() {\n\t\tSystem.out.println(\"======================= 进入方法，测试会话session接口 ========================= \");\n\t\tSystem.out.println(\"当前是否登录：\" + StpUtil.isLogin());\n\t\tSystem.out.println(\"当前token专属session: \" + StpUtil.getTokenSession().getId());\n\n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getTokenSession().get(\"name\"));\n\t\tStpUtil.getTokenSession().set(\"name\", \"张三\");\t// 写入一个值 \n\t\tSystem.out.println(\"测试取值name：\" + StpUtil.getTokenSession().get(\"name\"));\n\t\t\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 打印当前token信息， 浏览器访问： http://localhost:8081/test/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic AjaxJson tokenInfo() {\n\t\tSystem.out.println(\"======================= 进入方法，打印当前token信息 ========================= \");\n\t\tSaTokenInfo tokenInfo = StpUtil.getTokenInfo();\n\t\tSystem.out.println(tokenInfo);\n\t\treturn AjaxJson.getSuccessData(tokenInfo);\n\t}\n\t\n\t// 测试注解式鉴权， 浏览器访问： http://localhost:8081/test/atCheck\n\t@SaCheckLogin\t\t\t\t\t\t// 注解式鉴权：当前会话必须登录才能通过 \n\t@SaCheckRole(\"super-admin\")\t\t\t// 注解式鉴权：当前会话必须具有指定角色标识才能通过 \n\t@SaCheckPermission(\"user-add\")\t\t// 注解式鉴权：当前会话必须具有指定权限才能通过 \n\t@RequestMapping(\"atCheck\")\n\tpublic AjaxJson atCheck() {\n\t\tSystem.out.println(\"======================= 进入方法，测试注解鉴权接口 ========================= \");\n\t\tSystem.out.println(\"只有通过注解鉴权，才能进入此方法\");\n//\t\tStpUtil.checkActiveTimeout();\n//\t\tStpUtil.updateLastActiveToNow();\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试注解式鉴权， 浏览器访问： http://localhost:8081/test/atJurOr\n\t@RequestMapping(\"atJurOr\")\n\t@SaCheckPermission(value = {\"user-add\", \"user-all\", \"user-delete\"}, mode = SaMode.OR)\t\t// 注解式鉴权：只要具有其中一个权限即可通过校验 \n\tpublic AjaxJson atJurOr() {\n\t\treturn AjaxJson.getSuccessData(\"用户信息\");\n\t}\n\t\n\t// [活动时间] 续签： http://localhost:8081/test/rene\n\t@RequestMapping(\"rene\")\n\tpublic AjaxJson rene() {\n\t\tStpUtil.checkActiveTimeout();\n\t\tStpUtil.updateLastActiveToNow();\n\t\treturn AjaxJson.getSuccess(\"续签成功\");\n\t}\n\t\n\t// 测试踢人下线   浏览器访问： http://localhost:8081/test/kickOut \n\t@RequestMapping(\"kickOut\")\n\tpublic AjaxJson kickOut() {\n\t\t// 先登录上 \n\t\tStpUtil.login(10001);\n\t\t// 踢下线 \n\t\tStpUtil.kickout(10001);\n\t\t// 再尝试获取\n\t\tStpUtil.getLoginId();\n\t\t// 返回 \n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试登录接口, 按照设备登录， 浏览器访问： http://localhost:8081/test/login2\n\t@RequestMapping(\"login2\")\n\tpublic AjaxJson login2(@RequestParam(defaultValue=\"10001\") String id, @RequestParam(defaultValue=\"PC\") String device) {\n\t\tStpUtil.login(id, device);\n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试身份临时切换： http://localhost:8081/test/switchTo\n\t@RequestMapping(\"switchTo\")\n\tpublic AjaxJson switchTo() {\n\t\tSystem.out.println(\"当前会话身份：\" + StpUtil.getLoginIdDefaultNull());\n\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\tStpUtil.switchTo(10044, () -> {\n\t\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\t\tSystem.out.println(\"当前会话身份已被切换为：\" + StpUtil.getLoginId());\n\t\t});\t\t\n\t\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch()); \n\t\treturn AjaxJson.getSuccess();\n\t}\n\t\n\t// 测试会话治理   浏览器访问： http://localhost:8081/test/search\n\t@RequestMapping(\"search\")\n\tpublic AjaxJson search() {\n\t\tSystem.out.println(\"--------------\");\n\t\tTtime t = new Ttime().start();\n\t\tList<String> tokenValue = StpUtil.searchTokenValue(\"8feb8265f773\", 0, 10, true);\n\t\tfor (String v : tokenValue) {\n//\t\t\tSaSession session = StpUtil.getSessionBySessionId(sid);\n\t\t\tSystem.out.println(v);\n\t\t}\n\t\tSystem.out.println(\"用时：\" + t.end().toString());\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 测试指定设备登录   浏览器访问： http://localhost:8081/test/loginByDevice\n\t@RequestMapping(\"loginByDevice\")\n\tpublic AjaxJson loginByDevice() {\n\t\tSystem.out.println(\"--------------\");\n\t\tStpUtil.login(10001, \"PC\");\n\t\treturn AjaxJson.getSuccessData(\"登录成功\");\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic AjaxJson test() {\n\t\tSystem.out.println(\"------------进来了\"); \n\t\treturn AjaxJson.getSuccess(); \n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic AjaxJson test2() {\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-low-version/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-springboot-low-version</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<!-- 可以 -->\n\t\t<!--<version>2.2.0.RELEASE</version>-->\n\t\t<!-- 不可以 -->\n\t\t<version>2.1.18.RELEASE</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<java.run.main.class>com.pj.SaTokenApplication</java.run.main.class>\n\t\t<java.version>1.8</java.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-all</artifactId>\n\t\t\t<version>5.8.36</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t\t<!-- SpringBoot 版本过低时，需要追加的包 (低于2.2.0时，不包含2.2.0本身) -->\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-core</artifactId>\n\t\t\t<version>2.17.3</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-annotations</artifactId>\n\t\t\t<version>2.17.3</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t\t<version>2.17.3</version>\n\t\t</dependency>\n\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<!--<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>-->\n\n\t</dependencies>\n\n\t<!-- 构建配置 -->\n\t<build>\n\t\t<!-- 配置资源目录  -->\n\t\t<resources>\n\t\t\t<resource>\n\t\t\t\t<directory>src/main/java</directory>\n\t\t\t\t<includes>\n\t\t\t\t\t<include>**/*.xml</include>\n\t\t\t\t</includes>\n\t\t\t</resource>\n\t\t\t<resource>\n\t\t\t\t<directory>src/main/resources</directory>\n\t\t\t\t<includes>\n\t\t\t\t\t<include>**/*.*</include>\n\t\t\t\t</includes>\n\t\t\t</resource>\n\t\t</resources>\n\t\t<plugins>\n\t\t\t<!-- 打包jar文件时，配置manifest文件，加入lib包的jar依赖 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-jar-plugin</artifactId>\n\t\t\t\t<configuration>\n\t\t\t\t\t<archive>\n\t\t\t\t\t\t<manifest>\n\t\t\t\t\t\t\t<addClasspath>true</addClasspath>\n\t\t\t\t\t\t\t<classpathPrefix>lib/</classpathPrefix>\n\t\t\t\t\t\t\t<mainClass>${java.run.main.class}</mainClass>\n\t\t\t\t\t\t</manifest>\n\t\t\t\t\t</archive>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t\t<!-- 拷贝依赖的jar包到lib目录 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-dependency-plugin</artifactId>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<id>copy</id>\n\t\t\t\t\t\t<phase>package</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>copy-dependencies</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t<configuration>\n\t\t\t\t\t\t\t<outputDirectory>\n\t\t\t\t\t\t\t\t${project.build.directory}/lib\n\t\t\t\t\t\t\t</outputDirectory>\n\t\t\t\t\t\t</configuration>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/SaTokenApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n\n/**\n * Sa-Token 测试  \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n//\t\tSystem.out.println(StpUtil.getSessionByLoginId(10001));\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * 全局异常处理\n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace();\n\n\t\t// 返回给前端\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类\n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能\n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能\n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\n\t/**\n     * 注册 [Sa-Token 全局过滤器]\n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n\n        \t\t// 认证函数: 每次请求执行\n        \t\t.setAuth(obj -> {\n        \t\t\t// 输出 API 请求日志，方便调试代码\n        \t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n\n        \t\t})\n\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数\n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n\n        \t\t// 前置函数：在每次认证函数之前执行\n        \t\t.setBeforeAuth(obj -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称\n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以\n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探\n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n\n        \t\t\t// ---------- 设置跨域响应头 ----------\n        \t\t\t// 允许指定域访问跨域资源\n        \t\t\t.setHeader(\"Access-Control-Allow-Origin\", \"*\")\n        \t\t\t// 允许所有请求方式\n        \t\t\t.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n        \t\t\t// 有效时间\n        \t\t\t.setHeader(\"Access-Control-Max-Age\", \"3600\")\n        \t\t\t// 允许的header参数\n        \t\t\t.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n        \t\t\t// 如果是预检请求，则立即返回到前端\n        \t\t\tSaRouter.match(SaHttpMethod.OPTIONS)\n        \t\t\t\t.free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n        \t\t\t\t.back();\n        \t\t})\n        \t\t;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-low-version/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n/**\n * 登录测试\n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对\n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\tStpUtil.getTokenSession();\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 校验登录  ---- http://localhost:8081/acc/checkLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\tStpUtil.checkLogin();\n\t\treturn SaResult.ok();\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\n\t// 查询账号登录设备信息  ---- http://localhost:8081/acc/terminalInfo\n\t@RequestMapping(\"terminalInfo\")\n\tpublic SaResult terminalInfo() {\n\t\tSystem.out.println(\"账号 10001 登录设备信息：\");\n\t\tList<SaTerminalInfo> terminalList = StpUtil.getTerminalListByLoginId(10001);\n\t\tfor (SaTerminalInfo ter : terminalList) {\n\t\t\tSystem.out.println(\"登录index=\" + ter.getIndex() + \", 设备type=\" + ter.getDeviceType() + \", token=\" + ter.getTokenValue() + \", 登录time=\" + ter.getCreateTime());\n\t\t}\n\t\treturn SaResult.data(terminalList);\n\t}\n\n\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.login(10001, SaLoginParameter.create().setIsConcurrent(false));\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-low-version/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n        \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password:\n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-springboot-redis</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合  Redis (使用jdk默认序列化方式) -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency> -->\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/SaTokenDemoApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token整合SpringBoot 示例，整合redis\n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)\n\t\t\tthrows Exception {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace();\n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} \n\t\telse if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} \n\t\telse if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} \n\t\telse if(e instanceof DisableServiceException) {\t// 如果是被封禁异常\n\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\taj = AjaxJson.getNotJur(\"当前账号 \" + ee.getService() + \" 服务已被封禁 (level=\" + ee.getLevel() + \")：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t} \n\t\telse {\t// 普通异常, 输出：500 + 异常信息 \n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能\n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n        \t\t\t\n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行 （BeforeAuth不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/AtController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpBasic;\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaCheckSafe;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 注解鉴权测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class AtController {\n\n\t// 登录认证，登录之后才可以进入方法  ---- http://localhost:8081/at/checkLogin \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 权限认证，具备user-add权限才可以进入方法  ---- http://localhost:8081/at/checkPermission \n\t@SaCheckPermission(\"user-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，同时具备所有权限才可以进入  ---- http://localhost:8081/at/checkPermissionAnd \n\t@SaCheckPermission({\"user-add\", \"user-delete\", \"user-update\"})\n\t@RequestMapping(\"checkPermissionAnd\")\n\tpublic SaResult checkPermissionAnd() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，只要具备其中一个就可以进入  ---- http://localhost:8081/at/checkPermissionOr \n\t@SaCheckPermission(value = {\"user-add\", \"user-delete\", \"user-update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermissionOr\")\n\tpublic SaResult checkPermissionOr() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色认证，只有具备admin角色才可以进入  ---- http://localhost:8081/at/checkRole \n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 完成二级认证  ---- http://localhost:8081/at/openSafe \n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(200); // 打开二级认证，有效期为200秒\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过二级认证后才可以进入  ---- http://localhost:8081/at/checkSafe \n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过Basic认证后才可以进入  ---- http://localhost:8081/at/checkBasic \n\t@SaCheckHttpBasic(account = \"sa:123456\")\n\t@RequestMapping(\"checkBasic\")\n\tpublic SaResult checkBasic() {\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 压力测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/s-test/\")\npublic class StressTestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@RequestMapping(\"login\")\n\tpublic SaResult login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redis/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-springboot-redisson</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t\n\t\t<!-- Sa-Token整合 Redisson (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisson-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n        \n\t\t<!-- 无需提供Redis连接池 Redisson使用Netty管理 -->\n<!--\t\t<dependency>-->\n<!--            <groupId>org.apache.commons</groupId>-->\n<!--            <artifactId>commons-pool2</artifactId>-->\n<!--        </dependency>-->\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/SaTokenDemoApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * Sa-Token整合SpringBoot 示例，整合redis  \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)\n\t\t\tthrows Exception {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace(); \n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} \n\t\telse if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} \n\t\telse if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} \n\t\telse if(e instanceof DisableServiceException) {\t// 如果是被封禁异常\n\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\taj = AjaxJson.getNotJur(\"当前账号 \" + ee.getService() + \" 服务已被封禁 (level=\" + ee.getLevel() + \")：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t} \n\t\telse {\t// 普通异常, 输出：500 + 异常信息 \n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonConfig.java",
    "content": "package com.pj.redisson;\n\nimport org.redisson.config.SingleServerConfig;\nimport org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * redisson 配置\n *\n * @author 疯狂的狮子Li\n */\n@Configuration\n@EnableConfigurationProperties(RedissonProperties.class)\npublic class RedissonConfig {\n\n    @Autowired\n    private RedissonProperties redissonProperties;\n\n    /**\n     * 自定义Redisson配置注入器 被RedissonAutoConfiguration调用执行\n     * 具体参考 {@link org.redisson.spring.starter.RedissonAutoConfiguration}\n     * <p/>\n     * 使用自定义配置类手动注入配置数据\n     * 也可根据redisson官网使用properties文件配置\n     */\n    @Bean\n    public RedissonAutoConfigurationCustomizer redissonCustomizer() {\n        return config -> {\n            config.setThreads(redissonProperties.getThreads());\n            config.setNettyThreads(redissonProperties.getNettyThreads());\n            SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();\n            if (singleServerConfig != null) {\n                // 使用单机模式\n                config.useSingleServer()\n                    .setTimeout(singleServerConfig.getTimeout())\n                    .setClientName(singleServerConfig.getClientName())\n                    .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())\n                    .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())\n                    .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())\n                    .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());\n            }\n\n        };\n    }\n\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/redisson/RedissonProperties.java",
    "content": "package com.pj.redisson;\n\nimport org.redisson.config.SingleServerConfig;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.context.properties.NestedConfigurationProperty;\nimport org.springframework.stereotype.Component;\n\n/**\n * Redisson 配置属性\n *\n * @author 疯狂的狮子Li\n */\n@Component\n@ConfigurationProperties(prefix = \"redisson\")\npublic class RedissonProperties {\n\n    /**\n     * 线程池数量,默认值 = 当前处理核数量 * 2\n     */\n    private int threads;\n\n    /**\n     * Netty线程池数量,默认值 = 当前处理核数量 * 2\n     */\n    private int nettyThreads;\n\n    /**\n     * 单机服务配置\n     */\n    @NestedConfigurationProperty\n    private SingleServerConfig singleServerConfig;\n\n    public int getThreads() {\n        return threads;\n    }\n\n    public void setThreads(int threads) {\n        this.threads = threads;\n    }\n\n    public int getNettyThreads() {\n        return nettyThreads;\n    }\n\n    public void setNettyThreads(int nettyThreads) {\n        this.nettyThreads = nettyThreads;\n    }\n\n    public SingleServerConfig getSingleServerConfig() {\n        return singleServerConfig;\n    }\n\n    public void setSingleServerConfig(SingleServerConfig singleServerConfig) {\n        this.singleServerConfig = singleServerConfig;\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.util.SaResult;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// System.out.println(\"---------- sa全局认证 \" + SaHolder.getRequest().getRequestPath()); \n        \t\t\t\n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/AtController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.annotation.*;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 注解鉴权测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class AtController {\n\n\t// 登录认证，登录之后才可以进入方法  ---- http://localhost:8081/at/checkLogin \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 权限认证，具备user-add权限才可以进入方法  ---- http://localhost:8081/at/checkPermission \n\t@SaCheckPermission(\"user-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，同时具备所有权限才可以进入  ---- http://localhost:8081/at/checkPermissionAnd \n\t@SaCheckPermission({\"user-add\", \"user-delete\", \"user-update\"})\n\t@RequestMapping(\"checkPermissionAnd\")\n\tpublic SaResult checkPermissionAnd() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，只要具备其中一个就可以进入  ---- http://localhost:8081/at/checkPermissionOr \n\t@SaCheckPermission(value = {\"user-add\", \"user-delete\", \"user-update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermissionOr\")\n\tpublic SaResult checkPermissionOr() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色认证，只有具备admin角色才可以进入  ---- http://localhost:8081/at/checkRole \n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 完成二级认证  ---- http://localhost:8081/at/openSafe \n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(200); // 打开二级认证，有效期为200秒\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过二级认证后才可以进入  ---- http://localhost:8081/at/checkSafe \n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过Basic认证后才可以进入  ---- http://localhost:8081/at/checkBasic \n\t@SaCheckHttpBasic(account = \"sa:123456\")\n\t@RequestMapping(\"checkBasic\")\n\tpublic SaResult checkBasic() {\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 压力测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/s-test/\")\npublic class StressTestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@RequestMapping(\"login\")\n\tpublic SaResult login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\"); \n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot-redisson/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置\n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: localhost\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码 为空需注释掉\n        # password:\n        # 连接超时时间\n        timeout: 10s\n\nredisson:\n    # 线程池数量\n    threads: 8\n    # Netty线程池数量\n    nettyThreads: 32\n    # 单节点配置\n    singleServerConfig:\n        # 客户端名称\n        clientName: test\n        # 最小空闲连接数\n        connectionMinimumIdleSize: 8\n        # 连接池大小\n        connectionPoolSize: 32\n        # 连接空闲超时，单位：毫秒\n        idleConnectionTimeout: 10000\n        # 命令等待超时，单位：毫秒\n        timeout: 3000\n        # 如果尝试在此限制之内发送成功，则开始启用 timeout 计时。\n        retryAttempts: 3\n        # 命令重试发送时间间隔，单位：毫秒\n        retryInterval: 1500\n        # 发布和订阅连接池大小\n        subscriptionConnectionPoolSize: 50\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-springboot3-redis</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>3.5.11</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot3-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合  Redis (使用jdk默认序列化方式) -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency> -->\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/SaTokenSpringBoot3Application.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token 整合 SpringBoot3 示例，整合redis  \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenSpringBoot3Application {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenSpringBoot3Application.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)\n\t\t\tthrows Exception {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace(); \n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} \n\t\telse if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} \n\t\telse if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} \n\t\telse if(e instanceof DisableServiceException) {\t// 如果是被封禁异常\n\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\taj = AjaxJson.getNotJur(\"当前账号 \" + ee.getService() + \" 服务已被封禁 (level=\" + ee.getLevel() + \")：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t} \n\t\telse {\t// 普通异常, 输出：500 + 异常信息 \n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.util.SaResult;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// System.out.println(\"---------- sa全局认证 \" + SaHolder.getRequest().getRequestPath()); \n        \t\t\t\n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/AtController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpBasic;\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaCheckSafe;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 注解鉴权测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class AtController {\n\n\t// 登录认证，登录之后才可以进入方法  ---- http://localhost:8081/at/checkLogin \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 权限认证，具备user-add权限才可以进入方法  ---- http://localhost:8081/at/checkPermission \n\t@SaCheckPermission(\"user-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，同时具备所有权限才可以进入  ---- http://localhost:8081/at/checkPermissionAnd \n\t@SaCheckPermission({\"user-add\", \"user-delete\", \"user-update\"})\n\t@RequestMapping(\"checkPermissionAnd\")\n\tpublic SaResult checkPermissionAnd() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，只要具备其中一个就可以进入  ---- http://localhost:8081/at/checkPermissionOr \n\t@SaCheckPermission(value = {\"user-add\", \"user-delete\", \"user-update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermissionOr\")\n\tpublic SaResult checkPermissionOr() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色认证，只有具备admin角色才可以进入  ---- http://localhost:8081/at/checkRole \n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 完成二级认证  ---- http://localhost:8081/at/openSafe \n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(200); // 打开二级认证，有效期为200秒\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过二级认证后才可以进入  ---- http://localhost:8081/at/checkSafe \n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过Basic认证后才可以进入  ---- http://localhost:8081/at/checkBasic \n\t@SaCheckHttpBasic(account = \"sa:123456\")\n\t@RequestMapping(\"checkBasic\")\n\tpublic SaResult checkBasic() {\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 压力测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/s-test/\")\npublic class StressTestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@RequestMapping(\"login\")\n\tpublic SaResult login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\tSystem.out.println(SpringMVCUtil.getRequest());\n\t\tSystem.out.println(SaTokenContextJakartaServletUtil.getRequest());\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8081/test/getRequestPath\n\t@RequestMapping(\"getRequestPath\")\n\tpublic SaResult getRequestPath() {\n\t\tSystem.out.println(\"-------------- 测试请求 path 获取\");\n\t\tSystem.out.println(\"request.getRequestURI() \" + SpringMVCUtil.getRequest().getRequestURI());\n\t\tSystem.out.println(\"saRequest.getRequestPath() \" + SaHolder.getRequest().getRequestPath());\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot3-redis/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    data: \n        # redis配置 \n        redis:\n            # Redis数据库索引（默认为0）\n            database: 1\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password: \n            # 连接超时时间\n            timeout: 10s\n            lettuce:\n                pool:\n                    # 连接池最大连接数\n                    max-active: 200\n                    # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                    max-wait: -1ms\n                    # 连接池中的最大空闲连接\n                    max-idle: 10\n                    # 连接池中的最小空闲连接\n                    min-idle: 0\n            \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-springboot4-redis</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot 4 -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>4.0.3</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot 4 依赖：webmvc 替代 deprecated 的 starter-web，aspectj 替代 starter-aop -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-webmvc</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aspectj</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot4-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token整合 Redis  -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\n\t</dependencies>\n\t\n\t\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/SaTokenSpringBoot4Application.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token 整合 SpringBoot4 示例，整合 redis  \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenSpringBoot4Application {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenSpringBoot4Application.class, args); \n\t\tSystem.out.println(\"\\n🎉 启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)\n\t\t\tthrows Exception {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace(); \n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} \n\t\telse if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} \n\t\telse if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} \n\t\telse if(e instanceof DisableServiceException) {\t// 如果是被封禁异常\n\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\taj = AjaxJson.getNotJur(\"当前账号 \" + ee.getService() + \" 服务已被封禁 (level=\" + ee.getLevel() + \")：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t} \n\t\telse {\t// 普通异常, 输出：500 + 异常信息 \n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "//package com.pj.current;\n//\n//import java.io.IOException;\n//\n//import org.springframework.boot.web.servlet.error.ErrorController;\n//import org.springframework.web.bind.annotation.RequestMapping;\n//import org.springframework.web.bind.annotation.RestController;\n//\n//import cn.dev33.satoken.util.SaResult;\n//import jakarta.servlet.http.HttpServletRequest;\n//import jakarta.servlet.http.HttpServletResponse;\n//\n///**\n// * 处理 404\n// * @author click33\n// */\n//@RestController\n//public class NotFoundHandle implements ErrorController {\n//\n//\t@RequestMapping(\"/error\")\n//    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n//\t\tresponse.setStatus(200);\n//        return SaResult.get(404, \"not found\", null);\n//    }\n//\n//}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.util.SaResult;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能  \n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\t\n\t/**\n     * 注册 [Sa-Token 全局过滤器] \n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n        \t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\t// System.out.println(\"---------- sa全局认证 \" + SaHolder.getRequest().getRequestPath()); \n        \t\t\t\n        \t\t})\n        \t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t\n        \t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/AtController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpBasic;\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaCheckSafe;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 注解鉴权测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class AtController {\n\n\t// 登录认证，登录之后才可以进入方法  ---- http://localhost:8082/at/checkLogin \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 权限认证，具备user-add权限才可以进入方法  ---- http://localhost:8082/at/checkPermission \n\t@SaCheckPermission(\"user-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，同时具备所有权限才可以进入  ---- http://localhost:8082/at/checkPermissionAnd \n\t@SaCheckPermission({\"user-add\", \"user-delete\", \"user-update\"})\n\t@RequestMapping(\"checkPermissionAnd\")\n\tpublic SaResult checkPermissionAnd() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，只要具备其中一个就可以进入  ---- http://localhost:8082/at/checkPermissionOr \n\t@SaCheckPermission(value = {\"user-add\", \"user-delete\", \"user-update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermissionOr\")\n\tpublic SaResult checkPermissionOr() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色认证，只有具备admin角色才可以进入  ---- http://localhost:8082/at/checkRole \n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 完成二级认证  ---- http://localhost:8082/at/openSafe \n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(200); // 打开二级认证，有效期为200秒\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过二级认证后才可以进入  ---- http://localhost:8082/at/checkSafe \n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过Basic认证后才可以进入  ---- http://localhost:8082/at/checkBasic \n\t@SaCheckHttpBasic(account = \"sa:123456\")\n\t@RequestMapping(\"checkBasic\")\n\tpublic SaResult checkBasic() {\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/FaviconController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class FaviconController {\n\n    @RequestMapping(\"/favicon.ico\")\n    public String favicon() {\n        return \"\";\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8082/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8082/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8082/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8082/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 压力测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/s-test/\")\npublic class StressTestController {\n\n\t// 测试   浏览器访问： http://localhost:8082/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@RequestMapping(\"login\")\n\tpublic SaResult login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试   浏览器访问： http://localhost:8082/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\tSystem.out.println(SpringMVCUtil.getRequest());\n\t\tSystem.out.println(SaTokenContextJakartaServletUtil.getRequest());\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8082/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8082/test/getRequestPath\n\t@RequestMapping(\"getRequestPath\")\n\tpublic SaResult getRequestPath() {\n\t\tSystem.out.println(\"-------------- 测试请求 path 获取\");\n\t\tSystem.out.println(\"request.getRequestURI() \" + SpringMVCUtil.getRequest().getRequestURI());\n\t\tSystem.out.println(\"saRequest.getRequestPath() \" + SaHolder.getRequest().getRequestPath());\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-springboot4-redis/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8082\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    data: \n        # redis配置 \n        redis:\n            # Redis数据库索引（默认为0）\n            database: 1\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password: \n            # 连接超时时间\n            timeout: 10s\n            lettuce:\n                pool:\n                    # 连接池最大连接数\n                    max-active: 200\n                    # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                    max-wait: -1ms\n                    # 连接池中的最大空闲连接\n                    max-idle: 10\n                    # 连接池中的最小空闲连接\n                    min-idle: 0\n            \n        \n        \n        \n        \n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sse</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.7.18</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<java.run.main.class>com.pj.SaTokenSseApplication</java.run.main.class>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-all</artifactId>\n\t\t\t<version>5.8.36</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t</dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n\n/**\n * Sa-Token 测试  \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenSseApplication {\n\n\t// SSE 连接测试在线工具：https://toolshu.com/sse\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenSseApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类\n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能\n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\n\t/**\n\t * CORS 跨域处理\n\t */\n\t@Bean\n\tpublic SaCorsHandleFunction corsHandle() {\n\t\treturn (req, res, sto) -> {\n\t\t\tres.\n\t\t\t\t\t// 允许指定域访问跨域资源\n\t\t\t\t\tsetHeader(\"Access-Control-Allow-Origin\", \"*\")\n\t\t\t\t\t// 允许所有请求方式\n\t\t\t\t\t.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n\t\t\t\t\t// 有效时间\n\t\t\t\t\t.setHeader(\"Access-Control-Max-Age\", \"3600\")\n\t\t\t\t\t// 允许的header参数\n\t\t\t\t\t.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n\t\t\t// 如果是预检请求，则立即返回到前端\n\t\t\tSaRouter.match(SaHttpMethod.OPTIONS)\n\t\t\t\t\t.free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n\t\t\t\t\t.back();\n\t\t};\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.util.SseEmitterHolder;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?uid=10001\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(@RequestParam(defaultValue = \"10001\") long uid) {\n\t\tStpUtil.login(uid);\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tif(StpUtil.isLogin()) {\n\t\t\tlong uid = StpUtil.getLoginIdAsLong();\n\t\t\tSseEmitterHolder.closeByUid(uid);\n\t\t\tStpUtil.logout();\n\t\t}\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.util.SseEmitterHolder;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * SSE 推送\n */\n@RestController\npublic class SseAdminController {\n\n    // 推送消息   --- http://localhost:8081/sse/send?uid=10001&message=hello123\n    @RequestMapping(value = \"/sse/send\")\n    public SaResult sendMessage(long uid, String message) {\n        SseEmitterHolder.sendMessageByUid(uid, message);\n        return SaResult.ok();\n    }\n\n    // 断开   --- http://localhost:8081/sse/close?uid=10001\n    @RequestMapping(value = \"/sse/close\")\n    public SaResult close(long uid){\n        SseEmitterHolder.closeByUid(uid);\n        return SaResult.ok();\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java",
    "content": "package com.pj.test;\n\nimport com.pj.util.SseEmitterHolder;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\n/**\n * SSE 连接\n */\n@RestController\npublic class SseController {\n\n\n    // 创建连接   --- http://localhost:8081/sse?satoken=d8a8e1c7-62a4-4656-8b54-cc14e6348ceb\n    @RequestMapping(value = \"/sse\", produces = MediaType.TEXT_EVENT_STREAM_VALUE)\n    public SseEmitter createSse(String satoken) {\n        return SseEmitterHolder.createSse(satoken);\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java",
    "content": "package com.pj.util;\n\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.springframework.web.servlet.mvc.method.annotation.SseEmitter;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * SSE 连接管理器\n *\n * @author click33\n * @since 2025/4/11\n */\npublic class SseEmitterHolder {\n\n    public static final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();\n\n    /**\n     * 创建客户端\n     */\n    public static SseEmitter createSse(String satoken) {\n\n        Object loginId = StpUtil.getLoginIdByToken(satoken);\n        if(loginId == null) {\n            throw new NotLoginException(\"无效 token\", StpUtil.TYPE, NotLoginException.INVALID_TOKEN);\n        }\n        long uid = SaFoxUtil.getValueByType(loginId, Long.class);\n\n        // 默认 30 秒超时，设置为 0L 则永不超时\n        SseEmitter sseEmitter = new SseEmitter(600 * 1000L);\n        sseEmitterMap.put(satoken, sseEmitter);\n        System.out.println(\"连接成功：satoken=\" + satoken + \"，uid=\" + uid);\n\n        // 完成后回调\n        sseEmitter.onCompletion(() -> {\n            System.out.println(\"结束连接：satoken=\" + satoken + \"，uid=\" + uid);\n            sseEmitterMap.remove(satoken);\n        });\n\n        //超时回调\n        sseEmitter.onTimeout(() -> {\n            System.out.println(\"连接超时：satoken=\" + satoken + \"，uid=\" + uid);\n        });\n\n        //异常回调\n        sseEmitter.onError( e -> {\n//            try {\n                System.out.println(\"连接异常：satoken=\" + satoken + \"，uid=\" + uid);\n                System.err.println(e.getMessage());\n//                sseEmitter.send(SseEmitter.event()\n//                        .id(String.valueOf(uid))\n//                        .name(\"发生异常！\")\n//                        .data(\"发生异常请重试！\")\n//                        .reconnectTime(3000));\n//                sseEmitterMap.put(uid, sseEmitter);\n//            } catch (IOException ee) {\n//                ee.printStackTrace();\n//            }\n        });\n        try {\n            sseEmitter.send(SseEmitter.event().reconnectTime(5000));\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n\n        return sseEmitter;\n    }\n\n    /**\n     * 给指定 token 客户端发送消息\n     *\n     */\n    public static void sendMessageByToken(String satoken, String message) {\n        SseEmitter sseEmitter = sseEmitterMap.get(satoken);\n        if (sseEmitter == null) {\n            System.out.println(\"该 token 暂未建立连接：\" + satoken);\n            return;\n        }\n        try {\n            sseEmitter.send(SseEmitter.event().reconnectTime(60 * 1000L).data(message));\n            System.out.println(\"消息推送成功，token=\" + satoken + \", message=\" + message);\n        }catch (Exception e) {\n            e.printStackTrace();\n//            sseEmitterMap.remove(uid);\n//            log.info(\"用户{},消息id:{},推送异常:{}\", uid,messageId, e.getMessage());\n//            sseEmitter.complete();\n        }\n    }\n\n    /**\n     * 给指定 用户 所有客户端发送消息\n     *\n     */\n    public static void sendMessageByUid(long uid, String message) {\n        List<String> tokenList = StpUtil.getTokenValueListByLoginId(uid);\n        for (String token : tokenList) {\n            sendMessageByToken(token, message);\n        }\n    }\n\n    /**\n     * 指定 token 断开连接\n     *\n     */\n    public static void closeByToken(String satoken) {\n        SseEmitter sseEmitter = sseEmitterMap.get(satoken);\n        if (sseEmitter == null) {\n            System.out.println(\"该 token 暂未建立连接：\" + satoken);\n            return;\n        }\n        try {\n            sendMessageByToken(satoken, \"连接已断开！\");\n            sseEmitter.complete();\n            System.out.println(\"连接已断开，token=\" + satoken);\n        }catch (Exception e) {\n            e.printStackTrace();\n            // sseEmitterMap.remove(uid);\n            // log.info(\"用户{},消息id:{},推送异常:{}\", uid,messageId, e.getMessage());\n            // sseEmitter.complete();\n        }\n    }\n\n    /**\n     * 指定 uid 断开连接\n     *\n     */\n    public static void closeByUid(long uid) {\n        List<String> tokenList = StpUtil.getTokenValueListByLoginId(uid);\n        for (String token : tokenList) {\n            closeByToken(token);\n        }\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n        \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password:\n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/README.md",
    "content": "\n## SSM 架构集成 Sa-Token 示例\n\n说是SSM，其实没有M，仅仅是给使用 SpringMVC 非 SpringBoot 的项目提供一个简单的 Sa-Token 集成示例。\n\n直接运行项目即可，里面注释挺全的，也不必做过多说明了\n（其实就是我懒，光搭建起来这个架子就累瘫了，各种版本兼容问题报起错来大汗淋漓，推荐新项目能上 SpringBoot 就赶紧上吧，千万别在SSM上浪费生命）。\n\n推荐 jdk8 + tomcat8。\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-ssm</artifactId>\n\t<packaging>war</packaging>\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-compiler-plugin</artifactId>\n\t\t\t\t<configuration>\n\t\t\t\t\t<source>8</source>\n\t\t\t\t\t<target>8</target>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n\t\t<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n\t\t<!--<spring.version>4.2.5.RELEASE</spring.version>-->\n\t\t<spring.version>5.3.7</spring.version>\n\t\t<jackson.version>2.16.1</jackson.version>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- Servlet API -->\n\t\t<dependency>\n\t\t\t<groupId>javax.servlet</groupId>\n\t\t\t<artifactId>javax.servlet-api</artifactId>\n\t\t\t<version>3.1.0</version>\n\t\t</dependency>\n\n\t\t<!-- Spring 依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-core</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-web</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-oxm</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-tx</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-webmvc</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-context</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-context-support</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-aop</artifactId>\n\t\t\t<version>${spring.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Jackson 依赖  -->\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-core</artifactId>\n\t\t\t<version>${jackson.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t\t<version>${jackson.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证，在线文档：https://sa-token.cc -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t\t<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 整合 Redis （使用 jackson 序列化方式） -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-jackson</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>redis.clients</groupId>\n\t\t\t<artifactId>jedis</artifactId>\n\t\t\t<version>3.8.0</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.data</groupId>\n\t\t\t<artifactId>spring-data-redis</artifactId>\n\t\t\t<version>2.3.9.RELEASE</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.datatype</groupId>\n\t\t\t<artifactId>jackson-datatype-jsr310</artifactId>\n\t\t\t<version>2.11.2</version>\n\t\t</dependency>\n\n\t</dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/AtController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.annotation.*;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 注解鉴权测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class AtController {\n\n\t// 登录认证，登录之后才可以进入方法  ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkLogin\n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 权限认证，具备user-add权限才可以进入方法  ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermission\n\t@SaCheckPermission(\"user-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，同时具备所有权限才可以进入  ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermissionAnd\n\t@SaCheckPermission({\"user-add\", \"user-delete\", \"user-update\"})\n\t@RequestMapping(\"checkPermissionAnd\")\n\tpublic SaResult checkPermissionAnd() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，只要具备其中一个就可以进入  ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkPermissionOr\n\t@SaCheckPermission(value = {\"user-add\", \"user-delete\", \"user-update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermissionOr\")\n\tpublic SaResult checkPermissionOr() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色认证，只有具备admin角色才可以进入  ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkRole\n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 完成二级认证  ---- http://localhost:8080/sa_token_demo_ssm_war/at/openSafe\n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(200); // 打开二级认证，有效期为200秒\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过二级认证后才可以进入  ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkSafe\n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过Basic认证后才可以进入  ---- http://localhost:8080/sa_token_demo_ssm_war/at/checkBasic\n\t@SaCheckHttpBasic(account = \"sa:123456\")\n\t@RequestMapping(\"checkBasic\")\n\tpublic SaResult checkBasic() {\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/LoginController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8080/sa_token_demo_ssm_war/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\tSystem.out.println(\"-------- 12344\");\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8080/sa_token_demo_ssm_war/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 校验登录  ---- http://localhost:8080/sa_token_demo_ssm_war/acc/checkLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\tStpUtil.checkLogin();\n\t\treturn SaResult.ok();\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8080/sa_token_demo_ssm_war/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8080/sa_token_demo_ssm_war/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/PageController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\n/**\n * 页面访问测试\n * @author click33\n * @since 2024/4/14\n */\n@Controller\npublic class PageController {\n\n    // http://localhost:8080/sa_token_demo_ssm_war/home\n    @RequestMapping(\"/home\")\n    public String index() {\n        System.out.println(\"------- home页，所有游客可访问\");\n        return \"home\";\n    }\n\n    // http://localhost:8080/sa_token_demo_ssm_war/user\n    @RequestMapping(\"/user\")\n    public String user() {\n        System.out.println(\"------- user页，登录后才能访问\");\n        StpUtil.checkLogin();\n        return \"user\";\n    }\n\n    // http://localhost:8080/sa_token_demo_ssm_war/admin\n    @RequestMapping(\"/admin\")\n    public String admin() {\n        System.out.println(\"------- admin页，具有admin角色才能访问\");\n        StpUtil.checkRole(\"admin\");\n        return \"admin\";\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/controller/TestController.java",
    "content": "package com.pj.controller;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.stp.SaLoginConfig;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.Date;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试登录  ---- http://localhost:8080/sa_token_demo_ssm_war/test/login\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue = \"10001\") long id) {\n\t\tStpUtil.login(id, SaLoginConfig.setActiveTimeout(-1));\n\t\treturn SaResult.ok(\"登录成功\");\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8080/sa_token_demo_ssm_war/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了 \" + SaFoxUtil.formatDate(new Date()));\n\t\t// StpUtil.getLoginId();\n\t\t// 返回\n\t\treturn SaResult.data(null);\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8080/sa_token_demo_ssm_war/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8080/sa_token_demo_ssm_war/getRequestPath\n\t@RequestMapping(\"getRequestPath\")\n\tpublic SaResult getRequestPath() {\n\t\tSystem.out.println(\"------------ 测试访问路径获取 \");\n//\t\tSystem.out.println(\"SpringMVCUtil.getRequest().getRequestURI()  \" + SpringMVCUtil.getRequest().getRequestURI());\n\t\tSystem.out.println(\"SaHolder.getRequest().getRequestPath()  \" + SaHolder.getRequest().getRequestPath());\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n\n/**\n * 全局异常处理\n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.err.println(\"全局异常---------------\");\n\t\te.printStackTrace();\n\n\t\t// 返回给前端\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * 处理 404\n * @author click33\n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/model/SysUser.java",
    "content": "package com.pj.model;\n\n/**\n * User 实体类\n *\n * @author click33\n * @since 2022-10-15\n */\npublic class SysUser {\n\n\tpublic SysUser() {}\n\n\tpublic SysUser(long id, String name, int age) {\n\t\tsuper();\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.age = age;\n\t}\n\n\n\t/**\n\t * 用户id\n\t */\n\tprivate long id;\n\n\t/**\n\t * 用户名称\n\t */\n\tprivate String name;\n\n\t/**\n\t * 用户年龄\n\t */\n\tprivate int age;\n\n\t/**\n\t * @return id\n\t */\n\tpublic long getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id 要设置的 id\n\t */\n\tpublic void setId(long id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return name\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 要设置的 name\n\t */\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return age\n\t */\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\t/**\n\t * @param age 要设置的 age\n\t */\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SysUser [id=\" + id + \", name=\" + name + \", age=\" + age + \"]\";\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaInterceptorImpl.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\n\n/**\n * 路由拦截鉴权测试\n * @author click33\n * @since 2024/4/15\n */\npublic class SaInterceptorImpl extends SaInterceptor {\n\n    public SaInterceptorImpl() {\n        super(hadnle->{\n            System.out.println(\"-------------- SA 路由拦截鉴权，你访问的是：\" + SaHolder.getRequest().getRequestPath());\n            // System.out.println(\"你访问的是：\" + SaHolder.getRequest().getRequestPath());\n            // SaRouter.match(\"/test/test\", r -> StpUtil.checkLogin());\n\n            // 根据路由划分模块，不同模块不同鉴权\n//            SaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n//            SaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n//            SaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n//            SaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n//            SaRouter.match(\"/notice/**\", r -> StpUtil.checkPermission(\"notice\"));\n//            SaRouter.match(\"/comment/**\", r -> StpUtil.checkPermission(\"comment\"));\n\n            // 更多写法参考：https://sa-token.cc/doc.html#/use/route-check\n\n        });\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/SaTokenBeanInjection.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate;\nimport cn.dev33.satoken.json.SaJsonTemplateForJackson;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.plugin.SaTokenPluginHolder;\nimport cn.dev33.satoken.spring.SaBeanInject;\nimport cn.dev33.satoken.spring.SaTokenContextForSpring;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\n\n/**\n * 手动注入 Sa-Token 所需要的组件\n * @author click33\n * @since 2024/4/15\n */\npublic class SaTokenBeanInjection {\n\n    public SaTokenBeanInjection(\n            SaLog log,\n            SaTokenConfig config,\n            @Autowired(required = false) SaTokenPluginHolder pluginHolder,\n            RedisConnectionFactory connectionFactory,\n            String routePrefix\n    ) {\n        System.out.println(\"---------------- 手动注入 Sa-Token 所需要的组件 start ----------------\");\n\n        // 日志组件、配置信息\n        SaBeanInject inject = new SaBeanInject(log, config, pluginHolder);\n\n        // 基于 Spring 的上下文处理器\n        inject.setSaTokenContext(new SaTokenContextForSpring());\n\n        // 基于 Jackson 的 json解析器\n        inject.setSaJsonTemplate(new SaJsonTemplateForJackson());\n\n        // 基于 Jackson 序列化的 Redis 持久化组件\n        SaTokenDaoForRedisTemplate saTokenDaoForRedisTemplate = new SaTokenDaoForRedisTemplate();\n        saTokenDaoForRedisTemplate.init(connectionFactory);\n        inject.setSaTokenDao(saTokenDaoForRedisTemplate);\n\n        // 权限和角色数据\n        inject.setStpInterface(new StpInterfaceImpl());\n\n        // 项目路由前缀，方便路由拦截鉴权的\n        ApplicationInfo.routePrefix = routePrefix;\n        // System.out.println(routePrefix);\n\n        // 注入更多组件 ....\n        // inject.setXxx\n\n        System.out.println(\"---------------- 手动注入 Sa-Token 所需要的组件 end ----------------\");\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 自定义权限验证接口扩展 \n */\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n        \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/resources/applicationContext.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<beans xmlns=\"http://www.springframework.org/schema/beans\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txmlns:context=\"http://www.springframework.org/schema/context\"\n\txmlns:tx=\"http://www.springframework.org/schema/tx\"\n\txmlns:aop=\"http://www.springframework.org/schema/aop\"\n\txmlns:p=\"http://www.springframework.org/schema/p\"\n\txsi:schemaLocation=\n\t\t\"http://www.springframework.org/schema/beans\n\t\thttp://www.springframework.org/schema/beans/spring-beans-3.2.xsd\n\t\thttp://www.springframework.org/schema/context \n\t\thttp://www.springframework.org/schema/context/spring-context-3.2.xsd\n\t\thttp://www.springframework.org/schema/tx\n\t\thttp://www.springframework.org/schema/tx/spring-tx-3.2.xsd\n\t\thttp://www.springframework.org/schema/aop\n\t\thttp://www.springframework.org/schema/aop/spring-aop-3.2.xsd\">\n\n\t<!-- 导入配置 -->\n\t<import resource=\"spring-sa-token.xml\" />\n\t<import resource=\"spring-redis.xml\" />\n\n</beans>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-mvc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<beans xmlns=\"http://www.springframework.org/schema/beans\"\n       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n       xmlns:mvc=\"http://www.springframework.org/schema/mvc\"\n       xmlns:context=\"http://www.springframework.org/schema/context\"\n       xsi:schemaLocation=\"\n\t\thttp://www.springframework.org/schema/beans\n        \thttp://www.springframework.org/schema/beans/spring-beans.xsd\n        \thttp://www.springframework.org/schema/context\n       \t\thttp://www.springframework.org/schema/context/spring-context.xsd\n        \thttp://www.springframework.org/schema/mvc\n        \thttp://www.springframework.org/schema/mvc/spring-mvc.xsd\">\n\n    <!--避免IE执行AJAX时，返回JSON出现下载文件 -->\n    <bean id=\"mappingJackson2HttpMessageConverter\"\n          class=\"org.springframework.http.converter.json.MappingJackson2HttpMessageConverter\">\n        <property name=\"supportedMediaTypes\">\n            <list>\n                <value>text/html;charset=UTF-8</value>\n                <value>text/json;charset=UTF-8</value>\n                <value>application/json;charset=UTF-8</value>\n            </list>\n        </property>\n    </bean>\n\n    <!-- 添加扫描注解，此包下 -->\n    <context:component-scan base-package=\"com.pj.controller,com.pj.current\" />\n    <mvc:annotation-driven/>\n\n    <!-- 拦截器 -->\n    <mvc:interceptors>\n\n        <!-- 注解鉴权拦截器 -->\n        <!-- 想要使用注解鉴权，需要打开这个 -->\n        <mvc:interceptor>\n            <mvc:mapping path=\"/**\" />\n            <bean class=\"cn.dev33.satoken.interceptor.SaInterceptor\" />\n        </mvc:interceptor>\n\n        <!-- 路由鉴权拦截器 -->\n        <!--\n            注意：这里的 [路由拦截鉴权] 和上面的 [注解鉴权拦截器] 只能打开一个，\n            因为 SaInterceptor 自带注解鉴权效果，如果两个都打开了，会导致一个注解被拦截校验两次\n         -->\n        <!--<mvc:interceptor>\n            <mvc:mapping path=\"/**\" />\n            <bean class=\"com.pj.satoken.SaInterceptorImpl\" />\n        </mvc:interceptor>-->\n\n    </mvc:interceptors>\n\n    <!-- 配置视图解析器 -->\n    <!-- 对转向页面的路径解析。prefix：前缀， suffix：后缀 -->\n    <bean class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\" >\n        <property name=\"prefix\" value=\"/WEB-INF/jsp/\"/>\n        <property name=\"suffix\" value=\".jsp\"/>\n    </bean>\n\n</beans>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-redis.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<beans xmlns=\"http://www.springframework.org/schema/beans\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txmlns:context=\"http://www.springframework.org/schema/context\"\n\txmlns:tx=\"http://www.springframework.org/schema/tx\"\n\txmlns:aop=\"http://www.springframework.org/schema/aop\"\n\txmlns:p=\"http://www.springframework.org/schema/p\"\n\txsi:schemaLocation=\n\t\t\"http://www.springframework.org/schema/beans\n\t\thttp://www.springframework.org/schema/beans/spring-beans-3.2.xsd\n\t\thttp://www.springframework.org/schema/context \n\t\thttp://www.springframework.org/schema/context/spring-context-3.2.xsd\n\t\thttp://www.springframework.org/schema/tx\n\t\thttp://www.springframework.org/schema/tx/spring-tx-3.2.xsd\n\t\thttp://www.springframework.org/schema/aop\n\t\thttp://www.springframework.org/schema/aop/spring-aop-3.2.xsd\">\n\n\t<!-- Redis 连接信息 -->\n\t<bean id=\"redisConnectionFactory\"\n\t\t  class=\"org.springframework.data.redis.connection.jedis.JedisConnectionFactory\">\n\t\t<property name=\"hostName\" value=\"127.0.0.1\"/>\n\t\t<property name=\"port\" value=\"6379\"/>\n\t\t<property name=\"password\" value=\"\"/>\n\t\t<property name=\"database\" value=\"10\"/>\n\t\t<property name=\"poolConfig\" ref=\"poolConfig\"/>\n\t</bean>\n\t<!-- Redis 连接池配置 -->\n\t<bean id=\"poolConfig\" class=\"redis.clients.jedis.JedisPoolConfig\">\n\t\t<!--最大空闲数-->\n\t\t<property name=\"maxIdle\" value=\"300\"/>\n\t\t<!--连接池的最大数据库连接数  -->\n\t\t<property name=\"maxTotal\" value=\"1000\"/>\n\t\t<!--最大建立连接等待时间-->\n\t\t<property name=\"maxWaitMillis\" value=\"1000\"/>\n\t\t<!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟)-->\n\t\t<property name=\"minEvictableIdleTimeMillis\" value=\"300000\"/>\n\t\t<!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3-->\n\t\t<property name=\"numTestsPerEvictionRun\" value=\"1024\"/>\n\t\t<!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1-->\n\t\t<property name=\"timeBetweenEvictionRunsMillis\" value=\"30000\"/>\n\t\t<!--是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个-->\n\t\t<property name=\"testOnBorrow\" value=\"true\"/>\n\t\t<!--在空闲时检查有效性, 默认false  -->\n\t\t<property name=\"testWhileIdle\" value=\"true\"/>\n\t</bean>\n\n</beans>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/resources/spring-sa-token.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<beans xmlns=\"http://www.springframework.org/schema/beans\"\n\t   xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t   xsi:schemaLocation=\n\t\t\t   \"http://www.springframework.org/schema/beans\n\t\thttp://www.springframework.org/schema/beans/spring-beans-3.2.xsd\">\n\n\t<!-- Spring 集成 Sa-Token 时需要的所有 Bean -->\n\t<bean id=\"saTokenBeanInjection\" class=\"com.pj.satoken.SaTokenBeanInjection\">\n\t\t<constructor-arg ref=\"saLog\"/>\n\t\t<constructor-arg ref=\"saTokenConfig\"/>\n\t\t<constructor-arg ref=\"redisConnectionFactory\"/>\n\t\t<!-- 项目路由前缀，至关重要的一个属性，想要使用路由拦截鉴权必须把这个属性配置对 -->\n\t\t<constructor-arg value=\"/sa_token_demo_ssm_war\"/>\n\t</bean>\n\n\t<!--Sa-Token 日志输出对象 -->\n\t<bean id=\"saLog\" class=\"cn.dev33.satoken.log.SaLogForConsole\" />\n\n\t<!--Sa-Token 配置-->\n\t<bean id=\"saTokenConfig\" class=\"cn.dev33.satoken.config.SaTokenConfig\">\n\t\t<!-- token 名称（同时也是 cookie 名称） -->\n\t\t<property name=\"tokenName\" value=\"satoken\" />\n\t\t<!-- token 有效期（单位：秒） 默认30天，-1 代表永久有效 -->\n\t\t<property name=\"timeout\" value=\"2592000\" />\n\t\t<!-- token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结 -->\n\t\t<property name=\"activeTimeout\" value=\"-1\" />\n\t\t<!-- 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录） -->\n\t\t<property name=\"isConcurrent\" value=\"false\" />\n\t\t<!-- 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token） -->\n\t\t<property name=\"isShare\" value=\"false\" />\n\t\t<!-- token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）-->\n\t\t<property name=\"tokenStyle\" value=\"uuid\" />\n\t\t<!-- 是否输出操作日志 -->\n\t\t<property name=\"isLog\" value=\"true\"/>\n\t</bean>\n\n\t<!-- 导入了 spring-redis.xml 才能使用里面的配置对象 -->\n\t<import resource=\"spring-redis.xml\" />\n\n</beans>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/admin.jsp",
    "content": "<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %>\n<html>\n<head>\n    <title>Admin.jsp</title>\n</head>\n<body>\n    <h2> Admin.jsp </h2>\n    <p>具有 admin 角色才可以访问</p>\n</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/home.jsp",
    "content": "<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %>\n<html>\n<head>\n    <title>Home.jsp</title>\n</head>\n<body>\n    <h2> Home.jsp </h2>\n    <p>所有游客可访问</p>\n</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/jsp/user.jsp",
    "content": "<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %>\n<html>\n<head>\n    <title>User.jsp</title>\n</head>\n<body>\n    <h2> User.jsp </h2>\n    <p>登录后才可以访问</p>\n</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/webapp/WEB-INF/web.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<web-app xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xmlns=\"http://java.sun.com/xml/ns/javaee\"\n         xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd\"\n         id=\"WebApp_ID\" version=\"3.0\">\n    <display-name>yixiao2</display-name>\n\n    <!-- Spring -->\n    <context-param>\n        <param-name>contextConfigLocation</param-name>\n        <param-value>classpath:applicationContext.xml</param-value>\n    </context-param>\n    <listener>\n        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>\n    </listener>\n\n    <servlet>\n        <servlet-name>springmvc</servlet-name>\n        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>\n        <init-param>\n            <param-name>contextConfigLocation</param-name>\n            <param-value>classpath:spring-mvc.xml</param-value>\n        </init-param>\n        <load-on-startup>1</load-on-startup>\n    </servlet>\n\n    <servlet-mapping>\n        <servlet-name>springmvc</servlet-name>\n        <url-pattern>/</url-pattern>\n    </servlet-mapping>\n    <servlet-mapping>\n        <servlet-name>default</servlet-name>\n        <url-pattern>/static/*</url-pattern>\n    </servlet-mapping>\n\n    <!-- 全局错误页 -->\n    <error-page>\n        <error-code>404</error-code>\n        <location>/error</location>\n    </error-page>\n    <!--<error-page>\n        <error-code>500</error-code>\n        <location>/jsp/error/500.jsp</location>\n    </error-page>-->\n\n    <!-- 欢迎页 -->\n    <welcome-file-list>\n        <welcome-file>index.jsp</welcome-file>\n    </welcome-file-list>\n</web-app>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-ssm/src/main/webapp/index.jsp",
    "content": "<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %>\n<html>\n<head>\n    <title>欢迎页 index.jsp</title>\n</head>\n<body>\n    <h2> 欢迎页 index.jsp </h2>\n    <p>这是个外置位的 jsp 页面，可以不经过 Controller 直接访问到</p>\n</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-Token-SSO-Client端-测试页（前后端分离版-原生h5）</title>\n\t</head>\n\t<body>\n\t\t<h2>Sa-Token SSO-Client 应用端（前后端分离版-原生h5）</h2>\n\t\t<p>当前是否登录：<b class=\"is-login\"></b></p>\n\t\t<p>\n\t\t\t<a href=\"javascript: login();\">登录</a> - \n\t\t\t<a href=\"javascript: doLogoutByAlone();\">单应用注销</a> - \n\t\t\t<a href=\"javascript: doLogoutBySingleDeviceId();\">单浏览器注销</a> - \n\t\t\t<a href=\"javascript: doLogout();\">全端注销</a> - \n\t\t\t<a href=\"javascript: doMyInfo();\">账号资料</a>\n\t\t</p>\n\t\t<script src=\"sso-common.js\"></script>\n\t\t<script type=\"text/javascript\">\n\t\t\t\n\t\t\t// 登录 \n\t\t\tfunction login() {\n\t\t\t\tlocation.href = 'sso-login.html?back=' + encodeURIComponent(location.href);\n\t\t\t}\n\t\t\t\n\t\t\t// 单应用注销\n\t\t\tfunction doLogoutByAlone() {\n\t\t\t\tajax('/sso/logoutByAlone', {}, function(res){\n\t\t\t\t\tdoIsLogin();\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 单浏览器注销\n\t\t\tfunction doLogoutBySingleDeviceId() {\n\t\t\t\tajax('/sso/logout', { singleDeviceIdLogout: true }, function(res){\n\t\t\t\t\tdoIsLogin();\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 全端注销\n\t\t\tfunction doLogout() {\n\t\t\t\tajax('/sso/logout', {  }, function(res){\n\t\t\t\t\tdoIsLogin();\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 账号资料\n\t\t\tfunction doMyInfo() {\n\t\t\t\tajax('/sso/myInfo', {  }, function(res){\n\t\t\t\t\talert(JSON.stringify(res));\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 判断是否登录 \n\t\t\tfunction doIsLogin() {\n\t\t\t\tajax('/sso/isLogin', {}, function(res){\n\t\t\t\t\tif(res.data) {\n\t\t\t\t\t\tsetHtml('.is-login', res.data + ' (' + res.loginId + ')');\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsetHtml('.is-login', res.data);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\tdoIsLogin();\n\t\t\t\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-common.js",
    "content": "// 服务器接口主机地址\n// var baseUrl = \"http://sa-sso-client1.com:9002\";  // 模式二后端 \nvar baseUrl = \"http://sa-sso-client1.com:9003\";  // 模式三后端 \n\n// 封装一下Ajax\nfunction ajax(path, data, successFn, errorFn) {\n\tconsole.log('发起请求：', baseUrl + path, JSON.stringify(data));\n\tfetch(baseUrl + path, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t\t'satoken': localStorage.getItem('satoken')\n\t\t\t},\n\t\t\tbody: serializeToQueryString(data),\n\t\t})\n\t\t.then(response => response.json())\n\t\t.then(res => {\n\t\t\tconsole.log('返回数据：', res);\n\t\t\tif(res.code === 500) {\n\t\t\t\treturn alert(res.msg);\n\t\t\t}\n\t\t\tsuccessFn(res);\n\t\t})\n\t\t.catch(error => {\n\t\t\tconsole.error('请求失败:', error);\n\t\t\treturn alert(\"异常：\" + JSON.stringify(error));\n\t\t});\n}\n\n// ------------ 工具方法 ---------------\n\n// 从url中查询到指定名称的参数值\nfunction getParam(name, defaultValue) {\n\tvar query = window.location.search.substring(1);\n\tvar vars = query.split(\"&\");\n\tfor (var i = 0; i < vars.length; i++) {\n\t\tvar pair = vars[i].split(\"=\");\n\t\tif (pair[0] == name) {\n\t\t\treturn pair[1];\n\t\t}\n\t}\n\treturn (defaultValue == undefined ? null : defaultValue);\n}\n\n// 将 json 对象序列化为kv字符串，形如：name=Joh&age=30&active=true\nfunction serializeToQueryString(obj) {\n\treturn Object.entries(obj)\n\t\t.filter(([_, value]) => value != null) // 过滤 null 和 undefined\n\t\t.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n\t\t.join('&');\n}\n\n// 向指定标签里 set 内容 \nfunction setHtml(select, html) {\n\tconst dom = document.querySelector('.is-login');\n\tif(dom) {\n\t\tdom.innerHTML = html;\n\t}\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5/sso-login.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-Token-SSO-Client端-登录中转页页</title>\n\t\t<style type=\"text/css\">\n\t\t\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t加载中 ... \n\t\t</div>\n\t\t<script src=\"sso-common.js\"></script>\n\t\t<script type=\"text/javascript\">\n\t\t\n\t\t\tvar back = getParam('back', '/');\n\t\t\tvar ticket = getParam('ticket');\n\t\t\t\n\t\t\twindow.onload = function(){\n\t\t\t\tif(ticket) {\n\t\t\t\t\tdoLoginByTicket(ticket);\n\t\t\t\t} else {\n\t\t\t\t\tgoSsoAuthUrl();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// 重定向至认证中心 \n\t\t\tfunction goSsoAuthUrl() {\n\t\t\t\tajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {\n\t\t\t\t\tlocation.href = res.data;\n\t\t\t\t})\n\t\t\t}\n\t\t\n\t\t\t// 根据ticket值登录 \n\t\t\tfunction doLoginByTicket(ticket) {\n\t\t\t\tajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {\n\t\t\t\t\tlocalStorage.setItem('satoken', res.data);\n\t\t\t\t\tlocation.href = decodeURIComponent(back); \n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/README.md",
    "content": "# sa-token-demo-sso-client-vue2\nSa-Token SSO-Client 应用端（前后端分离版-Vue2）\n\n在线文档：[https://sa-token.cc/](https://sa-token.cc/)\n\n## 运行\n先安装依赖\n``` bat\nnpm install --registry=https://registry.npm.taobao.org\n```\n\n运行\n``` bat\nnpm run serve\n```\n\n打包\n``` bat\nnpm run build\n```\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"module\": \"esnext\",\n    \"baseUrl\": \"./\",\n    \"moduleResolution\": \"node\",\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ]\n    },\n    \"lib\": [\n      \"esnext\",\n      \"dom\",\n      \"dom.iterable\",\n      \"scripthost\"\n    ]\n  }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/package.json",
    "content": "{\n  \"name\": \"hello-world\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.1.3\",\n    \"core-js\": \"^3.6.5\",\n    \"vue\": \"^2.6.11\",\n    \"vue-router\": \"^3.6.5\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.12.16\",\n    \"@babel/eslint-parser\": \"^7.12.16\",\n    \"@vue/cli-plugin-babel\": \"~5.0.0\",\n    \"@vue/cli-plugin-eslint\": \"~5.0.0\",\n    \"@vue/cli-service\": \"~5.0.0\",\n    \"eslint\": \"^7.32.0\",\n    \"eslint-plugin-vue\": \"^8.0.3\"\n  },\n  \"eslintConfig\": {\n    \"root\": true,\n    \"env\": {\n      \"node\": true\n    },\n    \"extends\": [\n      \"plugin:vue/vue3-essential\",\n      \"eslint:recommended\"\n    ],\n    \"parserOptions\": {\n      \"parser\": \"@babel/eslint-parser\"\n    },\n    \"rules\": {}\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not dead\",\n    \"not ie 11\"\n  ]\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <title>Sa-Token SSO-Client 应用端（前后端分离版-Vue2）</title>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <router-view />\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'App'\n}\n</script>\n\n<style>\n\n</style>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/main.js",
    "content": "import Vue from 'vue'\nimport App from './App.vue'\n\nVue.config.productionTip = false\n\nimport router from './router'\n\nnew Vue({\n  router,\n  render: h => h(App)\n}).$mount('#app')\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/router/index.js",
    "content": "import Vue from 'vue'\nimport Router from 'vue-router'\n\nVue.use(Router)\n\n/**\n * 路由表\n */\nexport const routes = [\n    // 首页\n    {\n        name: 'index',\n        path: \"/index\",\n        component: () => import('../views/sso-index.vue')\n    },\n    // SSO-登录页\n    {\n        name: 'sso-login',\n        path: '/sso-login',\n        component: () => import('../views/sso-login.vue')\n    },\n\n    // 访问 / 时自动重定向到 /index\n    {\n        path: '/',\n        redirect: '/index'\n    }\n]\n\nconst router = new Router({\n    routes: routes\n})\n\nexport default router\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-common.js",
    "content": "import axios from 'axios'\n\n// sso-client 的后端服务地址\n// export const baseUrl = \"http://sa-sso-client1.com:9002\"; // 模式二后端\nexport const baseUrl = \"http://sa-sso-client1.com:9003\";  // 模式三后端\n\n// 封装一下 Ajax 方法\nexport const ajax = function(path, data, successFn) {\n    console.log('发起请求：', baseUrl + path, JSON.stringify(data));\n    axios({\n        url: baseUrl + path,\n        method: 'post',\n        data: data,\n        headers: {\n            \"Content-Type\": \"application/x-www-form-urlencoded\",\n            \"satoken\": localStorage.getItem(\"satoken\")\n        }\n    }).\n    then(function (response) { // 成功时执行\n        const res = response.data;\n        console.log('返回数据：', res);\n        if(res.code === 500) {\n            return alert(res.msg);\n        }\n        successFn(res);\n    }).\n    catch(function (error) {\n        console.error('请求失败:', error);\n        return alert(\"异常：\" + JSON.stringify(error));\n    })\n}\n\n// 从url中查询到指定名称的参数值\nexport const getParam = function(name, defaultValue){\n    var query = window.location.search.substring(1);\n    var vars = query.split(\"&\");\n    for (var i=0;i<vars.length;i++) {\n        var pair = vars[i].split(\"=\");\n        if(pair[0] == name){return pair[1];}\n    }\n    return(defaultValue == undefined ? null : defaultValue);\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-index.vue",
    "content": "<!-- 项目首页 -->\n<template>\n  <div>\n    <h2> Sa-Token SSO-Client 应用端（前后端分离版-Vue2） </h2>\n    <p>当前是否登录：<b>{{isLogin}} ({{ loginId }})</b></p>\n    <p>\n      <a href=\"javascript: null;\" @click=\"login()\" >登录</a> -\n      <a href=\"javascript: null;\" @click=\"doLogoutByAlone()\" >单应用注销</a> -\n      <a href=\"javascript: null;\" @click=\"doLogoutBySingleDeviceId();\">单浏览器注销</a> -\n      <a href=\"javascript: null;\" @click=\"doLogout();\">全端注销</a> -\n      <a href=\"javascript: null;\" @click=\"doMyInfo();\">账号资料</a>\n    </p>\n  </div>\n</template>\n\n<script>\nimport {ajax} from './sso-common.js'\nimport router from \"@/router\";\n\nexport default {\n  name: 'App',\n  data() {\n    return {\n      // 是否登录\n      isLogin: false,\n      // 登录账号\n      loginId: ''\n    }\n  },\n  methods: {\n\n    // 登录\n    login: function() {\n      router.push('/sso-login?back=' + encodeURIComponent(location.href));\n    },\n\n    // 单应用注销\n    doLogoutByAlone: function() {\n      ajax('/sso/logoutByAlone', {}, function(){\n        this.doIsLogin();\n      }.bind(this))\n    },\n\n    // 单浏览器注销\n    doLogoutBySingleDeviceId: function() {\n      ajax('/sso/logout', { singleDeviceIdLogout: true }, function(){\n        this.doIsLogin();\n      }.bind(this))\n    },\n\n    // 全端注销\n    doLogout: function () {\n      ajax('/sso/logout', {  }, function(){\n        this.doIsLogin();\n      }.bind(this))\n    },\n\n    // 账号资料\n    doMyInfo: function() {\n      ajax('/sso/myInfo', {  }, function(res){\n        alert(JSON.stringify(res));\n      })\n    },\n\n    // 判断是否登录\n    doIsLogin: function() {\n      ajax('/sso/isLogin', {}, function(res){\n        this.isLogin = res.data;\n        this.loginId = res.loginId;\n      }.bind(this))\n    }\n  },\n  created() {\n    this.doIsLogin();\n  }\n}\n</script>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/src/views/sso-login.vue",
    "content": "<!-- Sa-Token-SSO-Client端-登录中转页 -->\n<template>\n  <div>加载中...</div>\n</template>\n\n<script>\nimport {ajax, getParam} from './sso-common.js';\nimport router from '../router';\n\n\nexport default {\n  name: 'App',\n  data() {\n    return {\n      back: getParam('back') || router.currentRoute.query.back,\n      ticket: getParam('ticket') || router.currentRoute.query.ticket\n    }\n  },\n  // 页面加载后触发\n  created() {\n    console.log('获取 back 参数：', this.back)\n    console.log('获取 ticket 参数：', this.ticket)\n\n    if(this.ticket) {\n      this.doLoginByTicket(this.ticket);\n    } else {\n      this.goSsoAuthUrl();\n    }\n  },\n  methods: {\n    // 重定向至认证中心\n    goSsoAuthUrl: function() {\n      ajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {\n        location.href = res.data;\n      })\n    },\n    // 根据ticket值登录\n    doLoginByTicket: function(ticket) {\n      ajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {\n        if(res.code === 200) {\n          localStorage.setItem('satoken', res.data);\n          location.href = decodeURIComponent(this.back);\n        } else {\n          alert(res.msg);\n        }\n      }.bind(this))\n    }\n  }\n\n}\n\n</script>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/vue.config.js",
    "content": "const { defineConfig } = require('@vue/cli-service')\nmodule.exports = defineConfig({\n  transpileDependencies: true\n})\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/README.md",
    "content": "# sa-token-demo-sso-client-vue3\nSa-Token SSO-Client 应用端（前后端分离版-Vue3）\n\n在线文档：[https://sa-token.cc/](https://sa-token.cc/)\n\n\n## 运行\n先安装依赖\n``` bat\nnpm install --registry=https://registry.npm.taobao.org\n```\n\n运行\n``` bat\nnpm run dev\n```\n\n打包\n``` bat\nnpm run build\n```\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title> Sa-Token SSO-Client 应用端（前后端分离版-Vue3） </title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/package.json",
    "content": "{\n  \"name\": \"sa-token-demo-sso-client-vue3\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.1.3\",\n    \"vue\": \"^3.2.41\",\n    \"vue-router\": \"^4.1.6\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^3.2.0\",\n    \"vite\": \"^3.2.7\"\n  }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/App.vue",
    "content": "<template>\n  <router-view />\n</template>\n\n<script setup>\n\n</script>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/main.js",
    "content": "import { createApp } from 'vue'\nimport App from './App.vue'\n\n// createApp\nconst app = createApp(App);\n\n// 安装 vue-router\nimport router from './router';\napp.use(router);\n\n\n\n// 绑定dom\napp.mount('#app');\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/router/index.js",
    "content": "import { createRouter, createWebHistory } from 'vue-router';\n\n/**\n * 创建 vue-router 实例\n */\nconst router = createRouter({\n    history: createWebHistory(),\n    routes: [\n        // 首页\n        {\n            name: 'index',\n            path: \"/index\",\n            component: () => import('../views/sso-index.vue'),\n        },\n        // SSO-登录页\n        {\n            name: 'sso-login',\n            path: '/sso-login',\n            component: () => import('../views/sso-login.vue'),\n        },\n\n        // 访问 / 时自动重定向到 /index\n        {\n            path: \"/\",\n            redirect: '/index'\n        }\n    ],\n});\n\n// 导出\nexport default router;\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-common.js",
    "content": "import axios from 'axios'\n\n// sso-client 的后端服务地址\n// export const baseUrl = \"http://sa-sso-client1.com:9002\"; // 模式二后端\nexport const baseUrl = \"http://sa-sso-client1.com:9003\";  // 模式三后端\n\n// 封装一下 Ajax 方法\nexport const ajax = function(path, data, successFn) {\n    console.log('发起请求：', baseUrl + path, JSON.stringify(data));\n    axios({\n        url: baseUrl + path,\n        method: 'post',\n        data: data,\n        headers: {\n            \"Content-Type\": \"application/x-www-form-urlencoded\",\n            \"satoken\": localStorage.getItem(\"satoken\")\n        }\n    }).\n    then(function (response) { // 成功时执行\n        const res = response.data;\n        console.log('返回数据：', res);\n        if(res.code === 500) {\n            return alert(res.msg);\n        }\n        successFn(res);\n    }).\n    catch(function (error) {\n        console.error('请求失败:', error);\n        return alert(\"异常：\" + JSON.stringify(error));\n    })\n}\n\n// 从url中查询到指定名称的参数值\nexport const getParam = function(name, defaultValue){\n    var query = window.location.search.substring(1);\n    var vars = query.split(\"&\");\n    for (var i=0;i<vars.length;i++) {\n        var pair = vars[i].split(\"=\");\n        if(pair[0] == name){return pair[1];}\n    }\n    return(defaultValue == undefined ? null : defaultValue);\n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-index.vue",
    "content": "<!-- 项目首页 -->\n<template>\n  <div>\n    <h2> Sa-Token SSO-Client 应用端（前后端分离版-Vue3） </h2>\n    <p>当前是否登录：<b>{{ state.isLogin }} ({{ state.loginId }})</b></p>\n    <p>\n      <a href=\"javascript: null;\" @click=\"login()\" >登录</a> -\n      <a href=\"javascript: null;\" @click=\"doLogoutByAlone()\" >单应用注销</a> -\n      <a href=\"javascript: null;\" @click=\"doLogoutBySingleDeviceId();\">单浏览器注销</a> -\n      <a href=\"javascript: null;\" @click=\"doLogout();\">全端注销</a> -\n      <a href=\"javascript: null;\" @click=\"doMyInfo();\">账号资料</a>\n    </p>\n  </div>\n</template>\n\n<script setup>\nimport { reactive } from 'vue'\nimport { ajax } from './sso-common.js'\nimport router from \"../router/index.js\";\n\n// 数据\nconst state = reactive({\n  isLogin: false,\n  loginId: '',\n})\n\n// 登录\nfunction login() {\n  router.push('/sso-login?back=' + encodeURIComponent(location.href));\n}\n\n// 单应用注销\nfunction doLogoutByAlone() {\n  ajax('/sso/logoutByAlone', {}, function(res){\n    doIsLogin();\n  })\n}\n\n// 单浏览器注销\nfunction doLogoutBySingleDeviceId() {\n  ajax('/sso/logout', { singleDeviceIdLogout: true }, function(res){\n    doIsLogin();\n  })\n}\n\n// 全端注销\nfunction doLogout() {\n  ajax('/sso/logout', {  }, function(res){\n    doIsLogin();\n  })\n}\n\n// 账号资料\nfunction doMyInfo() {\n  ajax('/sso/myInfo', {  }, function(res){\n    alert(JSON.stringify(res));\n  })\n}\n\n// 判断是否登录\nfunction doIsLogin() {\n  ajax('/sso/isLogin', {}, function(res){\n    state.isLogin = res.data;\n    state.loginId = res.loginId;\n  })\n}\ndoIsLogin();\n\n\n</script>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/src/views/sso-login.vue",
    "content": "<!-- Sa-Token-SSO-Client端-登录页 -->\n<template>\n  <div>加载中...</div>\n</template>\n\n<script setup>\nimport {onMounted} from \"vue\";\nimport {ajax, getParam} from './sso-common.js';\nimport router from '../router';\n\n// 获取参数\nconst back = getParam('back') || router.currentRoute.value.query.back;\nconst ticket = getParam('ticket') || router.currentRoute.value.query.ticket;\n\nconsole.log('获取 back 参数：', back)\nconsole.log('获取 ticket 参数：', ticket)\n\n// 页面加载后触发\nonMounted(() => {\n  if(ticket) {\n    doLoginByTicket(ticket);\n  } else {\n    goSsoAuthUrl();\n  }\n})\n\n// 重定向至认证中心\nfunction goSsoAuthUrl() {\n  ajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {\n    location.href = res.data;\n  })\n}\n\n// 根据ticket值登录\nfunction doLoginByTicket(ticket) {\n  ajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {\n    if(res.code === 200) {\n      localStorage.setItem('satoken', res.data);\n      location.href = decodeURIComponent(back);\n    } else {\n      alert(res.msg);\n    }\n  })\n}\n\n</script>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/vite.config.js",
    "content": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [vue()]\n})\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso-server</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot Web依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合 RedisTemplate -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- 视图引擎（在前后端不分离模式下提供视图支持） -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-thymeleaf</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/SaSsoServerApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaSsoServerApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSsoServerApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getServerConfig());\n\t\tSystem.out.println(\"统一认证登录地址：http://sa-sso-server.com:9000/sso/auth\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/H5Controller.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.sso.template.SaSsoServerUtil;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 前后台分离架构下集成SSO所需的代码 （SSO-Server端）\n * <p>（注：如果不需要前后端分离架构下集成SSO，可删除此包下所有代码）</p>\n * @author click33\n *\n */\n@RestController\npublic class H5Controller {\n\n\t/**\n\t * 返回当前是否已经登录\n\t */\n\t@RequestMapping(\"/sso/isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.data(StpUtil.isLogin());\n\t}\n\n\t/**\n\t * 获取 redirectUrl \n\t */\n\t@RequestMapping(\"/sso/getRedirectUrl\")\n\tpublic SaResult getRedirectUrl(String client, String redirect, String mode) {\n\t\t// 未登录情况下，返回 code=401 \n\t\tif(StpUtil.isLogin() == false) {\n\t\t\treturn SaResult.code(401);\n\t\t}\n\t\t// 已登录情况下，构建 redirectUrl\n\t\tredirect = SaFoxUtil.decoderUrl(redirect);\n\t\tif(SaSsoConsts.MODE_SIMPLE.equals(mode)) {\n\t\t\t// 模式一 \n\t\t\tSaSsoServerUtil.checkRedirectUrl(client, redirect);\n\t\t\treturn SaResult.data(redirect);\n\t\t} else {\n\t\t\t// 模式二或模式三\n\t\t\tString redirectUrl = SaSsoServerUtil.buildRedirectUrl(client, redirect, StpUtil.getLoginId(), StpUtil.getTokenValue());\n\t\t\treturn SaResult.data(redirectUrl);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/h5/SaTokenConfigure.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 （解决跨域问题）\n *\n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\n    /**\n     * CORS 跨域处理策略\n     */\n    @Bean\n    public SaCorsHandleFunction corsHandle() {\n        return (req, res, sto) -> {\n            res.\n                    // 允许指定域访问跨域资源\n                            setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                    // 允许所有请求方式\n                    .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                    // 有效时间\n                    .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                    // 允许的header参数\n                    .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n            // 如果是预检请求，则立即返回到前端\n            SaRouter.match(SaHttpMethod.OPTIONS)\n                    .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                    .back();\n        };\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/GlobalExceptionHandler.java",
    "content": "package com.pj.sso;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/HomeController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * SSO 平台中心模式示例，跳连接进入子系统\n */\n@RestController\npublic class HomeController {\n\n    // 平台化首页\n    @RequestMapping({\"/\", \"/home\"})\n    public Object index() {\n        // 如果未登录，则先去登录\n        if(!StpUtil.isLogin()) {\n            return SaHolder.getResponse().redirect(\"/sso/auth\");\n        }\n\n        // 拼接各个子系统的地址，格式形如：/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页}\n        String link1 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/\";\n        String link2 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/\";\n        String link3 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/\";\n\n        // 组织网页结构返回到前端\n        String title = \"<h2>SSO 平台首页 (平台中心模式)</h2>\";\n        String client1 = \"<p><a href='\" + link1 + \"' target='_blank'> 进入Client1系统 </a></p>\";\n        String client2 = \"<p><a href='\" + link2 + \"' target='_blank'> 进入Client2系统 </a></p>\";\n        String client3 = \"<p><a href='\" + link3 + \"' target='_blank'> 进入Client3系统 </a></p>\";\n\n        return title + client1 + client2 + client3;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/java/com/pj/sso/SsoServerController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.ModelAndView;\n\n/**\n * Sa-Token-SSO Server端 Controller \n * @author click33\n *\n */\n@RestController\npublic class SsoServerController {\n\n\t/**\n\t * SSO-Server端：处理所有SSO相关请求 \n\t * \t\thttp://{host}:{port}/sso/auth\t\t\t-- 单点登录授权地址\n\t * \t\thttp://{host}:{port}/sso/doLogin\t\t-- 账号密码登录接口，接受参数：name、pwd\n\t * \t\thttp://{host}:{port}/sso/signout\t\t-- 单点注销地址（isSlo=true时打开）\n\t */\n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoServerProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数 \n\t@Autowired\n\tprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\n\t\t// 配置：未登录时返回的View \n\t\tssoServerTemplate.strategy.notLoginView = () -> {\n\t\t\treturn new ModelAndView(\"sa-login.html\");\n\t\t};\n\t\t\n\t\t// 配置：登录处理函数 \n\t\tssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {\n\t\t\t// 此处仅做模拟登录，真实环境应该查询数据库进行登录\n\t\t\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\tString deviceId = SaHolder.getRequest().getParam(\"deviceId\", SaFoxUtil.getRandomString(32));\n\t\t\t\tStpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId));\n\t\t\t\treturn SaResult.ok(\"登录成功！\").setData(StpUtil.getTokenValue());\n\t\t\t}\n\t\t\treturn SaResult.error(\"登录失败！\");\n\t\t};\n\n\t\t// 添加消息处理器：userinfo (获取用户资料) （用于为 client 端开放拉取数据的接口）\n\t\tssoServerTemplate.messageHolder.addHandle(\"userinfo\", (ssoTemplate, message) -> {\n\t\t\tSystem.out.println(\"收到消息：\" + message);\n\n\t\t\t// 自定义返回结果（模拟）\n\t\t\treturn SaResult.ok()\n\t\t\t\t\t.set(\"id\", message.get(\"loginId\"))\n\t\t\t\t\t.set(\"name\", \"LinXiaoYu\")\n\t\t\t\t\t.set(\"sex\", \"女\")\n\t\t\t\t\t.set(\"age\", 18);\n\t\t});\n\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 9000\n\n# Sa-Token 配置\nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # SSO 模式一配置  (非模式一不需要配置)\n#    cookie:\n#         # 配置 Cookie 作用域\n#          domain: stp.com\n        \n    # SSO-Server 配置\n    sso-server:\n        # Ticket有效期 (单位: 秒)，默认五分钟 \n        ticket-timeout: 300\n        # 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\n        home-route: /home\n        # 是否启用匿名 client (开启匿名 client 后，允许客户端接入时不提交 client 参数)\n        allow-anon-client: true\n        # 所有允许的授权回调地址 (匿名 client 使用)\n        allow-url: \"*\"\n        # API 接口调用秘钥 (全局默认 + 匿名 client 使用)\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n        # 应用列表：配置接入的应用信息\n        clients:\n            # 应用 sso-client1：采用模式一对接 (同域、同Redis)\n            sso-client1:\n                client: sso-client1\n                allow-url: \"*\"\n            # 应用 sso-client2：采用模式二对接 (跨域、同Redis)\n            sso-client2:\n                client: sso-client2\n                allow-url: \"*\"\n                secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n            # 应用 sso-client3：采用模式三对接 (跨域、跨Redis)\n            sso-client3:\n                # 应用名称\n                client: sso-client3\n                # 允许授权地址\n                allow-url: \"*\"\n                # 是否接收消息推送\n                is-push: true\n                # 消息推送地址\n                push-url: http://sa-sso-client1.com:9003/sso/pushC\n                # 接口调用秘钥 (如果不配置则使用全局默认秘钥)\n                secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n            # 应用 sso-client3-resdk：采用 ReSdk 模式对接\n            sso-client3-resdk:\n                # 应用名称\n                client: sso-client3-resdk\n                # 允许授权地址\n                allow-url: \"*\"\n                # 是否接收消息推送\n                is-push: true\n                # 消息推送地址\n                push-url: http://sa-sso-client1.com:9005/sso/pushC\n                # 接口调用秘钥 (如果不配置则使用全局默认秘钥)\n                secret-key: SSO-C3-ReSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n        \nspring: \n    # Redis配置 （SSO模式一和模式二使用 Redis 来同步会话）\n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \nforest: \n    # 关闭 forest 请求日志打印\n    log-enabled: false\n    \n    \n    \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/layer.js",
    "content": "/*! layer-v3.1.1 Web弹层组件 MIT License  http://layer.layui.com/  By 贤心 */\n ;!function(e,t){\"use strict\";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;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:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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:\"&#x4FE1;&#x606F;\",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?'<div class=\"layui-layer-title\" style=\"'+(f?r.title[1]:\"\")+'\">'+(f?r.title[0]:r.title)+\"</div>\":\"\";return r.zIndex=s,t([r.shade?'<div class=\"layui-layer-shade\" id=\"layui-layer-shade'+a+'\" times=\"'+a+'\" style=\"'+(\"z-index:\"+(s-1)+\"; \")+'\"></div>':\"\",'<div class=\"'+l[0]+(\" layui-layer-\"+o.type[r.type])+(0!=r.type&&2!=r.type||r.shade?\"\":\" layui-layer-border\")+\" \"+(r.skin||\"\")+'\" id=\"'+l[0]+a+'\" type=\"'+o.type[r.type]+'\" times=\"'+a+'\" showtime=\"'+r.time+'\" conType=\"'+(e?\"object\":\"string\")+'\" style=\"z-index: '+s+\"; width:\"+r.area[0]+\";height:\"+r.area[1]+(r.fixed?\"\":\";position:absolute;\")+'\">'+(e&&2!=r.type?\"\":u)+'<div id=\"'+(r.id||\"\")+'\" class=\"layui-layer-content'+(0==r.type&&r.icon!==-1?\" layui-layer-padding\":\"\")+(3==r.type?\" layui-layer-loading\"+r.icon:\"\")+'\">'+(0==r.type&&r.icon!==-1?'<i class=\"layui-layer-ico layui-layer-ico'+r.icon+'\"></i>':\"\")+(1==r.type&&e?\"\":r.content||\"\")+'</div><span class=\"layui-layer-setwin\">'+function(){var e=c?'<a class=\"layui-layer-min\" href=\"javascript:;\"><cite></cite></a><a class=\"layui-layer-ico layui-layer-max\" href=\"javascript:;\"></a>':\"\";return r.closeBtn&&(e+='<a class=\"layui-layer-ico '+l[7]+\" \"+l[7]+(r.title?r.closeBtn:4==r.type?\"1\":\"2\")+'\" href=\"javascript:;\"></a>'),e}()+\"</span>\"+(r.btn?function(){var e=\"\";\"string\"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t<i;t++)e+='<a class=\"'+l[6]+t+'\">'+r.btn[t]+\"</a>\";return'<div class=\"'+l[6]+\" layui-layer-btn-\"+(r.btnAlign||\"\")+'\">'+e+\"</div>\"}():\"\")+(r.resize?'<span class=\"layui-layer-resize\"></span>':\"\")+\"</div>\"],u,i('<div class=\"layui-layer-move\"></div>')),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='<iframe scrolling=\"'+(t.content[1]||\"auto\")+'\" allowtransparency=\"true\" id=\"'+l[4]+a+'\" name=\"'+l[4]+a+'\" onload=\"this.className=\\'\\';\" class=\"layui-layer-load\" frameborder=\"0\" src=\"'+t.content[0]+'\"></iframe>';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]+'<i class=\"layui-layer-TipsG\"></i>',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;a<c.stX&&(a=c.stX),a>f&&(a=f),o<c.stY&&(o=c.stY),o>u&&(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?'<textarea class=\"layui-layer-input\"'+a+\">\"+(e.value||\"\")+\"</textarea>\":function(){return'<input type=\"'+(1==e.formType?\"password\":\"text\")+'\" class=\"layui-layer-input\" value=\"'+(e.value||\"\")+'\">'}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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(\"&#x6700;&#x591A;&#x8F93;&#x5165;\"+(e.maxlength||500)+\"&#x4E2A;&#x5B57;&#x6570;\",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='<span class=\"'+n+'\">'+t[0].title+\"</span>\";i<e;i++)a+=\"<span>\"+t[i].title+\"</span>\";return a}(),content:'<ul class=\"layui-layer-tabmain\">'+function(){var e=t.length,i=1,a=\"\";if(e>0)for(a='<li class=\"layui-layer-tabli '+n+'\">'+(t[0].content||\"no content\")+\"</li>\";i<e;i++)a+='<li class=\"layui-layer-tabli\">'+(t[i].content||\"no  content\")+\"</li>\";return a}()+\"</ul>\",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(\"&#x6CA1;&#x6709;&#x56FE;&#x7247;\")}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]<r[1]&&(a[0]=a[0]/r[1],a[1]=a[1]/r[1])}return[a[0]+\"px\",a[1]+\"px\"]}(),title:!1,shade:.9,shadeClose:!0,closeBtn:!1,move:\".layui-layer-phimg img\",moveType:1,scrollbar:!1,moveOut:!0,isOutAnim:!1,skin:\"layui-layer-photos\"+c(\"photos\"),content:'<div class=\"layui-layer-phimg\"><img src=\"'+u[d].src+'\" alt=\"'+(u[d].alt||\"\")+'\" layer-pid=\"'+u[d].pid+'\"><div class=\"layui-layer-imgsee\">'+(u.length>1?'<span class=\"layui-layer-imguide\"><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgprev\"></a><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgnext\"></a></span>':\"\")+'<div class=\"layui-layer-imgbar\" style=\"display:'+(a?\"block\":\"\")+'\"><span class=\"layui-layer-imgtit\"><a href=\"javascript:;\">'+(u[d].alt||\"\")+\"</a><em>\"+s.imgIndex+\"/\"+u.length+\"</em></span></div></div></div>\",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(\"&#x5F53;&#x524D;&#x56FE;&#x7247;&#x5730;&#x5740;&#x5F02;&#x5E38;<br>&#x662F;&#x5426;&#x7EE7;&#x7EED;&#x67E5;&#x770B;&#x4E0B;&#x4E00;&#x5F20;&#xFF1F;\",{time:3e4,btn:[\"&#x4E0B;&#x4E00;&#x5F20;\",\"&#x4E0D;&#x770B;&#x4E86;\"],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);"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/layer.js",
    "content": "/*! layer mobile-v2.0.0 Web弹层组件 MIT License  http://layer.layui.com/mobile  By 贤心 */\n ;!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?'<h3 style=\"'+(e?n.title[1]:\"\")+'\">'+(e?n.title[0]:n.title)+\"</h3>\":\"\"}(),c=function(){\"string\"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e='<span yes type=\"1\">'+n.btn[0]+\"</span>\",2===t&&(e='<span no type=\"0\">'+n.btn[1]+\"</span>\"+e),'<div class=\"layui-m-layerbtn\">'+e+\"</div>\"):\"\"}();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='<i></i><i class=\"layui-m-layerload\"></i><i></i><p>'+(n.content||\"\")+\"</p>\"),n.skin&&(n.anim=\"up\"),\"msg\"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?\"<div \"+(\"string\"==typeof n.shade?'style=\"'+n.shade+'\"':\"\")+' class=\"layui-m-layershade\"></div>':\"\")+'<div class=\"layui-m-layermain\" '+(n.fixed?\"\":'style=\"position:static;\"')+'><div class=\"layui-m-layersection\"><div class=\"layui-m-layerchild '+(n.skin?\"layui-m-layer-\"+n.skin+\" \":\"\")+(n.className?n.className:\"\")+\" \"+(n.anim?\"layui-m-anim-\"+n.anim:\"\")+'\" '+(n.style?'style=\"'+n.style+'\"':\"\")+\">\"+l+'<div class=\"layui-m-layercont\">'+n.content+\"</div>\"+c+\"</div></div></div>\",!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;o<r;o++)l.touch(s[o],a);if(e.shade&&e.shadeClose){var c=t[i](\"layui-m-layershade\")[0];l.touch(c,function(){layer.close(n.index,e.end)})}e.end&&(l.end[n.index]=e.end)},e.layer={v:\"2.0\",index:r,open:function(e){var t=new c(e||{});return t.index},close:function(e){var n=a(\"#\"+o[0]+e)[0];n&&(n.innerHTML=\"\",t.body.removeChild(n),clearTimeout(l.timer[e]),delete l.timer[e],\"function\"==typeof l.end[e]&&l.end[e](),delete l.end[e])},closeAll:function(){for(var e=t[i](o[0]),n=0,a=e.length;n<a;n++)layer.close(0|e[0].getAttribute(\"index\"))}},\"function\"==typeof define?define(function(){return layer}):function(){var e=document.scripts,n=e[e.length-1],i=n.src,a=i.substring(0,i.lastIndexOf(\"/\")+1);n.getAttribute(\"merge\")||document.head.appendChild(function(){var e=t.createElement(\"link\");return e.href=a+\"need/layer.css?2.0\",e.type=\"text/css\",e.rel=\"styleSheet\",e.id=\"layermcss\",e}())}()}(window);"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/mobile/need/layer.css",
    "content": ".layui-m-layer{position:relative;z-index:19891014}.layui-m-layer *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.layui-m-layermain,.layui-m-layershade{position:fixed;left:0;top:0;width:100%;height:100%}.layui-m-layershade{background-color:rgba(0,0,0,.7);pointer-events:auto}.layui-m-layermain{display:table;font-family:Helvetica,arial,sans-serif;pointer-events:none}.layui-m-layermain .layui-m-layersection{display:table-cell;vertical-align:middle;text-align:center}.layui-m-layerchild{position:relative;display:inline-block;text-align:left;background-color:#fff;font-size:14px;border-radius:5px;box-shadow:0 0 8px rgba(0,0,0,.1);pointer-events:auto;-webkit-overflow-scrolling:touch;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@-webkit-keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.layui-m-anim-scale{animation-name:layui-m-anim-scale;-webkit-animation-name:layui-m-anim-scale}@-webkit-keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}.layui-m-anim-up{-webkit-animation-name:layui-m-anim-up;animation-name:layui-m-anim-up}.layui-m-layer0 .layui-m-layerchild{width:90%;max-width:640px}.layui-m-layer1 .layui-m-layerchild{border:none;border-radius:0}.layui-m-layer2 .layui-m-layerchild{width:auto;max-width:260px;min-width:40px;border:none;background:0 0;box-shadow:none;color:#fff}.layui-m-layerchild h3{padding:0 10px;height:60px;line-height:60px;font-size:16px;font-weight:400;border-radius:5px 5px 0 0;text-align:center}.layui-m-layerbtn span,.layui-m-layerchild h3{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-m-layercont{padding:50px 30px;line-height:22px;text-align:center}.layui-m-layer1 .layui-m-layercont{padding:0;text-align:left}.layui-m-layer2 .layui-m-layercont{text-align:center;padding:0;line-height:0}.layui-m-layer2 .layui-m-layercont i{width:25px;height:25px;margin-left:8px;display:inline-block;background-color:#fff;border-radius:100%;-webkit-animation:layui-m-anim-loading 1.4s infinite ease-in-out;animation:layui-m-anim-loading 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.layui-m-layerbtn,.layui-m-layerbtn span{position:relative;text-align:center;border-radius:0 0 5px 5px}.layui-m-layer2 .layui-m-layercont p{margin-top:20px}@-webkit-keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}@keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.layui-m-layer2 .layui-m-layercont i:first-child{margin-left:0;-webkit-animation-delay:-.32s;animation-delay:-.32s}.layui-m-layer2 .layui-m-layercont i.layui-m-layerload{-webkit-animation-delay:-.16s;animation-delay:-.16s}.layui-m-layer2 .layui-m-layercont>div{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}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/layer/theme/default/layer.css",
    "content": ".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}}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.css",
    "content": "*{margin: 0; padding: 0;}\nbody{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}\n::-webkit-input-placeholder{color: #ccc;}\n\n/* 视图盒子 */\n.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}\n/* 背景 EAEFF3 */\n.bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);}\n.bg-2{height: 50%; background-color: #EAEFF3;}\n\n/* 渐变背景 */\n/*.bg-1{\n    background-size: 500%;\n\tbackground-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5);\n\tanimation: bganimation 30s infinite;\n}\n@keyframes bganimation{\n    0%{background-position: 0% 50%;}\n    50%{background-position: 100% 50%;}\n    100%{background-position: 0% 50%;}\n}  */\n/* 背景 */\n.bg-1{background: #101C34;}\n.bg-2{background: #101C34;}\n/* .bg-1{height: 100%; background-image: url(./login-bg.png); background-size: 100% 100%;} */\n\n\n/* 内容盒子 */\n.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}\n\n/* 登录盒子 */\n/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */\n.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}\n.login-box{display: flex; align-items: center; text-align: center;}\n\n/* 表单 */\n.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}\n.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}\n.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}\n\n/* 输入框 */\n.from-item{border: 0px #000 solid; margin-bottom: 15px;}\n.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;}\n.s-input{font-size: 12px;}\n.s-input:focus{border-color: #409eff}\n\n/* 登录按钮 */\n.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}\n.s-btn:hover{background-color: #50aEFF;}\n\n/* 重置按钮 */\n.reset-box{text-align: left; font-size: 12px;}\n.reset-box a{text-decoration: none;}\n.reset-box a:hover{text-decoration: underline;}\n\n/* loading框样式 */\n.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/static/sa-res/login.js",
    "content": "// sa \nvar sa = {};\n\n// 打开loading\nsa.loading = function(msg) {\n\tlayer.closeAll();\t// 开始前先把所有弹窗关了\n\treturn layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });\n};\n\n// 隐藏loading\nsa.hideLoading = function() {\n\tlayer.closeAll();\n};\n\n\n// ----------------------------------- 登录事件 -----------------------------------\n\n$('.login-btn').click(function(){\n\tsa.loading(\"正在登录...\");\n\t// 开始登录\n\tsetTimeout(function() {\n\t\t$.ajax({\n\t\t\turl: \"sso/doLogin\",\n\t\t\ttype: \"post\", \n\t\t\tdata: {\n\t\t\t\tname: $('[name=name]').val(),\n\t\t\t\tpwd: $('[name=pwd]').val()\n\t\t\t},\n\t\t\tdataType: 'json',\n\t\t\tsuccess: function(res){\n\t\t\t\tconsole.log('返回数据：', res);\n\t\t\t\tsa.hideLoading();\n\t\t\t\tif(res.code == 200) {\n\t\t\t\t\tlayer.msg('登录成功', {anim: 0, icon: 6 }); \n\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\tlocation.reload();\n\t\t\t\t\t}, 800)\n\t\t\t\t} else {\n\t\t\t\t\tlayer.msg(res.msg, {anim: 6, icon: 2 }); \n\t\t\t\t}\n\t\t\t},\n\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\tsa.hideLoading();\n\t\t\t\tif(xhr.status == 0){\n\t\t\t\t\treturn layer.alert('无法连接到服务器，请检查网络');\n\t\t\t\t}\n\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t}\n\t\t});\n\t}, 400);\n});\n\n// 绑定回车事件\n$('[name=name],[name=pwd]').bind('keypress', function(event){\n\tif(event.keyCode == \"13\") {\n\t\t$('.login-btn').click();\n\t}\n});\n\n// 输入框获取焦点\n$(\"[name=name]\").focus();\n\n// 打印信息 \nvar str = \"This page is provided by Sa-Token, Please refer to: \" + \"https://sa-token.cc/\";\nconsole.log(str);\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/templates/sa-login.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<title>Sa-SSO-Server 认证中心-登录</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<base th:href=\"@{/}\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t\t<link rel=\"stylesheet\" href=\"./sa-res/login.css\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\">\n\t\t\t<div class=\"bg-1\"></div>\n\t\t\t<div class=\"bg-2\"></div>\n\t\t\t<div class=\"content-box\">\n\t\t\t\t<div class=\"login-box\">\n\t\t\t\t\t<div class=\"from-box\">\n\t\t\t\t\t\t<h2 class=\"from-title\">Sa-SSO-Server 认证中心</h2>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"name\" placeholder=\"请输入账号\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"pwd\" type=\"password\" placeholder=\"请输入密码\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<button class=\"s-input s-btn login-btn\">登录</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item reset-box\">\n\t\t\t\t\t\t\t<a href=\"javascript: location.reload();\" >刷新</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- 底部 版权 -->\n\t\t\t<div style=\"position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;\">\n\t\t\t\tThis page is provided by Sa-Token-SSO \n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- scripts -->\n\t\t<script src=\"./sa-res/jquery.min.js\"></script>\n\t\t<script src=\"./sa-res/layer/layer.js\"></script>\n\t\t<script src=\"./sa-res/login.js\"></script>\n\t\t\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/common.js",
    "content": "// 服务端地址 \nvar baseUrl = \"http://sa-sso-server.com:9000\";\n\n// sa \nvar sa = {};\n\n// 打开loading\nsa.loading = function(msg) {\n\tlayer.closeAll();\t// 开始前先把所有弹窗关了\n\treturn layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});\n};\n\n// 隐藏loading\nsa.hideLoading = function() {\n\tlayer.closeAll();\n};\n\n// 封装一下Ajax\nsa.ajax = function(url, data, successFn) {\n\t$.ajax({\n\t\turl: baseUrl + url,\n\t\ttype: \"post\", \n\t\tdata: data,\n\t\tdataType: 'json',\n\t\theaders: {\n\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t'satoken': localStorage.getItem('satoken')\n\t\t},\n\t\tsuccess: function(res){\n\t\t\tconsole.log('返回数据：', res);\n\t\t\tsuccessFn(res);\n\t\t},\n\t\terror: function(xhr, type, errorThrown){\n\t\t\tif(xhr.status == 0){\n\t\t\t\treturn alert('无法连接到服务器，请检查网络');\n\t\t\t}\n\t\t\treturn alert(\"异常：\" + JSON.stringify(xhr));\n\t\t}\n\t});\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/home.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>SSO-Server 平台首页</title>\n\t</head>\n\t<body>\n\t\t<h2>SSO-Server 平台首页 (前后端分离模式) (平台中心模式)</h2>\n\t\t<p>\n\t\t\t<a href='sso-auth.html?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/'\n\t\t\t\ttarget='_blank'> 进入Client1系统 </a>\n\t\t</p>\n\t\t<p>\n\t\t\t<a href='sso-auth.html?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/'\n\t\t\t\ttarget='_blank'> 进入Client2系统 </a>\n\t\t</p>\n\t\t<p>\n\t\t\t<a href='sso-auth.html?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/'\n\t\t\t\ttarget='_blank'> 进入Client3系统 </a>\n\t\t</p>\n\t\t\n\t\t<!-- scripts -->\n\t\t<script src=\"https://unpkg.com/jquery@3.4.1/dist/jquery.min.js\"></script>\n\t\t<script src=\"https://www.layuicdn.com/layer-v3.1.1/layer.js\"></script>\n\t\t<script src=\"./common.js\"></script>\n\t\t<script>\n\t\t\tsa.ajax(\"/sso/isLogin\", {}, function(res) {\n\t\t\t\tif(res.data) {\n\t\t\t\t\t// 已登录... \n\t\t\t\t\tconsole.log('已登录，开始操作...');\n\t\t\t\t} else {\n\t\t\t\t\tlayer.msg('未登录，请先登录...')\n\t\t\t\t\tsetTimeout(function(){\n\t\t\t\t\t\tlocation.href = './sso-auth.html';\n\t\t\t\t\t}, 1000)\n\t\t\t\t}\n\t\t\t})\n\t\t</script>\n\t</body>\n</html>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.css",
    "content": "*{margin: 0; padding: 0;}\nbody{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}\n::-webkit-input-placeholder{color: #ccc;}\n\n/* 视图盒子 */\n.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}\n/* 背景 EAEFF3 */\n.bg-1{height: 100%; background: #c0cCf4;}\n\n/* 内容盒子 */\n.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}\n\n/* 登录盒子 */\n/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */\n.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}\n.login-box{display: flex; align-items: center; text-align: center;}\n\n/* 表单 */\n.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}\n.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}\n.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}\n\n/* 输入框 */\n.from-item{border: 0px #000 solid; margin-bottom: 15px;}\n.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;}\n.s-input{font-size: 12px;}\n.s-input:focus{border-color: #409eff}\n\n/* 登录按钮 */\n.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}\n.s-btn:hover{background-color: #50aEFF;}\n\n/* 重置按钮 */\n.reset-box{text-align: left; font-size: 12px;}\n.reset-box a{text-decoration: none;}\n.reset-box a:hover{text-decoration: underline;}\n\n/* loading框样式 */\n.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<title>Sa-SSO-Server 认证中心-登录</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t\t<link rel=\"stylesheet\" href=\"./sso-auth.css\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\">\n\t\t\t<div class=\"bg-1\"></div>\n\t\t\t<div class=\"content-box\">\n\t\t\t\t<div class=\"login-box\">\n\t\t\t\t\t<div class=\"from-box\">\n\t\t\t\t\t\t<h2 class=\"from-title\">Sa-SSO-Server 认证中心（前后端分离版）</h2>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"name\" placeholder=\"请输入账号\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"pwd\" type=\"password\" placeholder=\"请输入密码\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<button class=\"s-input s-btn login-btn\">登录</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item reset-box\">\n\t\t\t\t\t\t\t<a href=\"javascript: location.reload();\" >刷新</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- 底部 版权 -->\n\t\t\t<div style=\"position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;\">\n\t\t\t\tThis page is provided by Sa-Token-SSO \n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- scripts -->\n\t\t<script src=\"https://unpkg.com/jquery@3.4.1/dist/jquery.min.js\"></script>\n\t\t<script src=\"https://www.layuicdn.com/layer-v3.1.1/layer.js\"></script>\n\t\t<script src=\"./common.js\"></script>\n\t\t<script src=\"./sso-auth.js\"></script>\n\t\t\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5/sso-auth.js",
    "content": "\n// ----------------------------------- 相关事件 -----------------------------------\n\n// 检查当前是否已经登录，如果已登录则直接开始跳转，如果未登录则等待用户输入账号密码 \nvar pData = {\n\tclient: getParam('client', ''), \n\tredirect: getParam('redirect', ''), \n\tmode: getParam('mode', '')\n};\n// 提供 redirect 参数时，登录后往 redirect 跳转\nif(pData.redirect) {\n\tsa.ajax(\"/sso/getRedirectUrl\", pData, function(res) {\n\t\tif(res.code == 200) {\n\t\t\t// 已登录，并且redirect地址有效，开始跳转  \n\t\t\tlocation.href = res.data;\n\t\t} else if(res.code == 401) {\n\t\t\tconsole.log('未登录');\n\t\t} else {\n\t\t\tlayer.alert(res.msg); \n\t\t}\n\t})\n} else {\n\t// 未提供 redirect 参数时，登录后往 home 跳转\n\tsa.ajax(\"/sso/isLogin\", {}, function(res) {\n\t\tif(res.data) {\n\t\t\tlocation.href = './home.html';\n\t\t} else {\n\t\t\tconsole.log('未登录，请先登录...');\n\t\t}\n\t})\n}\n\n\n// 登录\n$('.login-btn').click(function(){\n\tsa.loading(\"正在登录...\");\n\t// 开始登录\n\tvar data = {\n\t\tname: $('[name=name]').val(),\n\t\tpwd: $('[name=pwd]').val()\n\t};\n\tsa.ajax(\"/sso/doLogin\", data, function(res) {\n\t\tsa.hideLoading();\n\t\tif(res.code == 200) {\n\t\t\tlocalStorage.setItem('satoken', res.data);\n\t\t\tlayer.msg('登录成功', {anim: 0, icon: 6 }); \n\t\t\tsetTimeout(function() {\n\t\t\t\tlocation.reload();\n\t\t\t}, 800);\n\t\t} else {\n\t\t\tlayer.msg(res.msg, {anim: 6, icon: 2 }); \n\t\t}\n\t})\n});\n\n\n// 绑定回车事件\n$('[name=name],[name=pwd]').bind('keypress', function(event){\n\tif(event.keyCode == \"13\") {\n\t\t$('.login-btn').click();\n\t}\n});\n\n// 输入框获取焦点\n$(\"[name=name]\").focus();\n\n// 从url中查询到指定名称的参数值 \nfunction getParam(name, defaultValue){\n\tvar query = window.location.search.substring(1);\n\tvar vars = query.split(\"&\");\n\tfor (var i=0;i<vars.length;i++) {\n\t\tvar pair = vars[i].split(\"=\");\n\t\tif(pair[0] == name){return pair[1] + (pair[2] ? '=' + pair[2] : '');}\n\t}\n\treturn(defaultValue == undefined ? null : defaultValue);\n}\n\n// 打印信息 \nvar str = \"This page is provided by Sa-Token, Please refer to: \" + \"https://sa-token.cc/\";\nconsole.log(str);\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso1-client</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合 SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合 RedisTemplate  -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n\t\t<dependency>\n\t\t    <groupId>cn.dev33</groupId>\n\t\t    <artifactId>sa-token-alone-redis</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/SaSso1ClientApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * SSO模式一，Client端 Demo \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaSso1ClientApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSso1ClientApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://s1.stp.com:9001\");\n\t\tSystem.out.println(\"测试访问应用端二: http://s2.stp.com:9001\");\n\t\tSystem.out.println(\"测试访问应用端三: http://s3.stp.com:9001\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@RestController\npublic class SsoClientController {\n\n\t// SSO-Client端：首页 \n\t@RequestMapping(\"/\")\n\tpublic String index(HttpServletRequest request) {\n\t\tString url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), request.getQueryString()) );\n\t\tSaSsoClientConfig cfg = SaSsoManager.getClientConfig();\n\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式一)</h2>\" +\n\t\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\t\"<p>\" +\n\t\t\t\t\t\t\"<a href='\" + cfg.splicingAuthUrl() + \"?mode=simple&client=\" + cfg.getClient() + \"&redirect=\" + url + \"'>登录</a> - \" +\n\t\t\t\t\t\t\"<a href='\" + cfg.splicingSignoutUrl() + \"?singleDeviceIdLogout=true&back=\" + url + \"'>单浏览器注销</a> - \" +\n\t\t\t\t\t\t\"<a href='\" + cfg.splicingSignoutUrl() + \"?back=\" + url + \"'>全端注销</a> \" +\n\t\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\t\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 9001\n\n# Sa-Token 配置 \nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # SSO-相关配置\n    sso-client:\n        # client 标识\n        client: sso-client1\n        # SSO-Server端主机地址\n        server-url: http://sso.stp.com:9000\n    \n    # 配置 Sa-Token 单独使用的Redis连接（此处需要和 SSO-Server 端连接同一个 Redis）\n    # 注：使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖\n    alone-redis: \n        # Redis数据库索引\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso2-client</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n        \n\t\t<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n\t\t<dependency>\n\t\t    <groupId>cn.dev33</groupId>\n\t\t    <artifactId>sa-token-alone-redis</artifactId>\n            <version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/SaSso2ClientApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaSso2ClientApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSso2ClientApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式二 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9002\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9002\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9002\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/H5Controller.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.sso.model.SaCheckTicketResult;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 前后台分离架构下集成SSO所需的代码 （SSO-Client端）\n * <p>（注：如果不需要前后端分离架构下集成SSO，可删除此包下所有代码）</p>\n * @author click33\n *\n */\n@RestController\npublic class H5Controller {\n\n\t// 判断当前是否登录\n\t@RequestMapping(\"/sso/isLogin\")\n\tpublic Object isLogin() {\n\t\treturn SaResult.data(StpUtil.isLogin()).set(\"loginId\", StpUtil.getLoginIdDefaultNull());\n\t}\n\t\n\t// 返回SSO认证中心登录地址 \n\t@RequestMapping(\"/sso/getSsoAuthUrl\")\n\tpublic SaResult getSsoAuthUrl(String clientLoginUrl) {\n\t\tString serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, \"\");\n\t\treturn SaResult.data(serverAuthUrl);\n\t}\n\t\n\t// 根据 ticket 进行登录\n\t@RequestMapping(\"/sso/doLoginByTicket\")\n\tpublic SaResult doLoginByTicket(String ticket) {\n\t\tSaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);\n\t\tStpUtil.login(ctr.loginId, new SaLoginParameter()\n\t\t\t\t.setTimeout(ctr.remainTokenTimeout)\n\t\t\t\t.setDeviceId(ctr.deviceId)\n\t\t);\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/SaTokenConfigure.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类\n *\n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\n    /**\n     * CORS 跨域处理策略\n     */\n    @Bean\n    public SaCorsHandleFunction corsHandle() {\n        return (req, res, sto) -> {\n            res.\n                    // 允许指定域访问跨域资源\n                    setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                    // 允许所有请求方式\n                    .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                    // 有效时间\n                    .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                    // 允许的header参数\n                    .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n            // 如果是预检请求，则立即返回到前端\n            SaRouter.match(SaHttpMethod.OPTIONS)\n                    .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                    .back();\n        };\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@RestController\npublic class SsoClientController {\n\n\t// 首页 \n\t@RequestMapping(\"/\")\n\tpublic String index() {\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式二)</h2>\" +\n\t\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\t\"<p> \" +\n\t\t\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/myInfo' target='_blank'>账号资料</a>\" +\n\t\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\n\t/*\n\t * SSO-Client端：处理所有SSO相关请求 \n\t * \t\thttp://{host}:{port}/sso/login\t\t\t-- Client 端登录地址\n\t * \t\thttp://{host}:{port}/sso/logout\t\t\t-- Client 端注销地址（isSlo=true时打开）\n\t * \t\thttp://{host}:{port}/sso/pushC\t\t\t-- Client 端接收消息推送地址\n\t */\n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数\n\t@Autowired\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t}\n\n\t// 当前应用独自注销 (不退出其它应用)\n\t@RequestMapping(\"/sso/logoutByAlone\")\n\tpublic Object logoutByAlone() {\n\t\tStpUtil.logout();\n\t\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n\t}\n\n\t// 查询我的账号信息：sso-client 前端 -> sso-center 后端 -> sso-server 后端\n\t@RequestMapping(\"/sso/myInfo\")\n\tpublic Object myInfo() {\n\t\t// 如果尚未登录\n\t\tif( ! StpUtil.isLogin()) {\n\t\t\treturn \"尚未登录，无法获取\";\n\t\t}\n\n\t\t// 获取本地 loginId\n\t\tObject loginId = StpUtil.getLoginId();\n\n\t\t// 推送消息\n\t\tSaSsoMessage message = new SaSsoMessage();\n\t\tmessage.setType(\"userinfo\");\n\t\tmessage.set(\"loginId\", loginId);\n\t\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n\t\t// 返回给前端\n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 9002\n\n# sa-token配置 \nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # SSO-相关配置\n    sso-client:\n        # 应用标识\n        client: sso-client2\n        # SSO-Server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）\n        # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html\n        # API 接口调用秘钥 (单点注销时会用到)\n        secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n    \n    # 配置 Sa-Token 单独使用的Redis连接（此处需要和 SSO-Server 端连接同一个 Redis）\n    # 注：使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖\n    alone-redis:\n        # Redis数据库索引\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n\nforest:\n    # 关闭 forest 请求日志打印\n    log-enabled: false\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso3-client</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/SaSso3ClientApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaSso3ClientApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSso3ClientApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式三 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9003\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9003\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9003\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/H5Controller.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.sso.model.SaCheckTicketResult;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 前后台分离架构下集成SSO所需的代码 （SSO-Client端）\n * <p>（注：如果不需要前后端分离架构下集成SSO，可删除此包下所有代码）</p>\n *\n * @author click33\n */\n@RestController\npublic class H5Controller {\n\n\t// 当前是否登录 \n\t@RequestMapping(\"/sso/isLogin\")\n\tpublic Object isLogin() {\n\t\treturn SaResult.data(StpUtil.isLogin()).set(\"loginId\", StpUtil.getLoginIdDefaultNull());\n\t}\n\t\n\t// 返回SSO认证中心登录地址 \n\t@RequestMapping(\"/sso/getSsoAuthUrl\")\n\tpublic SaResult getSsoAuthUrl(String clientLoginUrl) {\n\t\tString serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, \"\");\n\t\treturn SaResult.data(serverAuthUrl);\n\t}\n\t\n\t// 根据ticket进行登录\n\t@RequestMapping(\"/sso/doLoginByTicket\")\n\tpublic SaResult doLoginByTicket(String ticket) {\n\t\tSaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket, \"/sso/doLoginByTicket\");\n\t\tStpUtil.login(ctr.loginId, new SaLoginParameter()\n\t\t\t\t.setTimeout(ctr.remainTokenTimeout)\n\t\t\t\t.setDeviceId(ctr.deviceId)\n\t\t);\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/h5/SaTokenConfigure.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类\n *\n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\n    /**\n     * CORS 跨域处理策略\n     */\n    @Bean\n    public SaCorsHandleFunction corsHandle() {\n        return (req, res, sto) -> {\n            res.\n                    // 允许指定域访问跨域资源\n                    setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                    // 允许所有请求方式\n                    .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                    // 有效时间\n                    .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                    // 允许的header参数\n                    .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n            // 如果是预检请求，则立即返回到前端\n            SaRouter.match(SaHttpMethod.OPTIONS)\n                    .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                    .back();\n        };\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/GlobalExceptionHandler.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@RestController\npublic class SsoClientController {\n\n\t// SSO-Client端：首页\n\t@RequestMapping(\"/\")\n\tpublic String index() {\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>\" +\n\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\"<p> \" +\n\t\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\t\"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - \" +\n\t\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> - \" +\n\t\t\t\t\t\"<a href='/sso/myInfo' target='_blank'>账号资料</a>\" +\n\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\t\n\t/*\n\t * SSO-Client端：处理所有SSO相关请求\n\t * \t\thttp://{host}:{port}/sso/login\t\t\t-- Client 端登录地址\n\t * \t\thttp://{host}:{port}/sso/logout\t\t\t-- Client 端注销地址（isSlo=true时打开）\n\t * \t\thttp://{host}:{port}/sso/pushC\t\t\t-- Client 端接收消息推送地址\n\t */\n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数 \n\t@Autowired\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t}\n\n\t// 当前应用独自注销 (不退出其它应用)\n\t@RequestMapping(\"/sso/logoutByAlone\")\n\tpublic Object logoutByAlone() {\n\t\tStpUtil.logout();\n\t\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n\t}\n\n\t// 查询我的账号信息：sso-client 前端 -> sso-center 后端 -> sso-server 后端\n\t@RequestMapping(\"/sso/myInfo\")\n\tpublic Object myInfo() {\n\t\t// 如果尚未登录\n\t\tif( ! StpUtil.isLogin()) {\n\t\t\treturn \"尚未登录，无法获取\";\n\t\t}\n\n\t\t// 获取本地 loginId\n\t\tObject loginId = StpUtil.getLoginId();\n\n\t\t// 推送消息\n\t\tSaSsoMessage message = new SaSsoMessage();\n\t\tmessage.setType(\"userinfo\");\n\t\tmessage.set(\"loginId\", loginId);\n\t\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n\t\t// 返回给前端\n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 9003\n\n# sa-token配置 \nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # sso-client 相关配置\n    sso-client:\n        # 应用标识\n        client: sso-client3\n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）\n        # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html\n        # 使用 Http 请求校验 ticket (模式三)\n        is-http: true\n        # API 接口调用秘钥\n        secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\nspring: \n    # 配置 Redis 连接 （此处与 SSO-Server 端连接不同的 Redis）\n    redis: \n        # Redis数据库索引\n        database: 3\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \nforest: \n    # 关闭 forest 请求日志打印\n    log-enabled: false\n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso3-client-anon</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n        \n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/SaSso3ClientAnonApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaSso3ClientAnonApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSso3ClientAnonApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式三 (匿名应用) Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9006\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9006\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9006\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/GlobalExceptionHandler.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@RestController\npublic class SsoClientController {\n\n\t// 首页 \n\t@RequestMapping(\"/\")\n\tpublic String index() {\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式三-匿名应用)</h2>\" +\n\t\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\t\"<p> \" +\n\t\t\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/myInfo' target='_blank'>账号资料</a>\" +\n\t\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\n\t/*\n\t * SSO-Client端：处理所有SSO相关请求 \n\t * \t\thttp://{host}:{port}/sso/login\t\t\t-- Client 端登录地址\n\t * \t\thttp://{host}:{port}/sso/logout\t\t\t-- Client 端注销地址（isSlo=true时打开）\n\t * \t\thttp://{host}:{port}/sso/pushC\t\t\t-- Client 端接收消息推送地址\n\t */\n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数\n\t@Autowired\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\t\t// 重写 loginId 与 centerId 转换策略函数，做到本地应用 userId 与认证中心 userId 的互相映射\n\n//\t\t// 将 centerId 转换为 loginId 的函数\n//\t\tssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> {\n//\t\t\treturn \"Stu\" + centerId;\n//\t\t};\n//\t\t// 将 loginId 转换为 centerId 的函数\n//\t\tssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> {\n//\t\t\treturn loginId.toString().substring(3);\n//\t\t};\n\t}\n\n\t// 当前应用独自注销 (不退出其它应用)\n\t@RequestMapping(\"/sso/logoutByAlone\")\n\tpublic Object logoutByAlone() {\n\t\tStpUtil.logout();\n\t\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n\t}\n\n\t// 查询我的账号信息：sso-client 前端 -> sso-center 后端 -> sso-server 后端\n\t@RequestMapping(\"/sso/myInfo\")\n\tpublic Object myInfo() {\n\t\t// 如果尚未登录\n\t\tif( ! StpUtil.isLogin()) {\n\t\t\treturn \"尚未登录，无法获取\";\n\t\t}\n\n\t\t// 原写法：直接调用 StpUtil.getLoginId() 当做 centerId 来提交\n\t\t// Object centerId = StpUtil.getLoginId();\n\n\t\t// 新写法：获取本地 loginId 对应的认证中心 centerId\n\t\tObject centerId = SaSsoClientUtil.getSsoTemplate().strategy.convertLoginIdToCenterId.run(StpUtil.getLoginId());\n\n\t\t// 推送消息\n\t\tSaSsoMessage message = new SaSsoMessage();\n\t\tmessage.setType(\"userinfo\");\n\t\tmessage.set(\"loginId\", centerId);\n\t\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n\t\t// 返回给前端\n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 9006\n\n# sa-token配置 \nsa-token:\n    # 配置一个不同的 token-name，以避免在和模式三 demo 一起测试时发生数据覆盖\n    token-name: satoken-client-anon\n    # sso-client 相关配置\n    sso-client:\n        # client 标识 匿名应用就是指不配置 client 标识的应用\n        # client: sso-client3\n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 使用 Http 请求校验ticket (模式三)\n        is-http: true\n        # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个)\n        reg-logout-call: true\n        # API 接口调用秘钥\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\nspring: \n    # 配置 Redis 连接 （此处与SSO-Server端连接不同的Redis）\n    redis: \n        # Redis数据库索引\n        database: 6\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \nforest: \n    # 关闭 forest 请求日志打印\n    log-enabled: false\n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso3-client-nosdk</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Http 请求工具 -->\n        <dependency>\n\t\t    <groupId>com.dtflys.forest</groupId>\n\t\t    <artifactId>forest-spring-boot-starter</artifactId>\n\t\t    <version>1.5.26</version>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaSsoClientNoSdkApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSsoClientNoSdkApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式三 (NoSdk版) demo 启动成功 ----------------------\");\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9004\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9004\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9004\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改hosts文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\n\t\tSystem.err.println(\"自 v1.43.0 版本起，Sa-Token SSO 不再维护 NoSdk 示例，此项目仅做留档\");\n\t\tSystem.err.println(\"如您需要非 Sa-Token 技术栈项目接入 SSO-Server 认证中心，请参考 ReSdk 版本示例\");\n\t}\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport java.io.IOException;\nimport java.util.Objects;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.servlet.http.HttpSession;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.sso.util.AjaxJson;\nimport com.pj.sso.util.MyHttpSessionHolder;\n\n/**\n * SSO Client端 Controller \n * @author click33\n */\n@RestController\npublic class SsoClientController {\n\n\t// SSO-Client端：首页\n\t@RequestMapping(\"/\")\n\tpublic String index(HttpSession session) {\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端</h2>\" + \n\t\t\t\t\t\"<p>当前会话登录账号：\" + session.getAttribute(\"userId\") + \"</p>\" + \n\t\t\t\t\t\"<p><a href=\\\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\\\">登录</a>\" + \n\t\t\t\t\t\" <a href='/sso/logout?back=' +  + encodeURIComponent(location.href);>注销</a>\" + \n\t\t\t\t\t\" <a href='/sso/myInfo' target=\\\"_blank\\\">获取资料</a></p>\";\n\t\treturn str;\n\t}\n\n\t// SSO-Client端：单点登录地址 \n\t@RequestMapping(\"/sso/login\")\n\tpublic Object ssoLogin(String ticket, @RequestParam(defaultValue = \"/\") String back, \n\t\t\tHttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {\n\t\t\n\t\t// 如果已经登录，则直接返回 \n\t\tif(session.getAttribute(\"userId\") != null) {\n\t\t\tresponse.sendRedirect(back);\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\t/*\n\t\t * 此时有两种情况: \n\t\t * 情况1：ticket无值，说明此请求是Client端访问，需要重定向至SSO认证中心 \n\t\t * 情况2：ticket有值，说明此请求从SSO认证中心重定向而来，需要根据ticket进行登录 \n\t\t */\n\t\tif(ticket == null) {\n\t\t\tString currUrl = request.getRequestURL().toString();\n\t\t\tString clientLoginUrl = currUrl + \"?back=\" + SsoRequestUtil.encodeUrl(back);\n\t\t\tString serverAuthUrl = SsoRequestUtil.authUrl + \"?redirect=\" + clientLoginUrl;\n\t\t\tresponse.sendRedirect(serverAuthUrl);\n\t\t\treturn null;\n\t\t} else {\n\t\t\t// 获取当前 client 端的单点注销回调地址 \n\t\t\tString ssoLogoutCall = \"\";\n\t\t\tif(SsoRequestUtil.isSlo) {\n\t\t\t\tssoLogoutCall = request.getRequestURL().toString().replace(\"/sso/login\", \"/sso/logoutCall\"); \n\t\t\t}\n\t\t\t\n\t\t\t// 校验 ticket\n\t\t\tString timestamp = String.valueOf(System.currentTimeMillis());\t// 时间戳\n\t\t\tString nonce = SsoRequestUtil.getRandomString(20);\t\t// 随机字符串\n\t\t\tString sign = SsoRequestUtil.getSignByTicket(ticket, ssoLogoutCall, timestamp, nonce);\t// 参数签名\n\t\t\tString checkUrl = SsoRequestUtil.checkTicketUrl +\n\t\t\t\t\t\"?timestamp=\" + timestamp +\n\t\t\t\t\t\"&nonce=\" + nonce +\n\t\t\t\t\t\"&sign=\" + sign +\n\t\t\t\t\t\"&ticket=\" + ticket +\n\t\t\t\t\t\"&ssoLogoutCall=\" + ssoLogoutCall;\n\t\t\tAjaxJson result = SsoRequestUtil.request(checkUrl);\n\t\t\t\n\t\t\t// 200 代表校验成功 \n\t\t\tif(result.getCode() == 200 && SsoRequestUtil.isEmpty(result.getData()) == false) {\n\t\t\t\t// 登录上 \n\t\t\t\tObject loginId = result.getData();\n\t\t\t\tsession.setAttribute(\"userId\", loginId);\n\t\t\t\t// 返回 back 地址\n\t\t\t\tresponse.sendRedirect(back);\n\t\t\t\treturn null;\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\t// 将 sso-server 回应的消息作为异常抛出 \n\t\t\t\tthrow new RuntimeException(result.getMsg());\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// SSO-Client端：单点注销地址\n\t@RequestMapping(\"/sso/logout\")\n\tpublic Object ssoLogout(@RequestParam(defaultValue = \"/\") String back, \n\t\t\tHttpServletResponse response, HttpSession session) throws IOException {\n\t\t\n\t\t// 如果未登录，则无需注销 \n        if(session.getAttribute(\"userId\") == null) {\n\t\t\tresponse.sendRedirect(back);\n\t\t\treturn null;\n        }\n        \n        // 调用 sso-server 认证中心单点注销API \n        Object loginId = session.getAttribute(\"userId\");  // 账号id \n\t\tString timestamp = String.valueOf(System.currentTimeMillis());\t// 时间戳 \n\t\tString nonce = SsoRequestUtil.getRandomString(20);\t\t// 随机字符串\n\t\tString sign = SsoRequestUtil.getSign(loginId, timestamp, nonce);\t// 参数签名\n\t\t\n        String url = SsoRequestUtil.sloUrl + \n        \t\t\"?loginId=\" + loginId +\n        \t\t\"&timestamp=\" + timestamp +\n        \t\t\"&nonce=\" + nonce +\n        \t\t\"&sign=\" + sign;\n        AjaxJson result = SsoRequestUtil.request(url);\n        \n\t\t// 校验响应状态码，200 代表成功 \n\t\tif(result.getCode() == 200) {\n\t\t\t\n\t        // 极端场景下，sso-server 中心的单点注销可能并不会通知到此 client 端，所以这里需要再补一刀\n\t\t\tsession.removeAttribute(\"userId\");\n\t\t\t// 返回 back 地址\n\t\t\tresponse.sendRedirect(back);\n\t\t\treturn null;\n\t\t\t\n\t\t} else {\n\t\t\t// 将 sso-server 回应的消息作为异常抛出 \n\t\t\tthrow new RuntimeException(result.getMsg());\n\t\t}\n\t}\n\t\n\t// SSO-Client端：单点注销回调地址\n\t@RequestMapping(\"/sso/logoutCall\")\n\tpublic Object ssoLogoutCall(String loginId, String autoLogout, String timestamp, String nonce, String sign) {\n\t\t\n\t\t// 校验签名 \n\t\tString calcSign = SsoRequestUtil.getSignByLogoutCall(loginId, autoLogout, timestamp, nonce);\n\t\tif(calcSign.equals(sign) == false) {\n\t\t\tSystem.out.println(\"无效签名，拒绝应答：\" + sign);\n\t\t\treturn AjaxJson.getError(\"无效签名，拒绝应答\" + sign);\n\t\t}\n\t\t\n\t\t// 注销这个账号id \n\t\tfor (HttpSession session: MyHttpSessionHolder.sessionList) {\n\t\t\tObject userId = session.getAttribute(\"userId\");\n\t\t\tif(Objects.equals(String.valueOf(userId), loginId)) {\n\t\t\t\tsession.removeAttribute(\"userId\");\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn AjaxJson.getSuccess(\"账号id=\" + loginId + \" 注销成功\");\n\t}\n\n\t// 查询我的账号信息 (调用此接口的前提是 sso-server 端开放了 /sso/userinfo 路由)\n\t@RequestMapping(\"/sso/myInfo\")\n\tpublic Object myInfo(HttpSession session) {\n\t\t// 如果尚未登录 \n\t\tif(session.getAttribute(\"userId\") == null) {\n\t\t\treturn \"尚未登录，无法获取\";\n\t\t}\n\n        // 组织 url 参数 \n        Object loginId = session.getAttribute(\"userId\");  // 账号id \n\t\tString timestamp = String.valueOf(System.currentTimeMillis());\t// 时间戳 \n\t\tString nonce = SsoRequestUtil.getRandomString(20);\t\t// 随机字符串\n\t\tString sign = SsoRequestUtil.getSign(loginId, timestamp, nonce);\t// 参数签名\n\t\t\n        String url = SsoRequestUtil.getDataUrl +\n        \t\t\"?loginId=\" + loginId +\n        \t\t\"&timestamp=\" + timestamp +\n        \t\t\"&nonce=\" + nonce +\n        \t\t\"&sign=\" + sign;\n        AjaxJson result = SsoRequestUtil.request(url);\n        \n        // 返回给前端 \n\t\treturn result;\n\t}\n\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn AjaxJson.getError(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java",
    "content": "package com.pj.sso;\n\nimport com.dtflys.forest.Forest;\nimport com.pj.sso.util.AjaxJson;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.security.MessageDigest;\nimport java.util.Map;\nimport java.util.Random;\n\n/**\n * 封装一些 sso 共用方法 \n * \n * @author click33\n * @since 2022-4-30\n */\npublic class SsoRequestUtil {\n\n\t/**\n\t * SSO-Server端主机地址\n\t */\n\tpublic static String serverUrl = \"http://sa-sso-server.com:9000\";\n\n\t/**\n\t * SSO-Server端 统一认证地址 \n\t */\n\tpublic static String authUrl = serverUrl + \"/sso/auth\";\n\n\t/**\n\t * SSO-Server端 ticket校验地址 \n\t */\n\tpublic static String checkTicketUrl = serverUrl + \"/sso/checkTicket\";\n\n\t/**\n\t * 单点注销地址 \n\t */\n\tpublic static String sloUrl = serverUrl + \"/sso/signout\";\n\n\t/**\n\t * SSO-Server端 查询userinfo地址\n\t */\n\tpublic static String getDataUrl = serverUrl + \"/sso/getData\";\n\n\t/**\n\t * 打开单点注销功能\n\t */\n\tpublic static boolean isSlo = true;\n\n\t/**\n\t * 接口调用秘钥 \n\t */\n\tpublic static String secretKey = \"kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\";\n\n\t\n\t\n\t// -------------------------- 工具方法 \n\t\n\t/**\n\t * 发出请求，并返回 SaResult 结果 \n\t * @param url 请求地址 \n\t * @return 返回的结果 \n\t */\n\tpublic static AjaxJson request(String url) {\n\t\tMap<String, Object> map = Forest.post(url).executeAsMap();\n\t\treturn new AjaxJson(map);\n\t}\n\n\t/**\n\t * 根据参数计算签名 \n\t * @param loginId 账号id\n\t * @param timestamp 当前时间戳，13位\n\t * @param nonce 随机字符串\n\t * @return 签名 \n\t */\n\tpublic static String getSign(Object loginId, String timestamp, String nonce) {\n\t\treturn md5(\"loginId=\" + loginId + \"&nonce=\" + nonce + \"&timestamp=\" + timestamp + \"&key=\" + secretKey);\n\t}\n\t// 单点注销回调时构建签名\n\tpublic static String getSignByLogoutCall(Object loginId, String autoLogout, String timestamp, String nonce) {\n\t\treturn md5(\"autoLogout=\" + autoLogout + \"&loginId=\" + loginId + \"&nonce=\" + nonce + \"&timestamp=\" + timestamp + \"&key=\" + secretKey);\n\t}\n\t// 校验ticket 时构建签名\n\tpublic static String getSignByTicket(String ticket, String ssoLogoutCall, String timestamp, String nonce) {\n\t\treturn md5(\"nonce=\" + nonce + \"&ssoLogoutCall=\" + ssoLogoutCall + \"&ticket=\" + ticket + \"&timestamp=\" + timestamp + \"&key=\" + secretKey);\n\t}\n\n\t/**\n\t * 指定元素是否为null或者空字符串\n\t * @param str 指定元素 \n\t * @return 是否为null或者空字符串\n\t */\n\tpublic static boolean isEmpty(Object str) {\n\t\treturn str == null || \"\".equals(str);\n\t}\n\t\n\t/**\n\t * md5加密 \n\t * @param str 指定字符串\n\t * @return 加密后的字符串\n\t */\n\tpublic static String md5(String str) {\n\t\tstr = (str == null ? \"\" : str);\n\t\tchar[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };\n\t\ttry {\n\t\t\tbyte[] btInput = str.getBytes();\n\t\t\tMessageDigest mdInst = MessageDigest.getInstance(\"MD5\");\n\t\t\tmdInst.update(btInput);\n\t\t\tbyte[] md = mdInst.digest();\n\t\t\tint j = md.length;\n\t\t\tchar[] strA = new char[j * 2];\n\t\t\tint k = 0;\n\t\t\tfor (byte byte0 : md) {\n\t\t\t\tstrA[k++] = hexDigits[byte0 >>> 4 & 0xf];\n\t\t\t\tstrA[k++] = hexDigits[byte0 & 0xf];\n\t\t\t}\n\t\t\treturn new String(strA);\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 生成指定长度的随机字符串\n\t * \n\t * @param length 字符串的长度\n\t * @return 一个随机字符串\n\t */\n\tpublic static String getRandomString(int length) {\n\t\tString str = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n\t\tRandom random = new Random();\n\t\tStringBuffer sb = new StringBuffer();\n\t\tfor (int i = 0; i < length; i++) {\n\t\t\tint number = random.nextInt(62);\n\t\t\tsb.append(str.charAt(number));\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * URL编码 \n\t * @param url see note \n\t * @return see note \n\t */\n\tpublic static String encodeUrl(String url) {\n\t\ttry {\n\t\t\treturn URLEncoder.encode(url, \"UTF-8\");\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/AjaxJson.java",
    "content": "package com.pj.sso.util;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n\n/**\n * ajax请求返回Json格式数据的封装 <br>\n * 所有预留字段：<br>\n * code=状态码 <br>\n * msg=描述信息 <br>\n * data=携带对象 <br>\n * pageNo=当前页 <br>\n * pageSize=页大小 <br>\n * startIndex=起始索引 <br>\n * dataCount=数据总数 <br>\n * pageCount=分页总数 <br>\n * <p> 返回范例：</p>\n *  <pre>\n\t{\n\t\t\"code\": 200,    // 成功时=200, 失败时=500  msg=失败原因\n\t\t\"msg\": \"ok\",\n\t\t\"data\": {}\n\t} \n\t</pre>\n */\npublic class AjaxJson extends LinkedHashMap<String, Object> implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\t\n\t\n\t// ============================  写值取值  ================================== \n\t\n\t/** 给code赋值，连缀风格 */\n\tpublic AjaxJson setCode(int code) {\n\t\tthis.put(\"code\", code);\n\t\treturn this;\n\t}\n\t/** 返回code */\n\tpublic Integer getCode() {\n\t\treturn (Integer)this.get(\"code\");\n\t}\n\n\t/** 给msg赋值，连缀风格 */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.put(\"msg\", msg);\n\t\treturn this;\n\t}\n\t/** 获取msg */\n\tpublic String getMsg() {\n\t\treturn (String)this.get(\"msg\");\n\t}\n\n\t/** 给data赋值，连缀风格 */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.put(\"data\", data);\n\t\treturn this;\n\t}\n\t/** 获取data */\n\tpublic Object getData() {\n\t\treturn this.get(\"data\");\n\t}\n\t/** 将data还原为指定类型并返回 */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) this.getData();\n\t}\n\n\t/** 给dataCount(数据总数)赋值，连缀风格 */\n\tpublic AjaxJson setDataCount(Long dataCount) {\n\t\tthis.put(\"dataCount\", dataCount);\n\t\t// 如果提供了数据总数，则尝试计算page信息\n\t\tif(dataCount != null && dataCount >= 0) {\t\t\n\t\t\t// 如果：已有page信息\n\t\t\tif(get(\"pageNo\") != null) {\n\t\t\t\tthis.initPageInfo();\n\t\t\t} \n//\t\t\t// 或者：是JavaWeb环境\n//\t\t\telse if(SoMap.isJavaWeb()) {\n//\t\t\t\tSoMap so = SoMap.getRequestSoMap();\n//\t\t\t\tthis.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());\n//\t\t\t\tthis.initPageInfo();\n//\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\t/** 获取dataCount(数据总数) */\n\tpublic Long getDataCount() {\n\t\treturn (Long)this.get(\"dataCount\");\n\t}\n\t\n\t/** 设置pageNo 和 pageSize，并计算出startIndex于pageCount */\n\tpublic AjaxJson setPageNoAndSize(long pageNo, long pageSize) {\n\t\tthis.put(\"pageNo\", pageNo);\n\t\tthis.put(\"pageSize\", pageSize);\n\t\treturn this;\n\t}\n\n\t/** 根据 pageSize dataCount，计算startIndex 与 pageCount */\n\tpublic AjaxJson initPageInfo() {\n\t\tlong pageNo = (long)this.get(\"pageNo\");\n\t\tlong pageSize = (long)this.get(\"pageSize\");\n\t\tlong dataCount = (long)this.get(\"dataCount\");\n\t\tthis.set(\"startIndex\", (pageNo - 1) * pageSize);\n\t\tlong pc = dataCount / pageSize;\n\t\tthis.set(\"pageCount\", (dataCount % pageSize == 0 ?  pc : pc + 1));\n\t\treturn this;\n\t}\n\t\n\t\n\t/** 写入一个值 自定义key, 连缀风格 */\n\tpublic AjaxJson set(String key, Object data) {\n\t\tthis.put(key, data);\n\t\treturn this;\n\t}\n\n\t/** 写入一个Map, 连缀风格 */\n\tpublic AjaxJson setMap(Map<String, ?> map) {\n\t\tfor (String key : map.keySet()) {\n\t\t\tthis.put(key, map.get(key));\n\t\t}\n\t\treturn this;\n\t}\n\t\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.setCode(code);\n\t\tthis.setMsg(msg);\n\t\tthis.setData(data);\n\t\tif(dataCount != null) {\n\t\t\tthis.setDataCount(dataCount);\n\t\t}\n\t}\n\n\tpublic AjaxJson(Map<String, Object> map) {\n\t\tfor (String key: map.keySet()) {\n\t\t\tthis.set(key, map.get(key));\n\t\t}\n\t}\n\t\n\t/** 返回成功 */\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t\n\t/** 返回失败 */\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t/** 返回警告  */\n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\n\t/** 返回未登录  */\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\n\t/** 返回没有权限的  */\n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t/** 返回一个自定义状态码的  */\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t/** 返回分页和数据的  */\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t/** 返回, 根据受影响行数的(大于0=ok，小于0=error)  */\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t/** 返回，根据布尔值来确定最终结果的  (true=ok，false=error)  */\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n//  // 历史版本遗留代码 \n//\tpublic int code; \t// 状态码\n//\tpublic String msg; \t// 描述信息 \n//\tpublic Object data; // 携带对象\n//\tpublic Long dataCount;\t// 数据总数，用于分页 \n\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/util/MyHttpSessionHolder.java",
    "content": "package com.pj.sso.util;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.servlet.http.HttpSession;\nimport javax.servlet.http.HttpSessionEvent;\nimport javax.servlet.http.HttpSessionListener;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 记录所有已创建的 HttpSession 对象 \n * \n * <b> 此种方式有性能问题，仅做demo示例，真实项目中请更换为其它方案记录用户会话数据 </b>\n * \n * @author click33\n * @since 2022-4-30\n */\n@Component\npublic class MyHttpSessionHolder implements HttpSessionListener {\n\t\n\tpublic static List<HttpSession> sessionList = new ArrayList<>();\n\t\n\tpublic void sessionCreated(HttpSessionEvent httpSessionEvent) {\n\t\tsessionList.add(httpSessionEvent.getSession());\n\t}\n\n\tpublic void sessionDestroyed(HttpSessionEvent httpSessionEvent) {\n\t\tHttpSession session = httpSessionEvent.getSession();\n\t\tsessionList.remove(session);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 9004\n\nforest: \n    # 打开/关闭Forest请求日志（默认为 true）\n    log-request: true\n    \nspring: \n    # Redis连接\n    redis: \n        # Redis数据库索引\n        database: 2\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso3-client-resdk</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-sso</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/SaSsoClientReSdkApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class SaSsoClientReSdkApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSsoClientReSdkApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式三 (ReSdk版) demo 启动成功 ----------------------\");\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9005\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9005\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9005\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改hosts文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/MyHttpSessionHolder.java",
    "content": "package com.pj.resdk;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.servlet.http.HttpSession;\nimport javax.servlet.http.HttpSessionEvent;\nimport javax.servlet.http.HttpSessionListener;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * 记录所有已创建的 HttpSession 对象 \n * \n * <b> 此种方式有性能问题，仅做demo示例，真实项目中请更换为其它方案记录用户会话数据 </b>\n * \n * @author click33\n * @since 2022-4-30\n */\n@Component\npublic class MyHttpSessionHolder implements HttpSessionListener {\n\t\n\tpublic static List<HttpSession> sessionList = new ArrayList<>();\n\t\n\tpublic void sessionCreated(HttpSessionEvent httpSessionEvent) {\n\t\tsessionList.add(httpSessionEvent.getSession());\n\t}\n\n\tpublic void sessionDestroyed(HttpSessionEvent httpSessionEvent) {\n\t\tHttpSession session = httpSessionEvent.getSession();\n\t\tsessionList.remove(session);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/resdk/StpLogicForHttpSession.java",
    "content": "package com.pj.resdk;\n\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\n\nimport javax.servlet.http.HttpSession;\nimport java.util.Objects;\n\n/**\n * 会话对象 - httpSession 版\n *\n * @author click33\n * @since 2025/5/6\n */\npublic class StpLogicForHttpSession extends StpLogic {\n\n    /**\n     * 初始化 StpLogic, 并指定账号类型\n     *\n     * @param type /\n     *\n     */\n    public StpLogicForHttpSession(String type) {\n        super(type);\n    }\n\n    // 判断当前会话是否已登录\n    @Override\n    public boolean isLogin() {\n\n        return SpringMVCUtil.getRequest().getSession().getAttribute(\"userId\") != null;\n    }\n\n    // 获取当前会话的登录ID\n    @Override\n    public Object getLoginId() {\n        Object userId = SpringMVCUtil.getRequest().getSession().getAttribute(\"userId\");\n        if(userId == null) {\n            throw new RuntimeException(\"当前会话未登录\");\n        }\n        return userId;\n    }\n\n    // 获取当前登录设备 id\n    @Override\n    public String getLoginDeviceId() {\n        return null;\n    }\n\n    // 当前会话注销\n    @Override\n    public void logout(SaLogoutParameter logoutParameter) {\n        SpringMVCUtil.getRequest().getSession().removeAttribute(\"userId\");\n    }\n\n    // 当前账号id注销\n    @Override\n    public void _logout(Object loginId, SaLogoutParameter logoutParameter) {\n        System.out.println(\"--- 注销账号id：\" + loginId);\n        for (HttpSession session: MyHttpSessionHolder.sessionList) {\n            Object userId = session.getAttribute(\"userId\");\n            if(Objects.equals(String.valueOf(userId), String.valueOf(loginId))) {\n                session.removeAttribute(\"userId\");\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/GlobalExceptionHandler.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport com.pj.resdk.StpLogicForHttpSession;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.servlet.http.HttpSession;\n\n/**\n * SSO Client端 Controller \n * @author click33\n */\n@RestController\npublic class SsoClientController {\n\n\t// SSO-Client端：首页\n\t@RequestMapping(\"/\")\n\tpublic String index(HttpSession session) {\n\t\tboolean isLogin = session.getAttribute(\"userId\") != null;\n\t\tObject loginId = session.getAttribute(\"userId\");\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式三-ReSdk)</h2>\" +\n\t\t\t\t\"<p>当前会话是否登录：\" + isLogin + \" (\" + loginId + \")</p>\" +\n\t\t\t\t\"<p> \" +\n\t\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> - \" +\n\t\t\t\t\t\"<a href='/sso/myInfo' target='_blank'>账号资料</a>\" +\n\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\n\t/*\n\t * SSO-Client端：处理所有 SSO 相关请求\n\t * \t\thttp://{host}:{port}/sso/login\t\t\t-- Client 端登录地址\n\t * \t\thttp://{host}:{port}/sso/logout\t\t\t-- Client 端注销地址（isSlo=true时打开）\n\t * \t\thttp://{host}:{port}/sso/pushC\t\t\t-- Client 端接收消息推送地址\n\t */\n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoLogin() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\n\t// 当前应用独自注销 (不退出其它应用)\n\t@RequestMapping(\"/sso/logoutByAlone\")\n\tpublic Object logoutByAlone(HttpSession session) {\n\t\tsession.removeAttribute(\"userId\");\n\t\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n\t}\n\n\t// 配置SSO相关参数\n\t@Autowired\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t\t// 自定义底层使用的会话操作对象\n\t\tssoClientTemplate.setStpLogic(new StpLogicForHttpSession(StpUtil.TYPE));\n\n\t\t// 自定义校验 ticket 返回值的处理逻辑 （每次从认证中心获取校验 ticket 的结果后调用）\n\t\tssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> {\n\t\t\tHttpSession session = SpringMVCUtil.getRequest().getSession();\n\t\t\tsession.setAttribute(\"userId\", ctr.loginId);\n\t\t\treturn SaHolder.getResponse().redirect(back);\n\t\t};\n\t}\n\n\t// 查询我的账号信息：sso-client 前端 -> sso-center 后端 -> sso-server 后端\n\t@RequestMapping(\"/sso/myInfo\")\n\tpublic Object myInfo(HttpSession session) {\n\t\t// 如果尚未登录 \n\t\tif(session.getAttribute(\"userId\") == null) {\n\t\t\treturn \"尚未登录，无法获取\";\n\t\t}\n\n\t\t// 推送消息\n\t\tSaSsoMessage message = new SaSsoMessage();\n\t\tmessage.setType(\"userinfo\");\n\t\tmessage.set(\"loginId\", session.getAttribute(\"userId\"));\n\t\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n        // 返回给前端 \n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 9005\n\n# sa-token 配置\nsa-token:\n    # 是否打印操作日志\n    is-log: true\n    # sso-client 相关配置\n    sso-client:\n        # client 标识\n        client: sso-client3-resdk\n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 使用 Http 请求校验ticket (模式三)\n        is-http: true\n        # API 接口调用秘钥\n        secret-key: SSO-C3-ReSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso-server-solon</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- Solon -->\n\t<parent>\n\t\t<groupId>org.noear</groupId>\n\t\t<artifactId>solon-parent</artifactId>\n\t\t<version>3.2.1</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot Web依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon-web</artifactId>\n\t\t\t<version>${solon.version}</version>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-solon-plugin</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合redis  -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redisx</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合snack3 (json) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-snack3</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- snack3 版号要 >= 3.2.133 -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>snack3</artifactId>\n\t\t\t<version>3.2.133</version>\n\t\t</dependency>\n\n\t\t<!-- 视图引擎（在前后端不分离模式下提供视图支持） -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon.view.thymeleaf</artifactId>\n\t\t\t<version>${solon.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaConfig.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisx;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * @author noear 2023/3/13 created\n */\n@Configuration\npublic class SaConfig {\n\n    /**\n     * 构建建 SaToken redis dao（如果不需要 redis；可以注释掉）\n     * */\n    @Bean\n    public SaTokenDao saTokenDaoInit(@Inject(\"${sa-token.dao.redis}\") SaTokenDaoForRedisx saTokenDao) {\n        return saTokenDao;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/SaSsoServerApp.java",
    "content": "package com.pj;\n\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.noear.solon.Solon;\nimport org.noear.solon.annotation.SolonMain;\n\n@SolonMain\npublic class SaSsoServerApp {\n\n\tpublic static void main(String[] args) {\n\t\tSolon.start(SaSsoServerApp.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getServerConfig());\n\t\tSystem.out.println(\"统一认证登录地址：http://sa-sso-server.com:9000/sso/auth\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\n\t/*\n\t * 类型序列化测试代码\n\t *\n\n\t\tSystem.out.println(SaManager.getSaJsonTemplate());\n\n\t\tSaSsoClientInfo sci = new SaSsoClientInfo();\n\t\tsci.setClient(\"client1\");\n\n\t\tList<SaSsoClientInfo> list = new ArrayList<>();\n\t\tlist.add(sci);\n\n\t\tStpUtil.getSessionByLoginId(10001).set(\"list\", list);\n\n\t\tList<SaSsoClientInfo> list2 = (List)StpUtil.getSessionByLoginId(10001).get(\"list\");\n\t\tfor (SaSsoClientInfo info : list2) {\n\t\t\tSystem.out.println(info);\n\t\t}\n\n\t */\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/H5Controller.java",
    "content": "package com.pj.h5;\n\n\nimport cn.dev33.satoken.sso.template.SaSsoUtil;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\n/**\n * 前后台分离架构下集成SSO所需的代码 （SSO-Server端）\n * <p>（注：如果不需要前后端分离架构下集成SSO，可删除此包下所有代码）</p>\n * @author click33\n *\n */\n@Controller\npublic class H5Controller {\n\n\t/**\n\t * 获取 redirectUrl\n\t */\n\t@Mapping(\"/sso/getRedirectUrl\")\n\tpublic SaResult getRedirectUrl(String client, String redirect, String mode) {\n\t\t// 未登录情况下，返回 code=401\n\t\tif(StpUtil.isLogin() == false) {\n\t\t\treturn SaResult.code(401);\n\t\t}\n\t\t// 已登录情况下，构建 redirectUrl\n\t\tredirect = SaFoxUtil.decoderUrl(redirect);\n\t\tif(SaSsoConsts.MODE_SIMPLE.equals(mode)) {\n\t\t\t// 模式一\n\t\t\tSaSsoUtil.checkRedirectUrl(client, redirect);\n\t\t\treturn SaResult.data(redirect);\n\t\t} else {\n\t\t\t// 模式二或模式三\n\t\t\tString redirectUrl = SaSsoUtil.buildRedirectUrl(client, redirect, StpUtil.getLoginId(), StpUtil.getLoginDeviceId());\n\t\t\treturn SaResult.data(redirectUrl);\n\t\t}\n\t}\n\n//\t/**\n//\t * 控制当前类的异常\n//\t */\n//\t@Override\n//\tpublic void render(Object data, Context ctx) throws Throwable {\n//\t\tif (data instanceof Throwable) {\n//\t\t\tThrowable e = (Throwable) data;\n//\t\t\te.printStackTrace();\n//\t\t\tctx.render(SaResult.error(e.getMessage()));\n//\t\t} else {\n//\t\t\tctx.render(data);\n//\t\t}\n//\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/h5/SaTokenConfigure.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 （解决跨域问题）\n *\n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\n    /**\n     * CORS 跨域处理策略\n     */\n    @Bean\n    public SaCorsHandleFunction corsHandle() {\n        return (req, res, sto) -> {\n            res.\n                    // 允许指定域访问跨域资源\n                    setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                    // 允许所有请求方式\n                    .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                    // 有效时间\n                    .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                    // 允许的header参数\n                    .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n            // 如果是预检请求，则立即返回到前端\n            SaRouter.match(SaHttpMethod.OPTIONS)\n                    .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                    .back();\n        };\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java",
    "content": "package com.pj.sso;\n\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Component;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@Component\npublic class GlobalExceptionFilter implements Filter {\n\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\ttry {\n\t\t\tchain.doFilter(ctx);\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t\tctx.render(SaResult.error(e.getMessage()));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/HomeController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\n/**\n * SSO 平台中心模式示例，跳连接进入子系统\n */\n@Controller\npublic class HomeController {\n\n    // 平台化首页\n    @Mapping(\"/home\")\n    public Object index() {\n        // 如果未登录，则先去登录\n        if(!StpUtil.isLogin()) {\n            return SaHolder.getResponse().redirect(\"/sso/auth\");\n        }\n\n        // 拼接各个子系统的地址，格式形如：/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页}\n        String link1 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/\";\n        String link2 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/\";\n        String link3 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/\";\n\n        // 组织网页结构返回到前端\n        String title = \"<h2>SSO 平台首页 (平台中心模式)</h2>\";\n        String client1 = \"<p><a href='\" + link1 + \"' target='_blank'> 进入Client1系统 </a></p>\";\n        String client2 = \"<p><a href='\" + link2 + \"' target='_blank'> 进入Client2系统 </a></p>\";\n        String client3 = \"<p><a href='\" + link3 + \"' target='_blank'> 进入Client3系统 </a></p>\";\n\n        return title + client1 + client2 + client3;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/java/com/pj/sso/SsoServerController.java",
    "content": "package com.pj.sso;\n\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\nimport org.noear.solon.core.handle.ModelAndView;\n\n/**\n * Sa-Token-SSO Server端 Controller \n * @author click33\n *\n */\n@Controller\n@Configuration\npublic class SsoServerController {\n\n\t/**\n\t * SSO-Server端：处理所有SSO相关请求\n\t * \t\thttp://{host}:{port}/sso/auth\t\t\t-- 单点登录授权地址\n\t * \t\thttp://{host}:{port}/sso/doLogin\t\t-- 账号密码登录接口，接受参数：name、pwd\n\t * \t\thttp://{host}:{port}/sso/signout\t\t-- 单点注销地址（isSlo=true时打开）\n\t */\n\t@Mapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoServerProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数\n\t@Bean\n\tprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\n\t\t// 配置：未登录时返回的View\n\t\tssoServerTemplate.strategy.notLoginView = () -> {\n\t\t\treturn new ModelAndView(\"sa-login.html\");\n\t\t};\n\n\t\t// 配置：登录处理函数\n\t\tssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {\n\t\t\t// 此处仅做模拟登录，真实环境应该查询数据库进行登录\n\t\t\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\tString deviceId = SaHolder.getRequest().getParam(\"deviceId\", SaFoxUtil.getRandomString(32));\n\t\t\t\tStpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId));\n\t\t\t\treturn SaResult.ok(\"登录成功！\").setData(StpUtil.getTokenValue());\n\t\t\t}\n\t\t\treturn SaResult.error(\"登录失败！\");\n\t\t};\n\n\t\t// 添加消息处理器：userinfo (获取用户资料) （用于为 client 端开放拉取数据的接口）\n\t\tssoServerTemplate.messageHolder.addHandle(\"userinfo\", (ssoTemplate, message) -> {\n\t\t\tSystem.out.println(\"收到消息：\" + message);\n\n\t\t\t// 自定义返回结果（模拟）\n\t\t\treturn SaResult.ok()\n\t\t\t\t\t.set(\"id\", message.get(\"loginId\"))\n\t\t\t\t\t.set(\"name\", \"LinXiaoYu\")\n\t\t\t\t\t.set(\"sex\", \"女\")\n\t\t\t\t\t.set(\"age\", 18);\n\t\t});\n\n\t}\n\n}\n"
  },
  {
    "path": "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",
    "content": "/*! layer-v3.1.1 Web弹层组件 MIT License  http://layer.layui.com/  By 贤心 */\n ;!function(e,t){\"use strict\";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;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:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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:\"&#x4FE1;&#x606F;\",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?'<div class=\"layui-layer-title\" style=\"'+(f?r.title[1]:\"\")+'\">'+(f?r.title[0]:r.title)+\"</div>\":\"\";return r.zIndex=s,t([r.shade?'<div class=\"layui-layer-shade\" id=\"layui-layer-shade'+a+'\" times=\"'+a+'\" style=\"'+(\"z-index:\"+(s-1)+\"; \")+'\"></div>':\"\",'<div class=\"'+l[0]+(\" layui-layer-\"+o.type[r.type])+(0!=r.type&&2!=r.type||r.shade?\"\":\" layui-layer-border\")+\" \"+(r.skin||\"\")+'\" id=\"'+l[0]+a+'\" type=\"'+o.type[r.type]+'\" times=\"'+a+'\" showtime=\"'+r.time+'\" conType=\"'+(e?\"object\":\"string\")+'\" style=\"z-index: '+s+\"; width:\"+r.area[0]+\";height:\"+r.area[1]+(r.fixed?\"\":\";position:absolute;\")+'\">'+(e&&2!=r.type?\"\":u)+'<div id=\"'+(r.id||\"\")+'\" class=\"layui-layer-content'+(0==r.type&&r.icon!==-1?\" layui-layer-padding\":\"\")+(3==r.type?\" layui-layer-loading\"+r.icon:\"\")+'\">'+(0==r.type&&r.icon!==-1?'<i class=\"layui-layer-ico layui-layer-ico'+r.icon+'\"></i>':\"\")+(1==r.type&&e?\"\":r.content||\"\")+'</div><span class=\"layui-layer-setwin\">'+function(){var e=c?'<a class=\"layui-layer-min\" href=\"javascript:;\"><cite></cite></a><a class=\"layui-layer-ico layui-layer-max\" href=\"javascript:;\"></a>':\"\";return r.closeBtn&&(e+='<a class=\"layui-layer-ico '+l[7]+\" \"+l[7]+(r.title?r.closeBtn:4==r.type?\"1\":\"2\")+'\" href=\"javascript:;\"></a>'),e}()+\"</span>\"+(r.btn?function(){var e=\"\";\"string\"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t<i;t++)e+='<a class=\"'+l[6]+t+'\">'+r.btn[t]+\"</a>\";return'<div class=\"'+l[6]+\" layui-layer-btn-\"+(r.btnAlign||\"\")+'\">'+e+\"</div>\"}():\"\")+(r.resize?'<span class=\"layui-layer-resize\"></span>':\"\")+\"</div>\"],u,i('<div class=\"layui-layer-move\"></div>')),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='<iframe scrolling=\"'+(t.content[1]||\"auto\")+'\" allowtransparency=\"true\" id=\"'+l[4]+a+'\" name=\"'+l[4]+a+'\" onload=\"this.className=\\'\\';\" class=\"layui-layer-load\" frameborder=\"0\" src=\"'+t.content[0]+'\"></iframe>';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]+'<i class=\"layui-layer-TipsG\"></i>',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;a<c.stX&&(a=c.stX),a>f&&(a=f),o<c.stY&&(o=c.stY),o>u&&(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?'<textarea class=\"layui-layer-input\"'+a+\">\"+(e.value||\"\")+\"</textarea>\":function(){return'<input type=\"'+(1==e.formType?\"password\":\"text\")+'\" class=\"layui-layer-input\" value=\"'+(e.value||\"\")+'\">'}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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(\"&#x6700;&#x591A;&#x8F93;&#x5165;\"+(e.maxlength||500)+\"&#x4E2A;&#x5B57;&#x6570;\",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='<span class=\"'+n+'\">'+t[0].title+\"</span>\";i<e;i++)a+=\"<span>\"+t[i].title+\"</span>\";return a}(),content:'<ul class=\"layui-layer-tabmain\">'+function(){var e=t.length,i=1,a=\"\";if(e>0)for(a='<li class=\"layui-layer-tabli '+n+'\">'+(t[0].content||\"no content\")+\"</li>\";i<e;i++)a+='<li class=\"layui-layer-tabli\">'+(t[i].content||\"no  content\")+\"</li>\";return a}()+\"</ul>\",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(\"&#x6CA1;&#x6709;&#x56FE;&#x7247;\")}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]<r[1]&&(a[0]=a[0]/r[1],a[1]=a[1]/r[1])}return[a[0]+\"px\",a[1]+\"px\"]}(),title:!1,shade:.9,shadeClose:!0,closeBtn:!1,move:\".layui-layer-phimg img\",moveType:1,scrollbar:!1,moveOut:!0,isOutAnim:!1,skin:\"layui-layer-photos\"+c(\"photos\"),content:'<div class=\"layui-layer-phimg\"><img src=\"'+u[d].src+'\" alt=\"'+(u[d].alt||\"\")+'\" layer-pid=\"'+u[d].pid+'\"><div class=\"layui-layer-imgsee\">'+(u.length>1?'<span class=\"layui-layer-imguide\"><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgprev\"></a><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgnext\"></a></span>':\"\")+'<div class=\"layui-layer-imgbar\" style=\"display:'+(a?\"block\":\"\")+'\"><span class=\"layui-layer-imgtit\"><a href=\"javascript:;\">'+(u[d].alt||\"\")+\"</a><em>\"+s.imgIndex+\"/\"+u.length+\"</em></span></div></div></div>\",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(\"&#x5F53;&#x524D;&#x56FE;&#x7247;&#x5730;&#x5740;&#x5F02;&#x5E38;<br>&#x662F;&#x5426;&#x7EE7;&#x7EED;&#x67E5;&#x770B;&#x4E0B;&#x4E00;&#x5F20;&#xFF1F;\",{time:3e4,btn:[\"&#x4E0B;&#x4E00;&#x5F20;\",\"&#x4E0D;&#x770B;&#x4E86;\"],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);"
  },
  {
    "path": "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",
    "content": "/*! layer mobile-v2.0.0 Web弹层组件 MIT License  http://layer.layui.com/mobile  By 贤心 */\n ;!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?'<h3 style=\"'+(e?n.title[1]:\"\")+'\">'+(e?n.title[0]:n.title)+\"</h3>\":\"\"}(),c=function(){\"string\"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e='<span yes type=\"1\">'+n.btn[0]+\"</span>\",2===t&&(e='<span no type=\"0\">'+n.btn[1]+\"</span>\"+e),'<div class=\"layui-m-layerbtn\">'+e+\"</div>\"):\"\"}();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='<i></i><i class=\"layui-m-layerload\"></i><i></i><p>'+(n.content||\"\")+\"</p>\"),n.skin&&(n.anim=\"up\"),\"msg\"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?\"<div \"+(\"string\"==typeof n.shade?'style=\"'+n.shade+'\"':\"\")+' class=\"layui-m-layershade\"></div>':\"\")+'<div class=\"layui-m-layermain\" '+(n.fixed?\"\":'style=\"position:static;\"')+'><div class=\"layui-m-layersection\"><div class=\"layui-m-layerchild '+(n.skin?\"layui-m-layer-\"+n.skin+\" \":\"\")+(n.className?n.className:\"\")+\" \"+(n.anim?\"layui-m-anim-\"+n.anim:\"\")+'\" '+(n.style?'style=\"'+n.style+'\"':\"\")+\">\"+l+'<div class=\"layui-m-layercont\">'+n.content+\"</div>\"+c+\"</div></div></div>\",!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;o<r;o++)l.touch(s[o],a);if(e.shade&&e.shadeClose){var c=t[i](\"layui-m-layershade\")[0];l.touch(c,function(){layer.close(n.index,e.end)})}e.end&&(l.end[n.index]=e.end)},e.layer={v:\"2.0\",index:r,open:function(e){var t=new c(e||{});return t.index},close:function(e){var n=a(\"#\"+o[0]+e)[0];n&&(n.innerHTML=\"\",t.body.removeChild(n),clearTimeout(l.timer[e]),delete l.timer[e],\"function\"==typeof l.end[e]&&l.end[e](),delete l.end[e])},closeAll:function(){for(var e=t[i](o[0]),n=0,a=e.length;n<a;n++)layer.close(0|e[0].getAttribute(\"index\"))}},\"function\"==typeof define?define(function(){return layer}):function(){var e=document.scripts,n=e[e.length-1],i=n.src,a=i.substring(0,i.lastIndexOf(\"/\")+1);n.getAttribute(\"merge\")||document.head.appendChild(function(){var e=t.createElement(\"link\");return e.href=a+\"need/layer.css?2.0\",e.type=\"text/css\",e.rel=\"styleSheet\",e.id=\"layermcss\",e}())}()}(window);"
  },
  {
    "path": "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",
    "content": ".layui-m-layer{position:relative;z-index:19891014}.layui-m-layer *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.layui-m-layermain,.layui-m-layershade{position:fixed;left:0;top:0;width:100%;height:100%}.layui-m-layershade{background-color:rgba(0,0,0,.7);pointer-events:auto}.layui-m-layermain{display:table;font-family:Helvetica,arial,sans-serif;pointer-events:none}.layui-m-layermain .layui-m-layersection{display:table-cell;vertical-align:middle;text-align:center}.layui-m-layerchild{position:relative;display:inline-block;text-align:left;background-color:#fff;font-size:14px;border-radius:5px;box-shadow:0 0 8px rgba(0,0,0,.1);pointer-events:auto;-webkit-overflow-scrolling:touch;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@-webkit-keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.layui-m-anim-scale{animation-name:layui-m-anim-scale;-webkit-animation-name:layui-m-anim-scale}@-webkit-keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}.layui-m-anim-up{-webkit-animation-name:layui-m-anim-up;animation-name:layui-m-anim-up}.layui-m-layer0 .layui-m-layerchild{width:90%;max-width:640px}.layui-m-layer1 .layui-m-layerchild{border:none;border-radius:0}.layui-m-layer2 .layui-m-layerchild{width:auto;max-width:260px;min-width:40px;border:none;background:0 0;box-shadow:none;color:#fff}.layui-m-layerchild h3{padding:0 10px;height:60px;line-height:60px;font-size:16px;font-weight:400;border-radius:5px 5px 0 0;text-align:center}.layui-m-layerbtn span,.layui-m-layerchild h3{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-m-layercont{padding:50px 30px;line-height:22px;text-align:center}.layui-m-layer1 .layui-m-layercont{padding:0;text-align:left}.layui-m-layer2 .layui-m-layercont{text-align:center;padding:0;line-height:0}.layui-m-layer2 .layui-m-layercont i{width:25px;height:25px;margin-left:8px;display:inline-block;background-color:#fff;border-radius:100%;-webkit-animation:layui-m-anim-loading 1.4s infinite ease-in-out;animation:layui-m-anim-loading 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.layui-m-layerbtn,.layui-m-layerbtn span{position:relative;text-align:center;border-radius:0 0 5px 5px}.layui-m-layer2 .layui-m-layercont p{margin-top:20px}@-webkit-keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}@keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.layui-m-layer2 .layui-m-layercont i:first-child{margin-left:0;-webkit-animation-delay:-.32s;animation-delay:-.32s}.layui-m-layer2 .layui-m-layercont i.layui-m-layerload{-webkit-animation-delay:-.16s;animation-delay:-.16s}.layui-m-layer2 .layui-m-layercont>div{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}"
  },
  {
    "path": "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",
    "content": ".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}}"
  },
  {
    "path": "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",
    "content": "*{margin: 0; padding: 0;}\nbody{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}\n::-webkit-input-placeholder{color: #ccc;}\n\n/* 视图盒子 */\n.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}\n/* 背景 EAEFF3 */\n.bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);}\n.bg-2{height: 50%; background-color: #EAEFF3;}\n\n/* 渐变背景 */\n/*.bg-1{\n    background-size: 500%;\n\tbackground-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5);\n\tanimation: bganimation 30s infinite;\n}\n@keyframes bganimation{\n    0%{background-position: 0% 50%;}\n    50%{background-position: 100% 50%;}\n    100%{background-position: 0% 50%;}\n}  */\n/* 背景 */\n.bg-1{background: #101C34;}\n.bg-2{background: #101C34;}\n/* .bg-1{height: 100%; background-image: url(./login-bg.png); background-size: 100% 100%;} */\n\n\n/* 内容盒子 */\n.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}\n\n/* 登录盒子 */\n/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */\n.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}\n.login-box{display: flex; align-items: center; text-align: center;}\n\n/* 表单 */\n.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}\n.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}\n.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}\n\n/* 输入框 */\n.from-item{border: 0px #000 solid; margin-bottom: 15px;}\n.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;}\n.s-input{font-size: 12px;}\n.s-input:focus{border-color: #409eff}\n\n/* 登录按钮 */\n.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}\n.s-btn:hover{background-color: #50aEFF;}\n\n/* 重置按钮 */\n.reset-box{text-align: left; font-size: 12px;}\n.reset-box a{text-decoration: none;}\n.reset-box a:hover{text-decoration: underline;}\n\n/* loading框样式 */\n.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }"
  },
  {
    "path": "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",
    "content": "// sa \nvar sa = {};\n\n// 打开loading\nsa.loading = function(msg) {\n\tlayer.closeAll();\t// 开始前先把所有弹窗关了\n\treturn layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });\n};\n\n// 隐藏loading\nsa.hideLoading = function() {\n\tlayer.closeAll();\n};\n\n\n// ----------------------------------- 登录事件 -----------------------------------\n\n$('.login-btn').click(function(){\n\tsa.loading(\"正在登录...\");\n\t// 开始登录\n\tsetTimeout(function() {\n\t\t$.ajax({\n\t\t\turl: \"sso/doLogin\",\n\t\t\ttype: \"post\", \n\t\t\tdata: {\n\t\t\t\tname: $('[name=name]').val(),\n\t\t\t\tpwd: $('[name=pwd]').val()\n\t\t\t},\n\t\t\tdataType: 'json',\n\t\t\tsuccess: function(res){\n\t\t\t\tconsole.log('返回数据：', res);\n\t\t\t\tsa.hideLoading();\n\t\t\t\tif(res.code == 200) {\n\t\t\t\t\tlayer.msg('登录成功', {anim: 0, icon: 6 }); \n\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\tlocation.reload();\n\t\t\t\t\t}, 800)\n\t\t\t\t} else {\n\t\t\t\t\tlayer.msg(res.msg, {anim: 6, icon: 2 }); \n\t\t\t\t}\n\t\t\t},\n\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\tsa.hideLoading();\n\t\t\t\tif(xhr.status == 0){\n\t\t\t\t\treturn layer.alert('无法连接到服务器，请检查网络');\n\t\t\t\t}\n\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t}\n\t\t});\n\t}, 400);\n});\n\n// 绑定回车事件\n$('[name=name],[name=pwd]').bind('keypress', function(event){\n\tif(event.keyCode == \"13\") {\n\t\t$('.login-btn').click();\n\t}\n});\n\n// 输入框获取焦点\n$(\"[name=name]\").focus();\n\n// 打印信息 \nvar str = \"This page is provided by Sa-Token, Please refer to: \" + \"https://sa-token.cc/\";\nconsole.log(str);\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/WEB-INF/view/sa-login.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<title>Sa-SSO-Server 认证中心-登录</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<base th:href=\"@{/static}\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t\t<link rel=\"stylesheet\" href=\"./sa-res/login.css\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\">\n\t\t\t<div class=\"bg-1\"></div>\n\t\t\t<div class=\"bg-2\"></div>\n\t\t\t<div class=\"content-box\">\n\t\t\t\t<div class=\"login-box\">\n\t\t\t\t\t<div class=\"from-box\">\n\t\t\t\t\t\t<h2 class=\"from-title\">Sa-SSO-Server 认证中心</h2>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"name\" placeholder=\"请输入账号\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"pwd\" type=\"password\" placeholder=\"请输入密码\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<button class=\"s-input s-btn login-btn\">登录</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item reset-box\">\n\t\t\t\t\t\t\t<a href=\"javascript: location.reload();\" >刷新</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- 底部 版权 -->\n\t\t\t<div style=\"position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;\">\n\t\t\t\tThis page is provided by Sa-Token-SSO \n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- scripts -->\n\t\t<script src=\"./sa-res/jquery.min.js\"></script>\n\t\t<script src=\"./sa-res/layer/layer.js\"></script>\n\t\t<script src=\"./sa-res/login.js\"></script>\n\t\t\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso-server-solon/src/main/resources/app.yml",
    "content": "# 端口\nserver:\n    port: 9000\n\n# Sa-Token 配置\nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # SSO 模式一配置  (非模式一不需要配置)\n#    cookie:\n#        # 配置 Cookie 作用域\n#        domain: stp.com\n\n    # SSO-Server 配置\n    sso-server:\n        # Ticket有效期 (单位: 秒)，默认五分钟\n        ticket-timeout: 300\n        # 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\n        home-route: /home\n        # 是否启用匿名 client (开启匿名 client 后，允许客户端接入时不提交 client 参数)\n        allow-anon-client: true\n        # 所有允许的授权回调地址 (匿名 client 使用)\n        allow-url: \"*\"\n        # API 接口调用秘钥 (全局默认 + 匿名 client 使用)\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n        # 应用列表：配置接入的应用信息\n        clients:\n            # 应用 sso-client1：采用模式一对接 (同域、同Redis)\n            sso-client1:\n                client: sso-client1\n                allowUrl: \"*\"\n            # 应用 sso-client2：采用模式二对接 (跨域、同Redis)\n            sso-client2:\n                client: sso-client2\n                allowUrl: \"*\"\n                secretKey: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n            # 应用 sso-client3：采用模式三对接 (跨域、跨Redis)\n            sso-client3:\n                # 应用名称\n                client: sso-client3\n                # 允许授权地址\n                allowUrl: \"*\"\n                # 是否接收消息推送\n                isPush: true\n                # 消息推送地址\n                pushUrl: http://sa-sso-client1.com:9003/sso/pushC\n                # 接口调用秘钥 (如果不配置则使用全局默认秘钥)\n                secretKey: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\nsa-token.dao: #名字可以随意取\n    redis:\n        server: \"localhost:6379\"\n#        password: 123456\n        db: 1\n        maxTotal: 200\n\n\n    \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso1-client-solon</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- Solon -->\n\t<parent>\n\t\t<groupId>org.noear</groupId>\n\t\t<artifactId>solon-parent</artifactId>\n\t\t<version>3.2.1</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon-web</artifactId>\n\t\t\t<version>${solon.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-solon-plugin</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 redisx -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisx</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 插件：整合snack3 (json) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-snack3</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- snack3 版号要 >= 3.2.133 -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>snack3</artifactId>\n\t\t\t<version>3.2.133</version>\n\t\t</dependency>\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaConfig.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisx;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * @author noear 2023/3/13 created\n */\n@Configuration\npublic class SaConfig {\n\n    /**\n     * 配置 Sa-Token 单独使用的Redis连接 （此处需要和SSO-Server端连接同一个Redis）\n     * */\n    @Bean\n    public SaTokenDao saTokenDaoInit(@Inject(\"${sa-token.dao.redis}\") SaTokenDaoForRedisx saTokenDao) {\n        return saTokenDao;\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/SaSso1ClientApp.java",
    "content": "package com.pj;\n\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.noear.solon.Solon;\nimport org.noear.solon.annotation.SolonMain;\n\n/**\n * SSO模式一，Client端 Demo \n * @author click33\n *\n */\n@SolonMain\npublic class SaSso1ClientApp {\n\n\tpublic static void main(String[] args) {\n\t\tSolon.start(SaSso1ClientApp.class, args);\n\t\tSystem.out.println(\"\\nSa-Token SSO模式一 Client端启动成功\");\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://s1.stp.com:9001\");\n\t\tSystem.out.println(\"测试访问应用端二: http://s2.stp.com:9001\");\n\t\tSystem.out.println(\"测试访问应用端三: http://s3.stp.com:9001\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\nimport org.noear.solon.annotation.Produces;\nimport org.noear.solon.boot.web.MimeType;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Render;\n\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@Controller\npublic class SsoClientController implements Render {\n\n\t// SSO-Client端：首页\n\t@Produces(MimeType.TEXT_HTML_VALUE)\n\t@Mapping(\"/\")\n\tpublic String index() {\n\t\tString url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), Context.current().queryString()) );\n\t\tSaSsoClientConfig cfg = SaSsoManager.getClientConfig();\n\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式一)</h2>\" +\n\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\"<p>\" +\n\t\t\t\t\"<a href='\" + cfg.splicingAuthUrl() + \"?mode=simple&client=\" + cfg.getClient() + \"&redirect=\" + url + \"'>登录</a> - \" +\n\t\t\t\t\"<a href='\" + cfg.splicingSignoutUrl() + \"?singleDeviceIdLogout=true&back=\" + url + \"'>单浏览器注销</a> - \" +\n\t\t\t\t\"<a href='\" + cfg.splicingSignoutUrl() + \"?back=\" + url + \"'>全端注销</a> \" +\n\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\n\t// 全局异常拦截并转换\n\t@Override\n\tpublic void render(Object data, Context ctx) throws Throwable {\n\t\tif(data instanceof Exception){\n\t\t\tdata = SaResult.error(((Exception)data).getMessage());\n\t\t}\n\t\tctx.render(data);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso1-client-solon/src/main/resources/app.yml",
    "content": "# 端口\nserver:\n    port: 9001\n\n# Sa-Token 配置 \nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # SSO-相关配置\n    sso-client:\n        # client 标识\n        client: sso-client1\n        # SSO-Server端 - 主机地址\n        server-url: http://sso.stp.com:9000\n    \n# 配置 Sa-Token 单独使用的Redis连接 （此处需要和SSO-Server端连接同一个Redis）\nsa-token.dao: #名字可以随意取\n    redis:\n        server: \"localhost:6379\"\n#        password: 123456\n        db: 1\n        maxTotal: 200\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso2-client-solon</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- Solon -->\n\t<parent>\n\t\t<groupId>org.noear</groupId>\n\t\t<artifactId>solon-parent</artifactId>\n\t\t<version>3.2.1</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- Solon 依赖 -->\n\t\t<!--<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon-api</artifactId>\n\t\t</dependency>-->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon-web</artifactId>\n\t\t\t<version>${solon.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-solon-plugin</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 redisx -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redisx</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合snack3 (json) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-snack3</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- snack3 版号要 >= 3.2.133 -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>snack3</artifactId>\n\t\t\t<version>3.2.133</version>\n\t\t</dependency>\n\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaConfig.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisx;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * @author noear 2023/3/13 created\n */\n@Configuration\npublic class SaConfig {\n\n    /**\n     * 配置 Sa-Token 单独使用的Redis连接 （此处需要和SSO-Server端连接同一个Redis）\n     * */\n    @Bean\n    public SaTokenDao saTokenDaoInit(@Inject(\"${sa-token.dao.redis}\") SaTokenDaoForRedisx saTokenDao) {\n        return saTokenDao;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/SaSso2ClientApp.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.noear.solon.Solon;\nimport org.noear.solon.annotation.SolonMain;\n\n@SolonMain\npublic class SaSso2ClientApp {\n\n\tpublic static void main(String[] args) {\n\t\tSolon.start(SaSso2ClientApp.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Solon Sa-Token SSO 模式二 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9002\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9002\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9002\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改hosts文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/H5Controller.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.sso.model.SaCheckTicketResult;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\n/**\n * 前后台分离架构下集成SSO所需的代码 （SSO-Client端）\n * <p>（注：如果不需要前后端分离架构下集成SSO，可删除此包下所有代码）</p>\n * @author click33\n *\n */\n@Controller\npublic class H5Controller {\n\n\t// 判断当前是否登录\n\t@Mapping(\"/sso/isLogin\")\n\tpublic Object isLogin() {\n\t\treturn SaResult.data(StpUtil.isLogin()).set(\"loginId\", StpUtil.getLoginIdDefaultNull());\n\t}\n\n\t// 返回SSO认证中心登录地址\n\t@Mapping(\"/sso/getSsoAuthUrl\")\n\tpublic SaResult getSsoAuthUrl(String clientLoginUrl) {\n\t\tString serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, \"\");\n\t\treturn SaResult.data(serverAuthUrl);\n\t}\n\n\t// 根据 ticket 进行登录\n\t@Mapping(\"/sso/doLoginByTicket\")\n\tpublic SaResult doLoginByTicket(String ticket) {\n\t\tSaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);\n\t\tStpUtil.login(ctr.loginId, new SaLoginParameter()\n\t\t\t\t.setTimeout(ctr.remainTokenTimeout)\n\t\t\t\t.setDeviceId(ctr.deviceId)\n\t\t);\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 （解决跨域问题）\n *\n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\n    /**\n     * CORS 跨域处理策略\n     */\n    @Bean\n    public SaCorsHandleFunction corsHandle() {\n        return (req, res, sto) -> {\n            res.\n                    // 允许指定域访问跨域资源\n                    setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                    // 允许所有请求方式\n                    .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                    // 有效时间\n                    .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                    // 允许的header参数\n                    .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n            // 如果是预检请求，则立即返回到前端\n            SaRouter.match(SaHttpMethod.OPTIONS)\n                    .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                    .back();\n        };\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java",
    "content": "package com.pj.sso;\n\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Component;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@Component\npublic class GlobalExceptionFilter implements Filter {\n\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\ttry {\n\t\t\tchain.doFilter(ctx);\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t\tctx.render(SaResult.error(e.getMessage()));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.*;\nimport org.noear.solon.boot.web.MimeType;\n\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@Controller\n@Configuration\npublic class SsoClientController {\n\n\t// 首页\n\t@Produces(MimeType.TEXT_HTML_VALUE)\n\t@Mapping(\"/\")\n\tpublic String index() {\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式二)</h2>\" +\n\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\"<p> \" +\n\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - \" +\n\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> - \" +\n\t\t\t\t\"<a href='/sso/myInfo' target='_blank'>账号资料</a>\" +\n\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\n\t/*\n\t * SSO-Client端：处理所有SSO相关请求\n\t * \t\thttp://{host}:{port}/sso/login\t\t\t-- Client 端登录地址\n\t * \t\thttp://{host}:{port}/sso/logout\t\t\t-- Client 端注销地址（isSlo=true时打开）\n\t * \t\thttp://{host}:{port}/sso/pushC\t\t\t-- Client 端接收消息推送地址\n\t */\n\t@Mapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数\n\t@Bean\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t}\n\n\t// 当前应用独自注销 (不退出其它应用)\n\t@Mapping(\"/sso/logoutByAlone\")\n\tpublic Object logoutByAlone() {\n\t\tStpUtil.logout();\n\t\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n\t}\n\n\t// 查询我的账号信息：sso-client 前端 -> sso-center 后端 -> sso-server 后端\n\t@Mapping(\"/sso/myInfo\")\n\tpublic Object myInfo() {\n\t\t// 如果尚未登录\n\t\tif( ! StpUtil.isLogin()) {\n\t\t\treturn \"尚未登录，无法获取\";\n\t\t}\n\n\t\t// 获取本地 loginId\n\t\tObject loginId = StpUtil.getLoginId();\n\n\t\t// 推送消息\n\t\tSaSsoMessage message = new SaSsoMessage();\n\t\tmessage.setType(\"userinfo\");\n\t\tmessage.set(\"loginId\", loginId);\n\t\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n\t\t// 返回给前端\n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso2-client-solon/src/main/resources/app.yml",
    "content": "# 端口\nserver:\n    port: 9002\n\n# sa-token配置\nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # SSO-相关配置\n    sso-client:\n        # 应用标识\n        client: sso-client2\n        # SSO-Server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）\n        # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html\n        # API 接口调用秘钥 (单点注销时会用到)\n        secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n    \n# 配置 Sa-Token 单独使用的Redis连接 （此处需要和SSO-Server端连接同一个Redis）\nsa-token.dao: #名字可以随意取\n    redis:\n        server: \"localhost:6379\"\n#        password: 123456\n        db: 1\n        maxTotal: 200\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-sso3-client-solon</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\n\t<!-- Solon -->\n\t<parent>\n\t\t<groupId>org.noear</groupId>\n\t\t<artifactId>solon-parent</artifactId>\n\t\t<version>3.2.1</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>solon-web</artifactId>\n\t\t\t<version>${solon.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-solon-plugin</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 插件：整合SSO -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 redisx -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redisx</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合 Forest 请求工具 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-forest</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 插件：整合snack3 (json) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-snack3</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- snack3 版号要 >= 3.2.133 -->\n\t\t<dependency>\n\t\t\t<groupId>org.noear</groupId>\n\t\t\t<artifactId>snack3</artifactId>\n\t\t\t<version>3.2.133</version>\n\t\t</dependency>\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaConfig.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisx;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * @author noear 2023/3/13 created\n */\n@Configuration\npublic class SaConfig {\n\n    /**\n     * 构建建 SaToken redis dao（如果不需要 redis；可以注释掉）\n     * */\n    @Bean\n    public SaTokenDao saTokenDaoInit(@Inject(\"${sa-token.dao.redis}\") SaTokenDaoForRedisx saTokenDao) {\n        return saTokenDao;\n    }\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/SaSso3ClientApp.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport org.noear.solon.Solon;\nimport org.noear.solon.annotation.SolonMain;\n\n@SolonMain\npublic class SaSso3ClientApp {\n\n\tpublic static void main(String[] args) {\n\t\tSolon.start(SaSso3ClientApp.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Solon Sa-Token SSO 模式三 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9003\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9003\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9003\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改hosts文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/H5Controller.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.sso.model.SaCheckTicketResult;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Controller;\nimport org.noear.solon.annotation.Mapping;\n\n/**\n * 前后台分离架构下集成SSO所需的代码 （SSO-Client端）\n * <p>（注：如果不需要前后端分离架构下集成SSO，可删除此包下所有代码）</p>\n * @author click33\n *\n */\n@Controller\npublic class H5Controller {\n\n\t// 判断当前是否登录\n\t@Mapping(\"/sso/isLogin\")\n\tpublic Object isLogin() {\n\t\treturn SaResult.data(StpUtil.isLogin()).set(\"loginId\", StpUtil.getLoginIdDefaultNull());\n\t}\n\n\t// 返回SSO认证中心登录地址\n\t@Mapping(\"/sso/getSsoAuthUrl\")\n\tpublic SaResult getSsoAuthUrl(String clientLoginUrl) {\n\t\tString serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, \"\");\n\t\treturn SaResult.data(serverAuthUrl);\n\t}\n\n\t// 根据 ticket 进行登录\n\t@Mapping(\"/sso/doLoginByTicket\")\n\tpublic SaResult doLoginByTicket(String ticket) {\n\t\tSaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);\n\t\tStpUtil.login(ctr.loginId, new SaLoginParameter()\n\t\t\t\t.setTimeout(ctr.remainTokenTimeout)\n\t\t\t\t.setDeviceId(ctr.deviceId)\n\t\t);\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/h5/SaTokenConfigure.java",
    "content": "package com.pj.h5;\n\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 （解决跨域问题）\n *\n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\n    /**\n     * CORS 跨域处理策略\n     */\n    @Bean\n    public SaCorsHandleFunction corsHandle() {\n        return (req, res, sto) -> {\n            res.\n                    // 允许指定域访问跨域资源\n                    setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                    // 允许所有请求方式\n                    .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                    // 有效时间\n                    .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                    // 允许的header参数\n                    .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n            // 如果是预检请求，则立即返回到前端\n            SaRouter.match(SaHttpMethod.OPTIONS)\n                    .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                    .back();\n        };\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/GlobalExceptionFilter.java",
    "content": "package com.pj.sso;\n\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.Component;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@Component\npublic class GlobalExceptionFilter implements Filter {\n\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\ttry {\n\t\t\tchain.doFilter(ctx);\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t\tctx.render(SaResult.error(e.getMessage()));\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/java/com/pj/sso/SsoClientController.java",
    "content": "package com.pj.sso;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoClientUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.noear.solon.annotation.*;\nimport org.noear.solon.boot.web.MimeType;\n\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@Controller\n@Configuration\npublic class SsoClientController {\n\n\t// SSO-Client端：首页\n\t@Produces(MimeType.TEXT_HTML_VALUE)\n\t@Mapping(\"/\")\n\tpublic String index() {\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>\" +\n\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\"<p> \" +\n\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - \" +\n\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> - \" +\n\t\t\t\t\"<a href='/sso/myInfo' target='_blank'>账号资料</a>\" +\n\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\n\t/*\n\t * SSO-Client端：处理所有SSO相关请求\n\t * \t\thttp://{host}:{port}/sso/login\t\t\t-- Client 端登录地址\n\t * \t\thttp://{host}:{port}/sso/logout\t\t\t-- Client 端注销地址（isSlo=true时打开）\n\t * \t\thttp://{host}:{port}/sso/pushC\t\t\t-- Client 端接收消息推送地址\n\t */\n\t@Mapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数\n\t@Bean\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t}\n\n\t// 当前应用独自注销 (不退出其它应用)\n\t@Mapping(\"/sso/logoutByAlone\")\n\tpublic Object logoutByAlone() {\n\t\tStpUtil.logout();\n\t\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n\t}\n\n\t// 查询我的账号信息：sso-client 前端 -> sso-center 后端 -> sso-server 后端\n\t@Mapping(\"/sso/myInfo\")\n\tpublic Object myInfo() {\n\t\t// 如果尚未登录\n\t\tif( ! StpUtil.isLogin()) {\n\t\t\treturn \"尚未登录，无法获取\";\n\t\t}\n\n\t\t// 获取本地 loginId\n\t\tObject loginId = StpUtil.getLoginId();\n\n\t\t// 推送消息\n\t\tSaSsoMessage message = new SaSsoMessage();\n\t\tmessage.setType(\"userinfo\");\n\t\tmessage.set(\"loginId\", loginId);\n\t\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n\t\t// 返回给前端\n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-sso-for-solon/sa-token-demo-sso3-client-solon/src/main/resources/app.yml",
    "content": "# 端口\nserver:\n    port: 9003\n\n# sa-token配置 \nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # sso-client 相关配置\n    sso-client:\n        # 应用标识\n        client: sso-client3\n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）\n        # auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html\n        # 使用 Http 请求校验 ticket (模式三)\n        is-http: true\n        # API 接口调用秘钥\n        secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\n# 配置 Sa-Token Dao（此处与SSO-Server端连接不同的Redis）\nsa-token.dao: #名字可以随意取\n    redis:\n        server: \"localhost:6379\"\n#        password: 123456\n        db: 4\n        maxTotal: 200\n\n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-test</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!--<version>2.3.0.RELEASE</version>-->\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t\t<java.run.main.class>com.pj.SaTokenApplication</java.run.main.class>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t<artifactId>hutool-all</artifactId>\n\t\t\t<version>5.8.36</version>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 整合  Redis (使用jdk默认序列化方式) -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency> -->\n\n\t\t<!-- Sa-Token 整合 RedisTemplate -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- 提供Redis连接池 -->\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency>\n\n\t\t<!-- Sa-Token API 参数签名 -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-sign</artifactId>\n\t\t\t<version>${sa-token.version}</version>\n\t\t</dependency>\n\n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n\n    </dependencies>\n\n\t<!-- 构建配置 -->\n\t<build>\n\t\t<!-- 配置资源目录  -->\n\t\t<resources>\n\t\t\t<resource>\n\t\t\t\t<directory>src/main/java</directory>\n\t\t\t\t<includes>\n\t\t\t\t\t<include>**/*.xml</include>\n\t\t\t\t</includes>\n\t\t\t</resource>\n\t\t\t<resource>\n\t\t\t\t<directory>src/main/resources</directory>\n\t\t\t\t<includes>\n\t\t\t\t\t<include>**/*.*</include>\n\t\t\t\t</includes>\n\t\t\t</resource>\n\t\t</resources>\n\t\t<plugins>\n\t\t\t<!-- 打包jar文件时，配置manifest文件，加入lib包的jar依赖 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-jar-plugin</artifactId>\n\t\t\t\t<configuration>\n\t\t\t\t\t<archive>\n\t\t\t\t\t\t<manifest>\n\t\t\t\t\t\t\t<addClasspath>true</addClasspath>\n\t\t\t\t\t\t\t<classpathPrefix>lib/</classpathPrefix>\n\t\t\t\t\t\t\t<mainClass>${java.run.main.class}</mainClass>\n\t\t\t\t\t\t</manifest>\n\t\t\t\t\t</archive>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t\t<!-- 拷贝依赖的jar包到lib目录 -->\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-dependency-plugin</artifactId>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<id>copy</id>\n\t\t\t\t\t\t<phase>package</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>copy-dependencies</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t\t<configuration>\n\t\t\t\t\t\t\t<outputDirectory>\n\t\t\t\t\t\t\t\t${project.build.directory}/lib\n\t\t\t\t\t\t\t</outputDirectory>\n\t\t\t\t\t\t</configuration>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/SaTokenApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n\n/**\n * Sa-Token 测试  \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/GlobalException.java",
    "content": "package com.pj.current;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport com.pj.util.AjaxJson;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace();\n\n\t\t// 不同异常返回不同状态码 \n\t\tAjaxJson aj = null;\n\t\tif (e instanceof NotLoginException) {\t// 如果是未登录异常\n\t\t\tNotLoginException ee = (NotLoginException) e;\n\t\t\taj = AjaxJson.getNotLogin().setMsg(ee.getMessage());\n\t\t} \n\t\telse if(e instanceof NotRoleException) {\t\t// 如果是角色异常\n\t\t\tNotRoleException ee = (NotRoleException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此角色：\" + ee.getRole());\n\t\t} \n\t\telse if(e instanceof NotPermissionException) {\t// 如果是权限异常\n\t\t\tNotPermissionException ee = (NotPermissionException) e;\n\t\t\taj = AjaxJson.getNotJur(\"无此权限：\" + ee.getPermission());\n\t\t} \n\t\telse if(e instanceof DisableServiceException) {\t// 如果是被封禁异常\n\t\t\tDisableServiceException ee = (DisableServiceException) e;\n\t\t\taj = AjaxJson.getNotJur(\"当前账号 \" + ee.getService() + \" 服务已被封禁 (level=\" + ee.getLevel() + \")：\" + ee.getDisableTime() + \"秒后解封\");\n\t\t} \n\t\telse {\t// 普通异常, 输出：500 + 异常信息 \n\t\t\taj = AjaxJson.getError(e.getMessage());\n\t\t}\n\t\t\n\t\t// 返回给前端\n\t\treturn aj;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/NotFoundHandle.java",
    "content": "package com.pj.current;\n\nimport java.io.IOException;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.boot.web.servlet.error.ErrorController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 处理 404  \n * @author click33 \n */\n@RestController\npublic class NotFoundHandle implements ErrorController {\n\n\t@RequestMapping(\"/error\")\n    public Object error(HttpServletRequest request, HttpServletResponse response) throws IOException {\n\t\tresponse.setStatus(200);\n        return SaResult.get(404, \"not found\", null);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysRole.java",
    "content": "package com.pj.model;\n\n/**\n * Role 实体类\n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysRole {\n//\n//\tpublic SysRole() {\n//\t}\n//\n//\tpublic SysRole(long id, String name) {\n//\t\tsuper();\n//\t\tthis.id = id;\n//\t\tthis.name = name;\n//\t}\n//\n//\n//\t/**\n//\t * 角色id\n//\t */\n//\tprivate long id;\n//\n//\t/**\n//\t * 角色名称\n//\t */\n//\tprivate String name;\n//\n//\t/**\n//\t * @return id\n//\t */\n//\tpublic long getId() {\n//\t\treturn id;\n//\t}\n//\n//\t/**\n//\t * @param id 要设置的 id\n//\t */\n//\tpublic void setId(long id) {\n//\t\tthis.id = id;\n//\t}\n//\n//\t/**\n//\t * @return name\n//\t */\n//\tpublic String getName() {\n//\t\treturn name;\n//\t}\n//\n//\t/**\n//\t * @param name 要设置的 name\n//\t */\n//\tpublic void setName(String name) {\n//\t\tthis.name = name;\n//\t}\n//\n//\t@Override\n//\tpublic String toString() {\n//\t\treturn \"SysRole [id=\" + id + \", name=\" + name + \"]\";\n//\t}\n//\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/model/SysUser.java",
    "content": "package com.pj.model;\n\n/**\n * User 实体类 \n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysUser {\n\n\tpublic SysUser() {\n\t}\n\t\n\tpublic SysUser(long id, String name, int age) {\n\t\tsuper();\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.age = age;\n\t}\n\t\n\n\t/**\n\t * 用户id\n\t */\n\tprivate long id;\n\t\n\t/**\n\t * 用户名称\n\t */\n\tprivate String name;\n\t\n\t/**\n\t * 用户年龄\n\t */\n\tprivate int age;\n\n\t/**\n\t * 用户角色\n\t */\n\tprivate SysRole role;\n\n\t/**\n\t * @return id\n\t */\n\tpublic long getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id 要设置的 id\n\t */\n\tpublic void setId(long id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return name\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 要设置的 name\n\t */\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return age\n\t */\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\t/**\n\t * @param age 要设置的 age\n\t */\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\tpublic SysRole getRole() {\n\t\treturn role;\n\t}\n\n\tpublic SysUser setRole(SysRole role) {\n\t\tthis.role = role;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SysUser{\" +\n\t\t\t\t\"id=\" + id +\n\t\t\t\t\", name='\" + name + '\\'' +\n\t\t\t\t\", age=\" + age +\n\t\t\t\t\", role=\" + role +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaLogForSlf4j.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.log.SaLog;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * 将 Sa-Token log 信息转接到 Slf4j \n * \n * @author click33\n * @since 2022-11-2\n */\n//@Component\npublic class SaLogForSlf4j implements SaLog {\n\n\tLogger log = LoggerFactory.getLogger(SaLogForSlf4j.class);\n\t\n\t@Override\n\tpublic void trace(String str, Object... args) {\n\t\tlog.trace(str, args);\n\t}\n\n\t@Override\n\tpublic void debug(String str, Object... args) {\n\t\tlog.debug(str, args);\n\t}\n\n\t@Override\n\tpublic void info(String str, Object... args) {\n\t\tlog.info(str, args);\n\t}\n\n\t@Override\n\tpublic void warn(String str, Object... args) {\n\t\tlog.warn(str, args);\n\t}\n\n\t@Override\n\tpublic void error(String str, Object... args) {\n\t\tlog.error(str, args);\n\t}\n\n\t@Override\n\tpublic void fatal(String str, Object... args) {\n\t\tlog.error(str, args);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.plugin.SaTokenPluginHolder;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n\n/**\n * [Sa-Token 权限认证] 配置类\n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\n\t/**\n\t * 注册 Sa-Token 拦截器打开注解鉴权功能\n\t */\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器打开注解鉴权功能\n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n\n\t/**\n     * 注册 [Sa-Token 全局过滤器]\n     */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n\n        \t\t// 指定 [拦截路由] 与 [放行路由]\n        \t\t.addInclude(\"/**\")// .addExclude(\"/favicon.ico\")\n\n        \t\t// 认证函数: 每次请求执行\n        \t\t.setAuth(obj -> {\n        \t\t\t// 输出 API 请求日志，方便调试代码\n        \t\t\t// SaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n\n        \t\t})\n\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数\n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n\n        \t\t// 前置函数：在每次认证函数之前执行\n        \t\t.setBeforeAuth(obj -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称\n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以\n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面\n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探\n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n\t\t\t\t\t;\n        \t\t})\n        \t\t;\n    }\n\n\t/**\n\t * CORS 跨域处理\n\t */\n\t@Bean\n\tpublic SaCorsHandleFunction corsHandle() {\n\t\treturn (req, res, sto) -> {\n\t\t\tres.\n\t\t\t\t\t// 允许指定域访问跨域资源\n\t\t\t\t\tsetHeader(\"Access-Control-Allow-Origin\", \"*\")\n\t\t\t\t\t// 允许所有请求方式\n\t\t\t\t\t.setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n\t\t\t\t\t// 有效时间\n\t\t\t\t\t.setHeader(\"Access-Control-Max-Age\", \"3600\")\n\t\t\t\t\t// 允许的header参数\n\t\t\t\t\t.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n\t\t\t// 如果是预检请求，则立即返回到前端\n\t\t\tSaRouter.match(SaHttpMethod.OPTIONS)\n\t\t\t\t\t.free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n\t\t\t\t\t.back();\n\t\t};\n\t}\n\n\t/**\n\t * 注册插件\n\t */\n\t@Bean\n\tpublic SaTokenPluginHolder getSaTokenPluginHolder() {\n\t\tSystem.out.println(\"自定义插件安装钩子函数...\");\n\n\t\treturn SaTokenPluginHolder.instance\n//\t\t\t\t.onBeforeInstall(SaTokenPluginForJackson.class, plugin -> {\n//\t\t\t\t\tSystem.out.println(\"SaTokenPluginForJackson 插件安装前置钩子...\");\n//\t\t\t\t})\n//\n//\t\t\t\t.onAfterInstall(SaTokenPluginForJackson.class, plugin -> {\n//\t\t\t\t\tSystem.out.println(\"SaTokenPluginForJackson 插件安装后置钩子...\");\n//\t\t\t\t})\n//\n//\t\t\t\t.onAfterInstall(SaTokenPluginForJackson.class, plugin -> {\n//\t\t\t\t\tSystem.out.println(\"SaTokenPluginForJackson 插件安装后置钩子2...\");\n//\t\t\t\t})\n\n//\t\t\t\t.onInstall(SaTokenPluginForJackson.class, plugin -> {\n//\t\t\t\t\tSystem.out.println(\"注册 install 钩子函数后，插件的默认安装行为将不再执行 ...\");\n//\t\t\t\t})\n\n\t\t\t\t;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport org.springframework.stereotype.Component;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/satoken/StpUserUtil.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaTwoParamFunction;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\n\nimport java.util.List;\n\n/**\n * 【User账号体系】Sa-Token 权限认证工具类\n *\n * @author click33\n * @since 1.0.0\n */\npublic class StpUserUtil {\n\n\tprivate StpUserUtil() {}\n\n\t/**\n\t * 多账号体系下的类型标识\n\t */\n\tpublic static final String TYPE = \"user\";\n\n\t/**\n\t * 底层使用的 StpLogic 对象\n\t */\n\tpublic static StpLogic stpLogic = new StpLogic(TYPE);\n\n\t/**\n\t * 获取当前 StpLogic 的账号类型\n\t *\n\t * @return /\n\t */\n\tpublic static String getLoginType(){\n\t\treturn stpLogic.getLoginType();\n\t}\n\n\t/**\n\t * 安全的重置 StpLogic 对象\n\t *\n\t * <br> 1、更改此账户的 StpLogic 对象\n\t * <br> 2、put 到全局 StpLogic 集合中\n\t * <br> 3、发送日志\n\t *\n\t * @param newStpLogic /\n\t */\n\tpublic static void setStpLogic(StpLogic newStpLogic) {\n\t\t// 1、重置此账户的 StpLogic 对象\n\t\tstpLogic = newStpLogic;\n\n\t\t// 2、添加到全局 StpLogic 集合中\n\t\t//    以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic\n\t\tSaManager.putStpLogic(newStpLogic);\n\n\t\t// 3、$$ 发布事件：更新了 stpLogic 对象\n\t\tSaTokenEventCenter.doSetStpLogic(stpLogic);\n\t}\n\n\t/**\n\t * 获取 StpLogic 对象\n\t *\n\t * @return /\n\t */\n\tpublic static StpLogic getStpLogic() {\n\t\treturn stpLogic;\n\t}\n\n\n\t// ------------------- 获取 token 相关 -------------------\n\n\t/**\n\t * 返回 token 名称，此名称在以下地方体现：Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀\n\t *\n\t * @return /\n\t */\n\tpublic static String getTokenName() {\n\t\treturn stpLogic.getTokenName();\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t */\n\tpublic static void setTokenValue(String tokenValue){\n\t\tstpLogic.setTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param cookieTimeout Cookie存活时间(秒)\n\t */\n\tpublic static void setTokenValue(String tokenValue, int cookieTimeout){\n\t\tstpLogic.setTokenValue(tokenValue, cookieTimeout);\n\t}\n\n\t/**\n\t * 在当前会话写入指定 token 值\n\t *\n\t * @param tokenValue token 值\n\t * @param loginParameter 登录参数\n\t */\n\tpublic static void setTokenValue(String tokenValue, SaLoginParameter loginParameter){\n\t\tstpLogic.setTokenValue(tokenValue, loginParameter);\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值\n\t *\n\t * @return 当前tokenValue\n\t */\n\tpublic static String getTokenValue() {\n\t\treturn stpLogic.getTokenValue();\n\t}\n\n\t/**\n\t * 获取当前请求的 token 值 （不裁剪前缀）\n\t *\n\t * @return /\n\t */\n\tpublic static String getTokenValueNotCut(){\n\t\treturn stpLogic.getTokenValueNotCut();\n\t}\n\n\t/**\n\t * 获取当前会话的 token 参数信息\n\t *\n\t * @return token 参数信息\n\t */\n\tpublic static SaTokenInfo getTokenInfo() {\n\t\treturn stpLogic.getTokenInfo();\n\t}\n\n\n\t// ------------------- 登录相关操作 -------------------\n\n\t// --- 登录\n\n\t/**\n\t * 会话登录\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t */\n\tpublic static void login(Object id) {\n\t\tstpLogic.login(id);\n\t}\n\n\t/**\n\t * 会话登录，并指定登录设备类型\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param deviceType 设备类型\n\t */\n\tpublic static void login(Object id, String deviceType) {\n\t\tstpLogic.login(id, deviceType);\n\t}\n\n\t/**\n\t * 会话登录，并指定是否 [记住我]\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param isLastingCookie 是否为持久Cookie，值为 true 时记住我，值为 false 时关闭浏览器需要重新登录\n\t */\n\tpublic static void login(Object id, boolean isLastingCookie) {\n\t\tstpLogic.login(id, isLastingCookie);\n\t}\n\n\t/**\n\t * 会话登录，并指定此次登录 token 的有效期, 单位:秒\n\t *\n\t * @param id      账号id，建议的类型：（long | int | String）\n\t * @param timeout 此次登录 token 的有效期, 单位:秒\n\t */\n\tpublic static void login(Object id, long timeout) {\n\t\tstpLogic.login(id, timeout);\n\t}\n\n\t/**\n\t * 会话登录，并指定所有登录参数 Model\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model\n\t */\n\tpublic static void login(Object id, SaLoginParameter loginParameter) {\n\t\tstpLogic.login(id, loginParameter);\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic static String createLoginSession(Object id) {\n\t\treturn stpLogic.createLoginSession(id);\n\t}\n\n\t/**\n\t * 创建指定账号 id 的登录会话数据\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model\n\t * @return 返回会话令牌\n\t */\n\tpublic static String createLoginSession(Object id, SaLoginParameter loginParameter) {\n\t\treturn stpLogic.createLoginSession(id, loginParameter);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的登录会话数据，如果获取不到则创建并返回\n\t *\n\t * @param id 账号id，建议的类型：（long | int | String）\n\t * @return 返回会话令牌\n\t */\n\tpublic static String getOrCreateLoginSession(Object id) {\n\t\treturn stpLogic.getOrCreateLoginSession(id);\n\t}\n\n\t// --- 注销 (根据 token)\n\n\t/**\n\t * 在当前客户端会话注销\n\t */\n\tpublic static void logout() {\n\t\tstpLogic.logout();\n\t}\n\n\t/**\n\t * 在当前客户端会话注销，根据注销参数\n\t */\n\tpublic static void logout(SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logout(logoutParameter);\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void logoutByTokenValue(String tokenValue) {\n\t\tstpLogic.logoutByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 注销下线，根据指定 token、注销参数\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic static void logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logoutByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void kickoutByTokenValue(String tokenValue) {\n\t\tstpLogic.kickoutByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 踢人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void kickoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.kickoutByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t */\n\tpublic static void replacedByTokenValue(String tokenValue) {\n\t\tstpLogic.replacedByTokenValue(tokenValue);\n\t}\n\n\t/**\n\t * 顶人下线，根据指定 token、注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter /\n\t */\n\tpublic static void replacedByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.replacedByTokenValue(tokenValue, logoutParameter);\n\t}\n\n\t// --- 注销 (根据 loginId)\n\n\t/**\n\t * 会话注销，根据账号id\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void logout(Object loginId) {\n\t\tstpLogic.logout(loginId);\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 设备类型\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表注销该账号的所有设备类型)\n\t */\n\tpublic static void logout(Object loginId, String deviceType) {\n\t\tstpLogic.logout(loginId, deviceType);\n\t}\n\n\t/**\n\t * 会话注销，根据账号id 和 注销参数\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void logout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.logout(loginId, logoutParameter);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void kickout(Object loginId) {\n\t\tstpLogic.kickout(loginId);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 (填 null 代表踢出该账号的所有设备类型)\n\t */\n\tpublic static void kickout(Object loginId, String deviceType) {\n\t\tstpLogic.kickout(loginId, deviceType);\n\t}\n\n\t/**\n\t * 踢人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-5 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void kickout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.kickout(loginId, logoutParameter);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void replaced(Object loginId) {\n\t\tstpLogic.replaced(loginId);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 设备类型\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型 （填 null 代表顶替该账号的所有设备类型）\n\t */\n\tpublic static void replaced(Object loginId, String deviceType) {\n\t\tstpLogic.replaced(loginId, deviceType);\n\t}\n\n\t/**\n\t * 顶人下线，根据账号id 和 注销参数\n\t * <p> 当对方再次访问系统时，会抛出 NotLoginException 异常，场景值=-4 </p>\n\t *\n\t * @param loginId 账号id\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic static void replaced(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tstpLogic.replaced(loginId, logoutParameter);\n\t}\n\n\t// --- 注销 (会话管理辅助方法)\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (注销下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByLogout(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByLogout(session, terminal);\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (踢人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByKickout(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByKickout(session, terminal);\n\t}\n\n\t/**\n\t * 在 Account-Session 上移除 Terminal 信息 (顶人下线方式)\n\t * @param session /\n\t * @param terminal /\n\t */\n\tpublic static void removeTerminalByReplaced(SaSession session, SaTerminalInfo terminal) {\n\t\tstpLogic.removeTerminalByReplaced(session, terminal);\n\t}\n\n\n\t// 会话查询\n\n\t/**\n\t * 判断当前会话是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic static boolean isLogin() {\n\t\treturn stpLogic.isLogin();\n\t}\n\n\t/**\n\t * 判断指定账号是否已经登录\n\t *\n\t * @return 已登录返回 true，未登录返回 false\n\t */\n\tpublic static boolean isLogin(Object loginId) {\n\t\treturn stpLogic.isLogin(loginId);\n\t}\n\n\t/**\n\t * 检验当前会话是否已经登录，如未登录，则抛出异常\n\t */\n\tpublic static void checkLogin() {\n\t\tstpLogic.checkLogin();\n\t}\n\n\t/**\n\t * 获取当前会话账号id，如果未登录，则抛出异常\n\t *\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginId() {\n\t\treturn stpLogic.getLoginId();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回默认值\n\t *\n\t * @param <T> 返回类型\n\t * @param defaultValue 默认值\n\t * @return 登录id\n\t */\n\tpublic static <T> T getLoginId(T defaultValue) {\n\t\treturn stpLogic.getLoginId(defaultValue);\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 如果未登录，则返回null\n\t *\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginIdDefaultNull() {\n\t\treturn stpLogic.getLoginIdDefaultNull();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 String 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static String getLoginIdAsString() {\n\t\treturn stpLogic.getLoginIdAsString();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 int 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static int getLoginIdAsInt() {\n\t\treturn stpLogic.getLoginIdAsInt();\n\t}\n\n\t/**\n\t * 获取当前会话账号id, 并转换为 long 类型\n\t *\n\t * @return 账号id\n\t */\n\tpublic static long getLoginIdAsLong() {\n\t\treturn stpLogic.getLoginIdAsLong();\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶、被冻结等状态，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n\tpublic static Object getLoginIdByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginIdByToken(tokenValue);\n\t}\n\n\t/**\n\t * 获取指定 token 对应的账号id，如果 token 无效或 token 处于被踢、被顶等状态 (不考虑被冻结)，则返回 null\n\t *\n\t * @param tokenValue token\n\t * @return 账号id\n\t */\n\tpublic Object getLoginIdByTokenNotThinkFreeze(String tokenValue) {\n\t\treturn stpLogic.getLoginIdByTokenNotThinkFreeze(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param key 键值\n\t * @return 对应的扩展数据\n\t */\n\tpublic static Object getExtra(String key) {\n\t\treturn stpLogic.getExtra(key);\n\t}\n\n\t/**\n\t * 获取指定 Token 的扩展信息（此函数只在jwt模式下生效）\n\t *\n\t * @param tokenValue 指定的 Token 值\n\t * @param key 键值\n\t * @return 对应的扩展数据\n\t */\n\tpublic static Object getExtra(String tokenValue, String key) {\n\t\treturn stpLogic.getExtra(tokenValue, key);\n\t}\n\n\n\t// ------------------- Account-Session 相关 -------------------\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @param isCreate 是否新建\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionByLoginId(Object loginId, boolean isCreate) {\n\t\treturn stpLogic.getSessionByLoginId(loginId, isCreate);\n\t}\n\n\t/**\n\t * 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建，则返回 null\n\t *\n\t * @param sessionId SessionId\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSessionBySessionId(String sessionId) {\n\t\treturn stpLogic.getSessionBySessionId(sessionId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param loginId 账号id\n\t * @return SaSession 对象\n\t */\n\tpublic static SaSession getSessionByLoginId(Object loginId) {\n\t\treturn stpLogic.getSessionByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session, 如果该 SaSession 尚未创建，isCreate=是否新建并返回\n\t *\n\t * @param isCreate 是否新建\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSession(boolean isCreate) {\n\t\treturn stpLogic.getSession(isCreate);\n\t}\n\n\t/**\n\t * 获取当前已登录账号的 Account-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic static SaSession getSession() {\n\t\treturn stpLogic.getSession();\n\t}\n\n\n\t// ------------------- Token-Session 相关 -------------------\n\n\t/**\n\t * 获取指定 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @param tokenValue Token值\n\t * @return Session对象\n\t */\n\tpublic static SaSession getTokenSessionByToken(String tokenValue) {\n\t\treturn stpLogic.getTokenSessionByToken(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session，如果该 SaSession 尚未创建，则新建并返回\n\t *\n\t * @return Session对象\n\t */\n\tpublic static SaSession getTokenSession() {\n\t\treturn stpLogic.getTokenSession();\n\t}\n\n\t/**\n\t * 获取当前匿名 Token-Session （可在未登录情况下使用的Token-Session）\n\t *\n\t * @return Token-Session 对象\n\t */\n\tpublic static SaSession getAnonTokenSession() {\n\t\treturn stpLogic.getAnonTokenSession();\n\t}\n\n\n\t// ------------------- Active-Timeout token 最低活跃度 验证相关 -------------------\n\n\t/**\n\t * 续签当前 token：(将 [最后操作时间] 更新为当前时间戳)\n\t * <h2>\n\t * \t\t请注意: 即使 token 已被冻结 也可续签成功，\n\t * \t\t如果此场景下需要提示续签失败，可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可\n\t * </h2>\n\t */\n\tpublic static void updateLastActiveToNow() {\n\t\tstpLogic.updateLastActiveToNow();\n\t}\n\n\t/**\n\t * 检查当前 token 是否已被冻结，如果是则抛出异常\n\t */\n\tpublic static void checkActiveTimeout() {\n\t\tstpLogic.checkActiveTimeout();\n\t}\n\n\n\t// ------------------- 过期时间相关 -------------------\n\n\t/**\n\t * 获取当前会话 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getTokenTimeout() {\n\t\treturn stpLogic.getTokenTimeout();\n\t}\n\n\t/**\n\t * 获取指定 token 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @param token 指定token\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getTokenTimeout(String token) {\n\t\treturn stpLogic.getTokenTimeout(token);\n\t}\n\n\t/**\n\t * 获取当前登录账号的 Account-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getSessionTimeout() {\n\t\treturn stpLogic.getSessionTimeout();\n\t}\n\n\t/**\n\t * 获取当前 token 的 Token-Session 剩余有效时间（单位: 秒，返回 -1 代表永久有效，-2 代表没有这个值）\n\t *\n\t * @return token剩余有效时间\n\t */\n\tpublic static long getTokenSessionTimeout() {\n\t\treturn stpLogic.getTokenSessionTimeout();\n\t}\n\n\t/**\n\t * 获取当前 token 剩余活跃有效期：当前 token 距离被冻结还剩多少时间（单位: 秒，返回 -1 代表永不冻结，-2 代表没有这个值或 token 已被冻结了）\n\t *\n\t * @return /\n\t */\n\tpublic static long getTokenActiveTimeout() {\n\t\treturn stpLogic.getTokenActiveTimeout();\n\t}\n\n\t/**\n\t * 对当前 token 的 timeout 值进行续期\n\t *\n\t * @param timeout 要修改成为的有效时间 (单位: 秒)\n\t */\n\tpublic static void renewTimeout(long timeout) {\n\t\tstpLogic.renewTimeout(timeout);\n\t}\n\n\t/**\n\t * 对指定 token 的 timeout 值进行续期\n\t *\n\t * @param tokenValue 指定 token\n\t * @param timeout 要修改成为的有效时间 (单位: 秒，填 -1 代表要续为永久有效)\n\t */\n\tpublic static void renewTimeout(String tokenValue, long timeout) {\n\t\tstpLogic.renewTimeout(tokenValue, timeout);\n\t}\n\n\n\t// ------------------- 角色认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的角色集合\n\t *\n\t * @return /\n\t */\n\tpublic static List<String> getRoleList() {\n\t\treturn stpLogic.getRoleList();\n\t}\n\n\t/**\n\t * 获取：指定账号的角色集合\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic static List<String> getRoleList(Object loginId) {\n\t\treturn stpLogic.getRoleList(loginId);\n\t}\n\n\t/**\n\t * 判断：当前账号是否拥有指定角色, 返回 true 或 false\n\t *\n\t * @param role 角色\n\t * @return /\n\t */\n\tpublic static boolean hasRole(String role) {\n\t\treturn stpLogic.hasRole(role);\n\t}\n\n\t/**\n\t * 判断：指定账号是否含有指定角色标识, 返回 true 或 false\n\t *\n\t * @param loginId 账号id\n\t * @param role 角色标识\n\t * @return 是否含有指定角色标识\n\t */\n\tpublic static boolean hasRole(Object loginId, String role) {\n\t\treturn stpLogic.hasRole(loginId, role);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n\tpublic static boolean hasRoleAnd(String... roleArray){\n\t\treturn stpLogic.hasRoleAnd(roleArray);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t * @return true或false\n\t */\n\tpublic static boolean hasRoleOr(String... roleArray){\n\t\treturn stpLogic.hasRoleOr(roleArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识, 如果验证未通过，则抛出异常: NotRoleException\n\t *\n\t * @param role 角色标识\n\t */\n\tpublic static void checkRole(String role) {\n\t\tstpLogic.checkRole(role);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n\tpublic static void checkRoleAnd(String... roleArray){\n\t\tstpLogic.checkRoleAnd(roleArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定角色标识 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param roleArray 角色标识数组\n\t */\n\tpublic static void checkRoleOr(String... roleArray){\n\t\tstpLogic.checkRoleOr(roleArray);\n\t}\n\n\n\t// ------------------- 权限认证操作 -------------------\n\n\t/**\n\t * 获取：当前账号的权限码集合\n\t *\n\t * @return /\n\t */\n\tpublic static List<String> getPermissionList() {\n\t\treturn stpLogic.getPermissionList();\n\t}\n\n\t/**\n\t * 获取：指定账号的权限码集合\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic static List<String> getPermissionList(Object loginId) {\n\t\treturn stpLogic.getPermissionList(loginId);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic static boolean hasPermission(String permission) {\n\t\treturn stpLogic.hasPermission(permission);\n\t}\n\n\t/**\n\t * 判断：指定账号 id 是否含有指定权限, 返回 true 或 false\n\t *\n\t * @param loginId 账号 id\n\t * @param permission 权限码\n\t * @return 是否含有指定权限\n\t */\n\tpublic static boolean hasPermission(Object loginId, String permission) {\n\t\treturn stpLogic.hasPermission(loginId, permission);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，必须全部具有 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n\tpublic static boolean hasPermissionAnd(String... permissionArray){\n\t\treturn stpLogic.hasPermissionAnd(permissionArray);\n\t}\n\n\t/**\n\t * 判断：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t * @return true 或 false\n\t */\n\tpublic static boolean hasPermissionOr(String... permissionArray){\n\t\treturn stpLogic.hasPermissionOr(permissionArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限, 如果验证未通过，则抛出异常: NotPermissionException\n\t *\n\t * @param permission 权限码\n\t */\n\tpublic static void checkPermission(String permission) {\n\t\tstpLogic.checkPermission(permission);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，必须全部验证通过 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic static void checkPermissionAnd(String... permissionArray) {\n\t\tstpLogic.checkPermissionAnd(permissionArray);\n\t}\n\n\t/**\n\t * 校验：当前账号是否含有指定权限 [ 指定多个，只要其一验证通过即可 ]\n\t *\n\t * @param permissionArray 权限码数组\n\t */\n\tpublic static void checkPermissionOr(String... permissionArray) {\n\t\tstpLogic.checkPermissionOr(permissionArray);\n\t}\n\n\n\t// ------------------- id 反查 token 相关操作 -------------------\n\n\t/**\n\t * 获取指定账号 id 的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @return token值\n\t */\n\tpublic static String getTokenValueByLoginId(Object loginId) {\n\t\treturn stpLogic.getTokenValueByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token\n\t * <p>\n\t * \t\t在配置为允许并发登录时，此方法只会返回队列的最后一个 token，\n\t * \t\t如果你需要返回此账号 id 的所有 token，请调用 getTokenValueListByLoginId\n\t * </p>\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return token值\n\t */\n\tpublic static String getTokenValueByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTokenValueByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有相关 token\n\t */\n\tpublic static List<String> getTokenValueListByLoginId(Object loginId) {\n\t\treturn stpLogic.getTokenValueListByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的 token 集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic static List<String> getTokenValueListByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTokenValueListByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @return 此 loginId 的所有登录 token\n\t */\n\tpublic static List<SaTerminalInfo> getTerminalListByLoginId(Object loginId) {\n\t\treturn stpLogic.getTerminalListByLoginId(loginId);\n\t}\n\n\t/**\n\t * 获取指定账号 id 指定设备类型端的已登录设备信息集合\n\t *\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型，填 null 代表不限设备类型\n\t * @return /\n\t */\n\tpublic static List<SaTerminalInfo> getTerminalListByLoginId(Object loginId, String deviceType) {\n\t\treturn stpLogic.getTerminalListByLoginId(loginId, deviceType);\n\t}\n\n\t/**\n\t * 获取指定账号 id 已登录设备信息集合，执行特定函数\n\t *\n\t * @param loginId 账号id\n\t * @param function 需要执行的函数\n\t */\n\tpublic static void forEachTerminalList(Object loginId, SaTwoParamFunction<SaSession, SaTerminalInfo> function) {\n\t\tstpLogic.forEachTerminalList(loginId, function);\n\t}\n\n\t/**\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic static String getLoginDeviceType() {\n\t\treturn stpLogic.getLoginDeviceType();\n\t}\n\n\t/**\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\tpublic static String getLoginDeviceTypeByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginDeviceTypeByToken(tokenValue);\n\t}\n\n\t/**\n\t * 获取当前 token 的最后活跃时间（13位时间戳），如果不存在则返回 -2\n\t *\n\t * @return /\n\t */\n\tpublic static long getTokenLastActiveTime() {\n\t\treturn stpLogic.getTokenLastActiveTime();\n\t}\n\n\t/**\n\t * 判断对于指定 loginId 来讲，指定设备 id 是否为可信任设备\n\t * @param deviceId /\n\t * @return /\n\t */\n\tpublic static boolean isTrustDeviceId(Object userId, String deviceId) {\n\t\treturn stpLogic.isTrustDeviceId(userId, deviceId);\n\t}\n\n\n\n\t// ------------------- 会话管理 -------------------\n\n\t/**\n\t * 根据条件查询缓存中所有的 token\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return token集合\n\t */\n\tpublic static List<String> searchTokenValue(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchTokenValue(keyword, start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 SessionId\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量  (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic static List<String> searchSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchSessionId(keyword, start, size, sortType);\n\t}\n\n\t/**\n\t * 根据条件查询缓存中所有的 Token-Session-Id\n\t *\n\t * @param keyword 关键字\n\t * @param start 开始处索引\n\t * @param size 获取数量 (-1代表一直获取到末尾)\n\t * @param sortType 排序类型（true=正序，false=反序）\n\t *\n\t * @return sessionId集合\n\t */\n\tpublic static List<String> searchTokenSessionId(String keyword, int start, int size, boolean sortType) {\n\t\treturn stpLogic.searchTokenSessionId(keyword, start, size, sortType);\n\t}\n\n\n\t// ------------------- 账号封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号\n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disable(Object loginId, long time) {\n\t\tstpLogic.disable(loginId, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁 (true=已被封禁, false=未被封禁)\n\t *\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic static boolean isDisable(Object loginId) {\n\t\treturn stpLogic.isDisable(loginId);\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void checkDisable(Object loginId) {\n\t\tstpLogic.checkDisable(loginId);\n\t}\n\n\t/**\n\t * 获取：指定账号剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic static long getDisableTime(Object loginId) {\n\t\treturn stpLogic.getDisableTime(loginId);\n\t}\n\n\t/**\n\t * 解封：指定账号\n\t *\n\t * @param loginId 账号id\n\t */\n\tpublic static void untieDisable(Object loginId) {\n\t\tstpLogic.untieDisable(loginId);\n\t}\n\n\n\t// ------------------- 分类封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号的指定服务\n\t * <p> 此方法不会直接将此账号id踢下线，如需封禁后立即掉线，请追加调用 StpUtil.logout(id)\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定服务\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disable(Object loginId, String service, long time) {\n\t\tstpLogic.disable(loginId, service, time);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务 是否已被封禁（true=已被封禁, false=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务\n\t * @return /\n\t */\n\tpublic static boolean isDisable(Object loginId, String service) {\n\t\treturn stpLogic.isDisable(loginId, service);\n\t}\n\n\t/**\n\t * 校验：指定账号 指定服务 是否已被封禁，如果被封禁则抛出异常\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个\n\t */\n\tpublic static void checkDisable(Object loginId, String... services) {\n\t\tstpLogic.checkDisable(loginId, services);\n\t}\n\n\t/**\n\t * 获取：指定账号 指定服务 剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\n\t *\n\t * @param loginId 账号id\n\t * @param service 指定服务\n\t * @return see note\n\t */\n\tpublic static long getDisableTime(Object loginId, String service) {\n\t\treturn stpLogic.getDisableTime(loginId, service);\n\t}\n\n\t/**\n\t * 解封：指定账号、指定服务\n\t *\n\t * @param loginId 账号id\n\t * @param services 指定服务，可以指定多个\n\t */\n\tpublic static void untieDisable(Object loginId, String... services) {\n\t\tstpLogic.untieDisable(loginId, services);\n\t}\n\n\n\t// ------------------- 阶梯封禁 -------------------\n\n\t/**\n\t * 封禁：指定账号，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 指定封禁等级\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disableLevel(Object loginId, int level, long time) {\n\t\tstpLogic.disableLevel(loginId, level, time);\n\t}\n\n\t/**\n\t * 封禁：指定账号的指定服务，并指定封禁等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 指定封禁等级\n\t * @param time 封禁时间, 单位: 秒 （-1=永久封禁）\n\t */\n\tpublic static void disableLevel(Object loginId, String service, int level, long time) {\n\t\tstpLogic.disableLevel(loginId, service, level, time);\n\t}\n\n\t/**\n\t * 判断：指定账号是否已被封禁到指定等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 指定封禁等级\n\t * @return /\n\t */\n\tpublic static boolean isDisableLevel(Object loginId, int level) {\n\t\treturn stpLogic.isDisableLevel(loginId, level);\n\t}\n\n\t/**\n\t * 判断：指定账号的指定服务，是否已被封禁到指定等级\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 指定封禁等级\n\t * @return /\n\t */\n\tpublic static boolean isDisableLevel(Object loginId, String service, int level) {\n\t\treturn stpLogic.isDisableLevel(loginId, service, level);\n\t}\n\n\t/**\n\t * 校验：指定账号是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id\n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic static void checkDisableLevel(Object loginId, int level) {\n\t\tstpLogic.checkDisableLevel(loginId, level);\n\t}\n\n\t/**\n\t * 校验：指定账号的指定服务，是否已被封禁到指定等级（如果已经达到，则抛出异常）\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @param level 封禁等级 （只有 封禁等级 ≥ 此值 才会抛出异常）\n\t */\n\tpublic static void checkDisableLevel(Object loginId, String service, int level) {\n\t\tstpLogic.checkDisableLevel(loginId, service, level);\n\t}\n\n\t/**\n\t * 获取：指定账号被封禁的等级，如果未被封禁则返回-2\n\t *\n\t * @param loginId 指定账号id\n\t * @return /\n\t */\n\tpublic static int getDisableLevel(Object loginId) {\n\t\treturn stpLogic.getDisableLevel(loginId);\n\t}\n\n\t/**\n\t * 获取：指定账号的 指定服务 被封禁的等级，如果未被封禁则返回-2\n\t *\n\t * @param loginId 指定账号id\n\t * @param service 指定封禁服务\n\t * @return /\n\t */\n\tpublic static int getDisableLevel(Object loginId, String service) {\n\t\treturn stpLogic.getDisableLevel(loginId, service);\n\t}\n\n\n\t// ------------------- 临时身份切换 -------------------\n\n\t/**\n\t * 临时切换身份为指定账号id\n\t *\n\t * @param loginId 指定loginId\n\t */\n\tpublic static void switchTo(Object loginId) {\n\t\tstpLogic.switchTo(loginId);\n\t}\n\n\t/**\n\t * 结束临时切换身份\n\t */\n\tpublic static void endSwitch() {\n\t\tstpLogic.endSwitch();\n\t}\n\n\t/**\n\t * 判断当前请求是否正处于 [ 身份临时切换 ] 中\n\t *\n\t * @return /\n\t */\n\tpublic static boolean isSwitch() {\n\t\treturn stpLogic.isSwitch();\n\t}\n\n\t/**\n\t * 在一个 lambda 代码段里，临时切换身份为指定账号id，lambda 结束后自动恢复\n\t *\n\t * @param loginId 指定账号id\n\t * @param function 要执行的方法\n\t */\n\tpublic static void switchTo(Object loginId, SaFunction function) {\n\t\tstpLogic.switchTo(loginId, function);\n\t}\n\n\n\t// ------------------- 二级认证 -------------------\n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param safeTime 维持时间 (单位: 秒)\n\t */\n\tpublic static void openSafe(long safeTime) {\n\t\tstpLogic.openSafe(safeTime);\n\t}\n\n\t/**\n\t * 在当前会话 开启二级认证\n\t *\n\t * @param service 业务标识\n\t * @param safeTime 维持时间 (单位: 秒)\n\t */\n\tpublic static void openSafe(String service, long safeTime) {\n\t\tstpLogic.openSafe(service, safeTime);\n\t}\n\n\t/**\n\t * 判断：当前会话是否处于二级认证时间内\n\t *\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic static boolean isSafe() {\n\t\treturn stpLogic.isSafe();\n\t}\n\n\t/**\n\t * 判断：当前会话 是否处于指定业务的二级认证时间内\n\t *\n\t * @param service 业务标识\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic static boolean isSafe(String service) {\n\t\treturn stpLogic.isSafe(service);\n\t}\n\n\t/**\n\t * 判断：指定 token 是否处于二级认证时间内\n\t *\n\t * @param tokenValue Token 值\n\t * @param service 业务标识\n\t * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时\n\t */\n\tpublic static boolean isSafe(String tokenValue, String service) {\n\t\treturn stpLogic.isSafe(tokenValue, service);\n\t}\n\n\t/**\n\t * 校验：当前会话是否已通过二级认证，如未通过则抛出异常\n\t */\n\tpublic static void checkSafe() {\n\t\tstpLogic.checkSafe();\n\t}\n\n\t/**\n\t * 校验：检查当前会话是否已通过指定业务的二级认证，如未通过则抛出异常\n\t *\n\t * @param service 业务标识\n\t */\n\tpublic static void checkSafe(String service) {\n\t\tstpLogic.checkSafe(service);\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @return 剩余有效时间\n\t */\n\tpublic static long getSafeTime() {\n\t\treturn stpLogic.getSafeTime();\n\t}\n\n\t/**\n\t * 获取：当前会话的二级认证剩余有效时间（单位: 秒, 返回-2代表尚未通过二级认证）\n\t *\n\t * @param service 业务标识\n\t * @return 剩余有效时间\n\t */\n\tpublic static long getSafeTime(String service) {\n\t\treturn stpLogic.getSafeTime(service);\n\t}\n\n\t/**\n\t * 在当前会话 结束二级认证\n\t */\n\tpublic static void closeSafe() {\n\t\tstpLogic.closeSafe();\n\t}\n\n\t/**\n\t * 在当前会话 结束指定业务标识的二级认证\n\t *\n\t * @param service 业务标识\n\t */\n\tpublic static void closeSafe(String service) {\n\t\tstpLogic.closeSafe(service);\n\t}\n\n\n\t// ------------------- Bean 对象、字段代理 -------------------\n\n\t/**\n\t * 根据当前配置对象创建一个 SaLoginParameter 对象\n\t *\n\t * @return /\n\t */\n\tpublic static SaLoginParameter createSaLoginParameter() {\n\t\treturn stpLogic.createSaLoginParameter();\n\t}\n\n\n\t// ------------------- 过期方法 -------------------\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceType </h2>\n\t * 返回当前会话的登录设备类型\n\t *\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic static String getLoginDevice() {\n\t\treturn stpLogic.getLoginDevice();\n\t}\n\n\t/**\n\t * <h2>请更换为 getLoginDeviceTypeByToken </h2>\n\t * 返回指定 token 会话的登录设备类型\n\t *\n\t * @param tokenValue 指定token\n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Deprecated\n\tpublic static String getLoginDeviceByToken(String tokenValue) {\n\t\treturn stpLogic.getLoginDeviceByToken(tokenValue);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/AtController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpBasic;\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaCheckPermission;\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.annotation.SaCheckSafe;\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 注解鉴权测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class AtController {\n\n\t// 登录认证，登录之后才可以进入方法  ---- http://localhost:8081/at/checkLogin \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 权限认证，具备user-add权限才可以进入方法  ---- http://localhost:8081/at/checkPermission \n\t@SaCheckPermission(\"user-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，同时具备所有权限才可以进入  ---- http://localhost:8081/at/checkPermissionAnd \n\t@SaCheckPermission({\"user-add\", \"user-delete\", \"user-update\"})\n\t@RequestMapping(\"checkPermissionAnd\")\n\tpublic SaResult checkPermissionAnd() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限认证，只要具备其中一个就可以进入  ---- http://localhost:8081/at/checkPermissionOr \n\t@SaCheckPermission(value = {\"user-add\", \"user-delete\", \"user-update\"}, mode = SaMode.OR)\n\t@RequestMapping(\"checkPermissionOr\")\n\tpublic SaResult checkPermissionOr() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色认证，只有具备admin角色才可以进入  ---- http://localhost:8081/at/checkRole \n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 完成二级认证  ---- http://localhost:8081/at/openSafe \n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(200); // 打开二级认证，有效期为200秒\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 通过二级认证后才可以进入  ---- http://localhost:8081/at/checkSafe \n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 通过Basic认证后才可以进入  ---- http://localhost:8081/at/checkBasic \n\t@SaCheckHttpBasic(account = \"sa:123456\")\n\t@RequestMapping(\"checkBasic\")\n\tpublic SaResult checkBasic() {\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\tStpUtil.getTokenSession();\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 校验登录  ---- http://localhost:8081/acc/checkLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\tStpUtil.checkLogin();\n\t\treturn SaResult.ok();\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\n\t// 查询账号登录设备信息  ---- http://localhost:8081/acc/terminalInfo\n\t@RequestMapping(\"terminalInfo\")\n\tpublic SaResult terminalInfo() {\n\t\tSystem.out.println(\"账号 10001 登录设备信息：\");\n\t\tList<SaTerminalInfo> terminalList = StpUtil.getTerminalListByLoginId(10001);\n\t\tfor (SaTerminalInfo ter : terminalList) {\n\t\t\tSystem.out.println(\"登录index=\" + ter.getIndex() + \", 设备type=\" + ter.getDeviceType() + \", token=\" + ter.getTokenValue() + \", 登录time=\" + ter.getCreateTime());\n\t\t}\n\t\treturn SaResult.data(terminalList);\n\t}\n\n\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.login(10001, SaLoginParameter.create().setIsConcurrent(false));\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/StressTestController.java",
    "content": "package com.pj.test;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.pj.util.Ttime;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 压力测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/s-test/\")\npublic class StressTestController {\n\n\t// 测试   浏览器访问： http://localhost:8081/s-test/login \n\t// 测试前，请先将 is-read-cookie 配置为 false\n\t@RequestMapping(\"login\")\n\tpublic SaResult login() {\n//\t\t\tStpUtil.getTokenSession().logout();\n//\t\t\tStpUtil.logoutByLoginId(10001);\n\n\t\tint count = 10;\t// 循环多少轮 \n\t\tint loginCount = 10000;\t// 每轮循环多少次  \n\t\t\n\t\t// 循环10次 取平均时间 \n\t\tList<Double> list = new ArrayList<>();\n\t\tfor (int i = 1; i <= count; i++) {\n\t\t\tSystem.out.println(\"\\n---------------------第\" + i + \"轮---------------------\");\n\t\t\tTtime t = new Ttime().start();\n\t\t\t// 每次登录的次数\n\t\t\tfor (int j = 1; j <= loginCount; j++) {\n\t\t\t\tStpUtil.login(\"1000\" + j, \"PC-\" + j);\n\t\t\t\tif(j % 1000 == 0) {\n\t\t\t\t\tSystem.out.println(\"已登录：\" + j);\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.end();\n\t\t\tlist.add((t.returnMs() + 0.0) / 1000);\n\t\t\tSystem.out.println(\"第\" + i + \"轮\" + \"用时：\" + t.toString());\n\t\t}\n//\t\t\tSystem.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());\n\t\t\n\t\tSystem.out.println(\"\\n---------------------测试结果---------------------\");\n\t\tSystem.out.println(list.size() + \"次测试: \" + list);\n\t\tdouble ss = 0;\n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tss += list.get(i);\n\t\t}\n\t\tSystem.out.println(\"平均用时: \" + ss / list.size());\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/Test2Controller.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\npublic class Test2Controller {\n\n\t// 测试登录  ---- http://localhost:8081/test\n\t@RequestMapping(\"/test\")\n\tpublic SaResult test2() {\n\n\t\tSystem.out.println(SpringMVCUtil.getRequest());\n\t\tSystem.out.println(SaTokenContextServletUtil.getRequest());\n\n//\t\tStpUtil.login(30003);\n//\t\tSystem.out.println(StpUtil.getSession().timeout());\n//\t\tSystem.out.println(StpUtil.getStpLogic().getTokenSession(false));\n\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.annotation.SaCheckHttpDigest;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t// 测试登录  ---- http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue = \"10001\") long id, String dt) {\n\t\tStpUtil.login(id, new SaLoginParameter()\n\t\t\t\t.setIsConcurrent(true)\n\t\t\t\t.setIsShare(false)\n//\t\t\t\t\t\t.setDeviceType(dt)\n\t\t\t\t.setMaxLoginCount(4)\n\t\t\t\t.setMaxTryTimes(12)\n\t\t\t\t.setTerminalExtra(\"deviceSimpleTitle\", \"XiaoMi 15 Ultra\")\n\t\t\t\t.setTerminalExtra(\"loginAddress\", \"浙江省杭州市西湖区\")\n\t\t\t\t.setTerminalExtra(\"loginIp\", \"127.0.0.1\")\n\t\t\t\t.setTerminalExtra(\"loginTime\", SaFoxUtil.formatDate(new Date()))\n\t\t);\n\t\tStpUtil.getTokenSession();\n\t\treturn SaResult.ok(\"登录成功\");\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了 \" + SaFoxUtil.formatDate(new Date()));\n\n\n\t\t// 获取所有已登录的会话id\n\t\tList<String> sessionIdList = StpUtil.searchSessionId(null, 0, -1, false);\n\n\t\tfor (String sessionId : sessionIdList) {\n\n\t\t\t// 根据会话id，查询对应的 SaSession 对象，此处一个 SaSession 对象即代表一个登录的账号\n\t\t\tSaSession session = StpUtil.getSessionBySessionId(sessionId);\n\n\t\t\t// 查询这个账号都在哪些设备登录了，依据上面的示例，账号A 的 SaTerminalInfo 数量是 3，账号B 的 SaTerminalInfo 数量是 2\n\t\t\tList<SaTerminalInfo> terminalList = session.terminalListCopy();\n\t\t\tSystem.out.println(\"会话id：\" + sessionId + \"，共在 \" + terminalList.size() + \" 设备登录\");\n\t\t}\n\n\n\t\t// 返回\n\t\treturn SaResult.data(null);\n\t}\n\t\n\t// 测试   浏览器访问： http://localhost:8081/test/test2\n\t@RequestMapping(\"test2\")\n\tpublic SaResult test2() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8081/test/getRequestPath\n\t@RequestMapping(\"getRequestPath\")\n\tpublic SaResult getRequestPath() {\n\t\tSystem.out.println(\"------------ 测试访问路径获取 \");\n\t\tSystem.out.println(\"SpringMVCUtil.getRequest().getRequestURI()  \" + SpringMVCUtil.getRequest().getRequestURI());\n\t\tSystem.out.println(\"SaHolder.getRequest().getRequestPath()  \" + SaHolder.getRequest().getRequestPath());\n\t\treturn SaResult.ok();\n\t}\n\n\t// 测试 Http Digest 认证   浏览器访问： http://localhost:8081/test/testDigest\n\t@SaCheckHttpDigest(\"sa:123456\")\n\t@RequestMapping(\"testDigest\")\n\tpublic SaResult testDigest() {\n\t\t// SaHttpDigestUtil.check(\"sa\", \"123456\");\n\t\t// 返回\n\t\treturn SaResult.data(null);\n\t}\n\n\t// 测试注销   浏览器访问： http://localhost:8081/test/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.data(null);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/AjaxJson.java",
    "content": "package com.pj.util;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n\n/**\n * ajax请求返回Json格式数据的封装 \n */\npublic class AjaxJson implements Serializable{\n\n\tprivate static final long serialVersionUID = 1L;\t// 序列化版本号\n\t\n\tpublic static final int CODE_SUCCESS = 200;\t\t\t// 成功状态码\n\tpublic static final int CODE_ERROR = 500;\t\t\t// 错误状态码\n\tpublic static final int CODE_WARNING = 501;\t\t\t// 警告状态码\n\tpublic static final int CODE_NOT_JUR = 403;\t\t\t// 无权限状态码\n\tpublic static final int CODE_NOT_LOGIN = 401;\t\t// 未登录状态码\n\tpublic static final int CODE_INVALID_REQUEST = 400;\t// 无效请求状态码\n\n\tpublic int code; \t// 状态码\n\tpublic String msg; \t// 描述信息 \n\tpublic Object data; // 携带对象\n\tpublic Long dataCount;\t// 数据总数，用于分页 \n\t\n\t/**\n\t * 返回code  \n\t * @return\n\t */\n\tpublic int getCode() {\n\t\treturn this.code;\n\t}\n\n\t/**\n\t * 给msg赋值，连缀风格\n\t */\n\tpublic AjaxJson setMsg(String msg) {\n\t\tthis.msg = msg;\n\t\treturn this;\n\t}\n\tpublic String getMsg() {\n\t\treturn this.msg;\n\t}\n\n\t/**\n\t * 给data赋值，连缀风格\n\t */\n\tpublic AjaxJson setData(Object data) {\n\t\tthis.data = data;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 将data还原为指定类型并返回\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getData(Class<T> cs) {\n\t\treturn (T) data;\n\t}\n\t\n\t// ============================  构建  ================================== \n\t\n\tpublic AjaxJson(int code, String msg, Object data, Long dataCount) {\n\t\tthis.code = code;\n\t\tthis.msg = msg;\n\t\tthis.data = data;\n\t\tthis.dataCount = dataCount;\n\t}\n\t\n\t// 返回成功\n\tpublic static AjaxJson getSuccess() {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, null, null);\n\t}\n\tpublic static AjaxJson getSuccess(String msg, Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, msg, data, null);\n\t}\n\tpublic static AjaxJson getSuccessData(Object data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\tpublic static AjaxJson getSuccessArray(Object... data) {\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, null);\n\t}\n\t\n\t// 返回失败\n\tpublic static AjaxJson getError() {\n\t\treturn new AjaxJson(CODE_ERROR, \"error\", null, null);\n\t}\n\tpublic static AjaxJson getError(String msg) {\n\t\treturn new AjaxJson(CODE_ERROR, msg, null, null);\n\t}\n\t\n\t// 返回警告 \n\tpublic static AjaxJson getWarning() {\n\t\treturn new AjaxJson(CODE_ERROR, \"warning\", null, null);\n\t}\n\tpublic static AjaxJson getWarning(String msg) {\n\t\treturn new AjaxJson(CODE_WARNING, msg, null, null);\n\t}\n\t\n\t// 返回未登录\n\tpublic static AjaxJson getNotLogin() {\n\t\treturn new AjaxJson(CODE_NOT_LOGIN, \"未登录，请登录后再次访问\", null, null);\n\t}\n\t\n\t// 返回没有权限的 \n\tpublic static AjaxJson getNotJur(String msg) {\n\t\treturn new AjaxJson(CODE_NOT_JUR, msg, null, null);\n\t}\n\t\n\t// 返回一个自定义状态码的\n\tpublic static AjaxJson get(int code, String msg){\n\t\treturn new AjaxJson(code, msg, null, null);\n\t}\n\t\n\t// 返回分页和数据的\n\tpublic static AjaxJson getPageData(Long dataCount, Object data){\n\t\treturn new AjaxJson(CODE_SUCCESS, \"ok\", data, dataCount);\n\t}\n\t\n\t// 返回，根据受影响行数的(大于0=ok，小于0=error)\n\tpublic static AjaxJson getByLine(int line){\n\t\tif(line > 0){\n\t\t\treturn getSuccess(\"ok\", line);\n\t\t}\n\t\treturn getError(\"error\").setData(line); \n\t}\n\n\t// 返回，根据布尔值来确定最终结果的  (true=ok，false=error)\n\tpublic static AjaxJson getByBoolean(boolean b){\n\t\treturn b ? getSuccess(\"ok\") : getError(\"error\"); \n\t}\n\t\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#toString()\n\t */\n\t@SuppressWarnings(\"rawtypes\")\n\t@Override\n\tpublic String toString() {\n\t\tString data_string = null;\n\t\tif(data == null){\n\t\t\t\n\t\t} else if(data instanceof List){\n\t\t\tdata_string = \"List(length=\" + ((List)data).size() + \")\";\n\t\t} else {\n\t\t\tdata_string = data.toString();\n\t\t}\n\t\treturn \"{\"\n\t\t\t\t+ \"\\\"code\\\": \" + this.getCode()\n\t\t\t\t+ \", \\\"msg\\\": \\\"\" + this.getMsg() + \"\\\"\"\n\t\t\t\t+ \", \\\"data\\\": \" + data_string\n\t\t\t\t+ \", \\\"dataCount\\\": \" + dataCount\n\t\t\t\t+ \"}\";\n\t}\n\t\n\t\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/java/com/pj/util/Ttime.java",
    "content": "package com.pj.util;\n\n\n/**\n * 用于测试用时\n * @author click33\n *\n */\npublic class Ttime {\n\n\tprivate long start=0;\t//开始时间\n\tprivate long end=0;\t\t//结束时间\n\t\n\tpublic static Ttime t = new Ttime();\t//static快捷使用\n\t\n\t/**\n\t * 开始计时\n\t * @return\n\t */\n\tpublic Ttime start() {\n\t\tstart=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\t\n\t\n\t/**\n\t * 结束计时\n\t */\n\tpublic Ttime end() {\n\t\tend=System.currentTimeMillis();\n\t\treturn this;\n\t}\n\n\t\n\t/**\n\t * 返回所用毫秒数\n\t */\n\tpublic long returnMs() {\n\t\treturn end-start;\n\t}\n\t\n\t/**\n\t * 格式化输出结果\n\t */\n\tpublic void outTime() {\n\t\tSystem.out.println(this.toString());\n\t}\n\t\n\t/**\n\t * 结束并格式化输出结果\n\t */\n\tpublic void endOutTime() {\n\t\tthis.end().outTime();\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn (returnMs() + 0.0) / 1000 + \"s\";\t\t// 格式化为：0.01s\n\t}\n\t\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-test/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n        \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n\nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password:\n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-thymeleaf</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- thymeleaf 视图引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-thymeleaf</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n        <!-- 在 thymeleaf 标签中使用 Sa-Token -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-thymeleaf</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t\n\t\t<!-- 热刷新 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-devtools</artifactId>\n\t\t\t<scope>provided</scope>\n\t\t</dependency>\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/SaTokenThymeleafDemoApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n@SpringBootApplication\npublic class SaTokenThymeleafDemoApplication {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenThymeleafDemoApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功，Sa-Token 配置如下：\" + SaManager.getConfig());\n\t\tSystem.out.println(\"\\n测试访问：http://localhost:8081/\");\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\nimport org.thymeleaf.spring5.view.ThymeleafViewResolver;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.thymeleaf.dialect.SaTokenDialect;\n\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t// Sa-Token 标签方言 (Thymeleaf版)\n\t@Bean\n\tpublic SaTokenDialect getSaTokenDialect() {\n\t\treturn new SaTokenDialect();\n\t}\n\n    // 为 Thymeleaf 注入全局变量，以便在页面中调用 Sa-Token 的方法 \n    @Autowired\n    private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) {\n    \tviewResolver.addStaticVariable(\"stp\", StpUtil.stpLogic);\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.ModelAndView;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 测试 Controller\n *\n * @author click33\n */\n@RestController\npublic class TestController {\n\n\t// 首页 \n\t@RequestMapping(\"/\")\n\tpublic Object index() {\n\t\treturn new ModelAndView(\"index.html\");\n\t}\n\t\n\t// 登录 \n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tStpUtil.login(id);\n\t\tStpUtil.getSession().set(\"name\", \"zhangsan\");\n\t\treturn SaResult.ok();\n\t}\n\n\t// 注销 \n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-thymeleaf/src/main/resources/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\" xmlns:sa=\"http://www.thymeleaf.org/extras/sa-token\">\n\t<head>\n\t\t<title>Sa-Token 集成 Thymeleaf 标签方言</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\" style=\"padding: 30px;\">\n\t\t\t<h2>Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面</h2>\n\t\t\t<p>当前是否登录：<span th:text=\"${stp.isLogin()}\"></span></p>\n\t\t\t<p>\n\t\t\t\t<a href=\"login\" target=\"_blank\">登录</a>\n\t\t\t\t<a href=\"logout\" target=\"_blank\">注销</a>\n\t\t\t</p>\n\n\t\t\t<p>登录之后才能显示：<span sa:login>value</span></p>\n\t\t\t<p>不登录才能显示：<span sa:notLogin>value</span></p>\n\n\t\t\t<p>具有角色 admin 才能显示：<span sa:hasRole=\"admin\">value</span></p>\n\t\t\t<p>同时具备多个角色才能显示：<span sa:hasRoleAnd=\"admin, ceo, cto\">value</span></p>\n\t\t\t<p>只要具有其中一个角色就能显示：<span sa:hasRoleOr=\"admin, ceo, cto\">value</span></p>\n\t\t\t<p>不具有角色 admin 才能显示：<span sa:notRole=\"admin\">value</span></p>\n\n\t\t\t<p>具有权限 user-add 才能显示：<span sa:hasPermission=\"user-add\">value</span></p>\n\t\t\t<p>同时具备多个权限才能显示：<span sa:hasPermissionAnd=\"user-add, user-delete, user-get\">value</span></p>\n\t\t\t<p>只要具有其中一个权限就能显示：<span sa:hasPermissionOr=\"user-add, user-delete, user-get\">value</span></p>\n\t\t\t<p>不具有权限 user-add 才能显示：<span sa:notPermission=\"user-add\">value</span></p>\n\n\t\t\t<p th:if=\"${stp.isLogin()}\">\n\t\t\t\t从SaSession中取值：\n\t\t\t\t<span th:text=\"${stp.getSession().get('name', '')}\"></span>\n\t\t\t</p>\n\n\t\t</div>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-webflux</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.7.18</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t    <groupId>org.springframework.boot</groupId>\n\t\t    <artifactId>spring-boot-starter-webflux</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Sa-Token 权限认证（Reactor响应式集成）, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n        \n\t\t<!-- Sa-Token 整合 Redis -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency> -->\n\n\t\t<!-- 提供redis连接池 -->\n\t\t<!-- <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency> -->\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/SaTokenWebfluxApplication.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token整合webflux 示例 \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenWebfluxApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenWebfluxApplication.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/satoken/MyFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.pj.satoken;\n\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n/**\n * 自定义过滤器\n */\n@Component\npublic class MyFilter implements WebFilter {\n\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\t\tSystem.out.println(\"进入自定义过滤器\");\n\n\t\ttry {\n\t\t\t// 先 set 上下文，再调用 Sa-Token 同步 API，并在 finally 里清除上下文\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSystem.out.println(StpUtil.isLogin());\n\t\t}\n\t\tfinally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\n\t\treturn chain.filter(exchange);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.reactor.filter.SaReactorFilter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure {\n\n\t/**\n     * 注册 [sa-token全局过滤器] \n     */\n    @Bean\n    public SaReactorFilter getSaReactorFilter() {\n        return new SaReactorFilter()\n        \t\t// 指定 [拦截路由]\n        \t\t.addInclude(\"/**\")\n        \t\t// 指定 [放行路由]\n        \t\t.addExclude(\"/favicon.ico\")\n        \t\t// 指定[认证函数]: 每次请求执行 \n        \t\t.setAuth(r -> {\n        \t\t\tSystem.out.println(\"---------- sa全局认证\");\n                    // SaRouter.match(\"/test/test\", () -> StpUtil.checkLogin());\n        \t\t})\n        \t\t// 指定[异常处理函数]：每次[认证函数]发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\te.printStackTrace();\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t;\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/DefineRoutes.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\n@Configuration\npublic class DefineRoutes {\n\n\t/**\n\t * 函数式编程，初始化路由表 \n\t * @return 路由表 \n\t */\n\t@SuppressWarnings(\"deprecation\")\n\t@Bean\n\tpublic RouterFunction<ServerResponse> getRoutes() {\n\t\treturn RouterFunctions.route(RequestPredicates.GET(\"/fun\"), req -> {\n\t\t\treturn SaReactorSyncHolder.setContext(req.exchange(), () -> {\n\t\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\t\tSaResult res = SaResult.data(StpUtil.getTokenInfo());\n\t\t\t\treturn ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).syncBody(res);\n\t\t\t});\n\t\t});\t\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.reactor.context.SaReactorHolder;\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.CookieValue;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\nimport java.time.Duration;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tUserService userService;\n\n\t// 登录测试：Controller 里调用 Sa-Token API   --- http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic Mono<SaResult> login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tStpUtil.login(id);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t});\n\t}\n\n\t// API测试：手动设置上下文、try-finally 形式     \t--- http://localhost:8081/test/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin(ServerWebExchange exchange) {\n\t\ttry {\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t} finally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\t}\n\n\t// API测试：手动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin2\n\t@RequestMapping(\"isLogin2\")\n\tpublic SaResult isLogin2(ServerWebExchange exchange) {\n\t\tSaResult res = SaReactorSyncHolder.setContext(exchange, ()->{\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t});\n\t\treturn SaResult.data(res);\n\t}\n\n\t// API测试：自动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin3\n\t@RequestMapping(\"isLogin3\")\n\tpublic Mono<SaResult> isLogin3() {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\tuserService.isLogin();\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t});\n\t}\n\n\t// API测试：自动设置上下文、调用 userService Mono 方法     \t--- http://localhost:8081/test/isLogin4\n\t@RequestMapping(\"isLogin4\")\n\tpublic Mono<SaResult> isLogin4() {\n\t\treturn userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\").flatMap(userId -> {\n\t\t\treturn SaReactorHolder.sync(() -> {\n\t\t\t\tStpUtil.login(userId);\n\t\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t\t});\n\t\t});\n\t}\n\n\t// API测试：切换线程、复杂嵌套调用 \t--- http://localhost:8081/test/isLogin5\n\t@RequestMapping(\"isLogin5\")\n\tpublic Mono<SaResult> isLogin5() {\n\t\tSystem.out.println(\"线程id-----\" + Thread.currentThread().getId());\n\t\t// 要点：在流里调用 Sa-Token API 之前，必须用 SaReactorHolder.sync( () -> {} ) 进行包裹\n\t\treturn Mono.delay(Duration.ofSeconds(1))\n\t\t\t\t.doOnNext(r-> System.out.println(\"线程id-----\" + Thread.currentThread().getId()))\n\t\t\t\t.map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))\n\t\t\t\t.map(r-> userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\"))\n\t\t\t\t.map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))\n\t\t\t\t.flatMap(isLogin -> {\n\t\t\t\t\tSystem.out.println(\"是否登录 \" + isLogin);\n\t\t\t\t\treturn SaReactorHolder.sync(() -> {\n\t\t\t\t\t\tSystem.out.println(\"是否登录 \" + StpUtil.isLogin());\n\t\t\t\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t\t\t\t});\n\t\t\t\t});\n\t}\n\n\t// API测试：使用上下文无关的API \t--- http://localhost:8081/test/isLogin6\n\t@RequestMapping(\"isLogin6\")\n\tpublic SaResult isLogin6(@CookieValue(\"satoken\") String satoken) {\n\t\tSystem.out.println(\"token 为：\" + satoken);\n\t\tSystem.out.println(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n\t\treturn SaResult.ok(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n\t}\n\n\t// API测试：SaSession 写值     \t--- http://localhost:8081/test/sessionSet\n\t@RequestMapping(\"sessionSet\")\n\tpublic Mono<SaResult> sessionSet() {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tSystem.out.println(\"session name 值为：\" + StpUtil.getSession().get(\"name\"));\n\t\t\tStpUtil.getSession().set(\"name\", \"zhangsan\");\n\t\t\tSystem.out.println(\"session name 值为：\" + StpUtil.getSession().get(\"name\"));\n\t\t\treturn SaResult.data(StpUtil.getSession().get(\"name\"));\n\t\t});\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"线程id------- \" + Thread.currentThread().getId());\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/java/com/pj/test/UserService.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\n\n/**\n * 模拟 Service 方法\n * @author click33\n * @since 2025/4/6\n */\n@Service\npublic class UserService {\n\n    public boolean isLogin() {\n        System.out.println(\"UserService 里调用 API 测试，是否登录：\" + StpUtil.isLogin());\n        return StpUtil.isLogin();\n    }\n\n    public Mono<Long> findUserIdByNamePwd(String name, String pwd) {\n        // ...\n        return Mono.just(10001L);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n        \nspring:\n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间（毫秒）\n        timeout: 10000ms\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-webflux-springboot3</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>3.0.1</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t    <groupId>org.springframework.boot</groupId>\n\t\t    <artifactId>spring-boot-starter-webflux</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证（Reactor响应式集成）, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 Redis -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency> -->\n\n\t\t<!-- 提供redis连接池 -->\n\t\t<!-- <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency> -->\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/SaTokenWebfluxSpringboot3Application.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token整合webflux 示例 (springboot3)\n * \n * @author click33\n * @since 2023年1月3日\n *\n */\n@SpringBootApplication\npublic class SaTokenWebfluxSpringboot3Application {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenWebfluxSpringboot3Application.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.pj.satoken;\n\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n/**\n * 自定义过滤器\n */\n@Component\npublic class MyFilter implements WebFilter {\n\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\t\tSystem.out.println(\"进入自定义过滤器\");\n\n\t\ttry {\n\t\t\t// 先 set 上下文，再调用 Sa-Token 同步 API，并在 finally 里清除上下文\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSystem.out.println(StpUtil.isLogin());\n\t\t}\n\t\tfinally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\n\t\treturn chain.filter(exchange);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.reactor.filter.SaReactorFilter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure {\n\n\t/**\n     * 注册 [sa-token全局过滤器] \n     */\n    @Bean\n    public SaReactorFilter getSaReactorFilter() {\n        return new SaReactorFilter()\n        \t\t// 指定 [拦截路由]\n        \t\t.addInclude(\"/**\")\n        \t\t// 指定 [放行路由]\n        \t\t.addExclude(\"/favicon.ico\")\n        \t\t// 指定[认证函数]: 每次请求执行 \n        \t\t.setAuth(r -> {\n        \t\t\tSystem.out.println(\"---------- sa全局认证\");\n                    // SaRouter.match(\"/test/test\", () -> StpUtil.checkLogin());\n        \t\t})\n        \t\t// 指定[异常处理函数]：每次[认证函数]发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t;\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/DefineRoutes.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\n@Configuration\npublic class DefineRoutes {\n\n\t/**\n\t * 函数式编程，初始化路由表 \n\t * @return 路由表 \n\t */\n\t@SuppressWarnings(\"deprecation\")\n\t@Bean\n\tpublic RouterFunction<ServerResponse> getRoutes() {\n\t\treturn RouterFunctions.route(RequestPredicates.GET(\"/fun\"), req -> {\n\t\t\treturn SaReactorSyncHolder.setContext(req.exchange(), () -> {\n\t\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\t\tSaResult res = SaResult.data(StpUtil.getTokenInfo());\n\t\t\t\treturn ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).syncBody(res);\n\t\t\t});\n\t\t});\t\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.reactor.context.SaReactorHolder;\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.CookieValue;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\nimport java.time.Duration;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tUserService userService;\n\n\t// 登录测试：Controller 里调用 Sa-Token API   --- http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic Mono<SaResult> login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tStpUtil.login(id);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t});\n\t}\n\n\t// API测试：手动设置上下文、try-finally 形式     \t--- http://localhost:8081/test/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin(ServerWebExchange exchange) {\n\t\ttry {\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t} finally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\t}\n\n\t// API测试：手动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin2\n\t@RequestMapping(\"isLogin2\")\n\tpublic SaResult isLogin2(ServerWebExchange exchange) {\n\t\tSaResult res = SaReactorSyncHolder.setContext(exchange, ()->{\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t});\n\t\treturn SaResult.data(res);\n\t}\n\n\t// API测试：自动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin3\n\t@RequestMapping(\"isLogin3\")\n\tpublic Mono<SaResult> isLogin3() {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\tuserService.isLogin();\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t});\n\t}\n\n\t// API测试：自动设置上下文、调用 userService Mono 方法     \t--- http://localhost:8081/test/isLogin4\n\t@RequestMapping(\"isLogin4\")\n\tpublic Mono<SaResult> isLogin4() {\n\t\treturn userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\").flatMap(userId -> {\n\t\t\treturn SaReactorHolder.sync(() -> {\n\t\t\t\tStpUtil.login(userId);\n\t\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t\t});\n\t\t});\n\t}\n\n\t// API测试：切换线程、复杂嵌套调用 \t--- http://localhost:8081/test/isLogin5\n\t@RequestMapping(\"isLogin5\")\n\tpublic Mono<SaResult> isLogin5() {\n\t\tSystem.out.println(\"线程id-----\" + Thread.currentThread().getId());\n\t\t// 要点：在流里调用 Sa-Token API 之前，必须用 SaReactorHolder.sync( () -> {} ) 进行包裹\n\t\treturn Mono.delay(Duration.ofSeconds(1))\n\t\t\t\t.doOnNext(r-> System.out.println(\"线程id-----\" + Thread.currentThread().getId()))\n\t\t\t\t.map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))\n\t\t\t\t.map(r-> userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\"))\n\t\t\t\t.map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))\n\t\t\t\t.flatMap(isLogin -> {\n\t\t\t\t\tSystem.out.println(\"是否登录 \" + isLogin);\n\t\t\t\t\treturn SaReactorHolder.sync(() -> {\n\t\t\t\t\t\tSystem.out.println(\"是否登录 \" + StpUtil.isLogin());\n\t\t\t\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t\t\t\t});\n\t\t\t\t});\n\t}\n\n\t// API测试：使用上下文无关的API \t--- http://localhost:8081/test/isLogin6\n\t@RequestMapping(\"isLogin6\")\n\tpublic SaResult isLogin6(@CookieValue(\"satoken\") String satoken) {\n\t\tSystem.out.println(\"token 为：\" + satoken);\n\t\tSystem.out.println(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n\t\treturn SaResult.ok(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n\t}\n\n\t// API测试：SaSession 写值     \t--- http://localhost:8081/test/sessionSet\n\t@RequestMapping(\"sessionSet\")\n\tpublic Mono<SaResult> sessionSet() {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tSystem.out.println(\"session name 值为：\" + StpUtil.getSession().get(\"name\"));\n\t\t\tStpUtil.getSession().set(\"name\", \"zhangsan\");\n\t\t\tSystem.out.println(\"session name 值为：\" + StpUtil.getSession().get(\"name\"));\n\t\t\treturn SaResult.data(StpUtil.getSession().get(\"name\"));\n\t\t});\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"线程id------- \" + Thread.currentThread().getId());\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/UserService.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\n\n/**\n * 模拟 Service 方法\n * @author click33\n * @since 2025/4/6\n */\n@Service\npublic class UserService {\n\n    public boolean isLogin() {\n        System.out.println(\"UserService 里调用 API 测试，是否登录：\" + StpUtil.isLogin());\n        return StpUtil.isLogin();\n    }\n\n    public Mono<Long> findUserIdByNamePwd(String name, String pwd) {\n        // ...\n        return Mono.just(10001L);\n    }\n\n}"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot3/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n        \nspring:\n    data:\n        # redis配置\n        redis:\n            # Redis数据库索引（默认为0）\n            database: 0\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # 连接超时时间（毫秒）\n            timeout: 10000ms\n            lettuce:\n                pool:\n                    # 连接池最大连接数\n                    max-active: 200\n                    # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                    max-wait: -1ms\n                    # 连接池中的最大空闲连接\n                    max-idle: 10\n                    # 连接池中的最小空闲连接\n                    min-idle: 0\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-webflux-springboot4</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot 4 -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>4.0.3</version>\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- springboot依赖 -->\n\t\t<dependency>\n\t\t    <groupId>org.springframework.boot</groupId>\n\t\t    <artifactId>spring-boot-starter-webflux</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证（Reactor响应式集成）, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-reactor-spring-boot4-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\n\t\t<!-- Sa-Token 整合 Redis -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency> -->\n\n\t\t<!-- 提供redis连接池 -->\n\t\t<!-- <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency> -->\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\n\t</dependencies>\n\t\n\t\n</project>\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java",
    "content": "package com.pj;\n\nimport cn.dev33.satoken.SaManager;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * Sa-Token整合webflux 示例 (springboot4)\n * \n * @author click33\n * @since 2026年2月27日\n *\n */\n@SpringBootApplication\npublic class SaTokenWebfluxSpringboot4Application {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenWebfluxSpringboot4Application.class, args);\n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.pj.satoken;\n\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n/**\n * 自定义过滤器\n */\n@Component\npublic class MyFilter implements WebFilter {\n\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\t\tSystem.out.println(\"进入自定义过滤器\");\n\n\t\ttry {\n\t\t\t// 先 set 上下文，再调用 Sa-Token 同步 API，并在 finally 里清除上下文\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSystem.out.println(StpUtil.isLogin());\n\t\t}\n\t\tfinally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\n\t\treturn chain.filter(exchange);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java",
    "content": "package com.pj.satoken;\n\nimport cn.dev33.satoken.reactor.filter.SaReactorFilter;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n *\n */\n@Configuration\npublic class SaTokenConfigure {\n\n\t/**\n     * 注册 [sa-token全局过滤器] \n     */\n    @Bean\n    public SaReactorFilter getSaReactorFilter() {\n        return new SaReactorFilter()\n        \t\t// 指定 [拦截路由]\n        \t\t.addInclude(\"/**\")\n        \t\t// 指定 [放行路由]\n        \t\t.addExclude(\"/favicon.ico\")\n        \t\t// 指定[认证函数]: 每次请求执行 \n        \t\t.setAuth(r -> {\n        \t\t\tSystem.out.println(\"---------- sa全局认证\");\n                    // SaRouter.match(\"/test/test\", () -> StpUtil.checkLogin());\n        \t\t})\n        \t\t// 指定[异常处理函数]：每次[认证函数]发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t;\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.satoken;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n */\n@Component\t// 打开此注解，保证此类被springboot扫描，即可完成sa-token的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user-add\");\n\t\tlist.add(\"user-delete\");\n\t\tlist.add(\"user-update\");\n\t\tlist.add(\"user-get\");\n\t\tlist.add(\"article-get\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本list仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.RequestPredicates;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\n@Configuration\npublic class DefineRoutes {\n\n\t/**\n\t * 函数式编程，初始化路由表 \n\t * @return 路由表 \n\t */\n\t@Bean\n\tpublic RouterFunction<ServerResponse> getRoutes() {\n\t\treturn RouterFunctions.route(RequestPredicates.GET(\"/fun\"), req -> {\n\t\t\treturn SaReactorSyncHolder.setContext(req.exchange(), () -> {\n\t\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\t\tSaResult res = SaResult.data(StpUtil.getTokenInfo());\n\t\t\t\treturn ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(res);\n\t\t\t});\n\t\t});\t\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n/**\n * 全局异常处理 \n */\n@RestControllerAdvice\npublic class GlobalException {\n\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace();\n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.reactor.context.SaReactorHolder;\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.CookieValue;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\nimport java.time.Duration;\n\n/**\n * 测试专用Controller \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tUserService userService;\n\n\t// 登录测试：Controller 里调用 Sa-Token API   --- http://localhost:8081/test/login\n\t@RequestMapping(\"login\")\n\tpublic Mono<SaResult> login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tStpUtil.login(id);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t});\n\t}\n\n\t// API测试：手动设置上下文、try-finally 形式     \t--- http://localhost:8081/test/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin(ServerWebExchange exchange) {\n\t\ttry {\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t} finally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\t}\n\n\t// API测试：手动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin2\n\t@RequestMapping(\"isLogin2\")\n\tpublic SaResult isLogin2(ServerWebExchange exchange) {\n\t\tSaResult res = SaReactorSyncHolder.setContext(exchange, ()->{\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t});\n\t\treturn SaResult.data(res);\n\t}\n\n\t// API测试：自动设置上下文、lambda 表达式形式    \t--- http://localhost:8081/test/isLogin3\n\t@RequestMapping(\"isLogin3\")\n\tpublic Mono<SaResult> isLogin3() {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t\tuserService.isLogin();\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t});\n\t}\n\n\t// API测试：自动设置上下文、调用 userService Mono 方法     \t--- http://localhost:8081/test/isLogin4\n\t@RequestMapping(\"isLogin4\")\n\tpublic Mono<SaResult> isLogin4() {\n\t\treturn userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\").flatMap(userId -> {\n\t\t\treturn SaReactorHolder.sync(() -> {\n\t\t\t\tStpUtil.login(userId);\n\t\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t\t});\n\t\t});\n\t}\n\n\t// API测试：切换线程、复杂嵌套调用 \t--- http://localhost:8081/test/isLogin5\n\t@RequestMapping(\"isLogin5\")\n\tpublic Mono<SaResult> isLogin5() {\n\t\tSystem.out.println(\"线程id-----\" + Thread.currentThread().getId());\n\t\t// 要点：在流里调用 Sa-Token API 之前，必须用 SaReactorHolder.sync( () -> {} ) 进行包裹\n\t\treturn Mono.delay(Duration.ofSeconds(1))\n\t\t\t\t.doOnNext(r-> System.out.println(\"线程id-----\" + Thread.currentThread().getId()))\n\t\t\t\t.map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))\n\t\t\t\t.map(r-> userService.findUserIdByNamePwd(\"ZhangSan\", \"123456\"))\n\t\t\t\t.map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))\n\t\t\t\t.flatMap(isLogin -> {\n\t\t\t\t\tSystem.out.println(\"是否登录 \" + isLogin);\n\t\t\t\t\treturn SaReactorHolder.sync(() -> {\n\t\t\t\t\t\tSystem.out.println(\"是否登录 \" + StpUtil.isLogin());\n\t\t\t\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t\t\t\t});\n\t\t\t\t});\n\t}\n\n\t// API测试：使用上下文无关的API \t--- http://localhost:8081/test/isLogin6\n\t@RequestMapping(\"isLogin6\")\n\tpublic SaResult isLogin6(@CookieValue(\"satoken\") String satoken) {\n\t\tSystem.out.println(\"token 为：\" + satoken);\n\t\tSystem.out.println(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n\t\treturn SaResult.ok(\"登录人：\" + StpUtil.getLoginIdByToken(satoken));\n\t}\n\n\t// API测试：SaSession 写值     \t--- http://localhost:8081/test/sessionSet\n\t@RequestMapping(\"sessionSet\")\n\tpublic Mono<SaResult> sessionSet() {\n\t\treturn SaReactorHolder.sync(() -> {\n\t\t\tSystem.out.println(\"session name 值为：\" + StpUtil.getSession().get(\"name\"));\n\t\t\tStpUtil.getSession().set(\"name\", \"zhangsan\");\n\t\t\tSystem.out.println(\"session name 值为：\" + StpUtil.getSession().get(\"name\"));\n\t\t\treturn SaResult.data(StpUtil.getSession().get(\"name\"));\n\t\t});\n\t}\n\n\t// 测试   浏览器访问： http://localhost:8081/test/test\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"线程id------- \" + Thread.currentThread().getId());\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.springframework.stereotype.Service;\nimport reactor.core.publisher.Mono;\n\n/**\n * 模拟 Service 方法\n * @author click33\n * @since 2025/4/6\n */\n@Service\npublic class UserService {\n\n    public boolean isLogin() {\n        System.out.println(\"UserService 里调用 API 测试，是否登录：\" + StpUtil.isLogin());\n        return StpUtil.isLogin();\n    }\n\n    public Mono<Long> findUserIdByNamePwd(String name, String pwd) {\n        // ...\n        return Mono.just(10001L);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n        \nspring:\n    data:\n        # redis配置\n        redis:\n            # Redis数据库索引（默认为0）\n            database: 0\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # 连接超时时间（毫秒）\n            timeout: 10000ms\n            lettuce:\n                pool:\n                    # 连接池最大连接数\n                    max-active: 200\n                    # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                    max-wait: -1ms\n                    # 连接池中的最大空闲连接\n                    max-idle: 10\n                    # 连接池中的最小空闲连接\n                    min-idle: 0\n\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-websocket</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- WebScoket 依赖 -->\n       \t<dependency>  \n\t\t\t<groupId>org.springframework.boot</groupId>  \n           \t<artifactId>spring-boot-starter-websocket</artifactId>  \n       \t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency> -->\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * Sa-Token 整合 WebSocket 鉴权示例 \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenWebSocketApplication {\n\n\t/*\n\t * 1、访问登录接口，拿到会话Token：\n\t * \t\thttp://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t * \n\t * 2、找一个WebSocket在线测试页面进行连接，\n\t * \t\t例如：\n\t * \t\t\thttps://www.bejson.com/httputil/websocket/\n\t * \t\t然后连接地址：\n\t * \t\t\tws://localhost:8081/ws-connect/2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5\n\t */\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenWebSocketApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\").set(\"token\", StpUtil.getTokenValue());\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java",
    "content": "package com.pj.ws;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.socket.server.standard.ServerEndpointExporter;\n\n/**\n * 开启WebSocket支持\n */\n@Configuration  \npublic class WebSocketConfig { \n\t\n\t@Bean  \n\tpublic ServerEndpointExporter serverEndpointExporter() {  \n\t\treturn new ServerEndpointExporter();  \n\t}\n\t\n} "
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java",
    "content": "package com.pj.ws;\n\nimport java.io.IOException;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport javax.websocket.OnClose;\nimport javax.websocket.OnError;\nimport javax.websocket.OnMessage;\nimport javax.websocket.OnOpen;\nimport javax.websocket.Session;\nimport javax.websocket.server.PathParam;\nimport javax.websocket.server.ServerEndpoint;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * WebSocket 连接测试 \n */\n@Component\n@ServerEndpoint(\"/ws-connect/{satoken}\")\npublic class WebSocketConnect {\n\n    /**\n     * 固定前缀 \n     */\n    private static final String USER_ID = \"user_id_\";\n\t\n\t /** \n\t  * 存放Session集合，方便推送消息 （javax.websocket.Session）  \n\t  */\n    private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();\n    \n\t// 监听：连接成功\n\t@OnOpen\n\tpublic void onOpen(Session session, @PathParam(\"satoken\") String satoken) throws IOException {\n\t\t\n\t\t// 根据 token 获取对应的 userId \n\t\tObject loginId = StpUtil.getLoginIdByToken(satoken);\n\t\tif(loginId == null) {\n\t\t\tsession.close();\n\t\t\tthrow new SaTokenException(\"连接失败，无效Token：\" + satoken);\n\t\t}\n\t\t\n\t\t// put到集合，方便后续操作 \n\t\tlong userId = SaFoxUtil.getValueByType(loginId, long.class);\n\t\tsessionMap.put(USER_ID + userId, session);\n\t\t\n\t\t// 给个提示 \n\t\tString tips = \"Web-Socket 连接成功，sid=\" + session.getId() + \"，userId=\" + userId;\n\t\tSystem.out.println(tips);\n\t\tsendMessage(session, tips);\n\t}\n\n\t// 监听: 连接关闭\n\t@OnClose\n\tpublic void onClose(Session session) {\n\t\tSystem.out.println(\"连接关闭，sid=\" + session.getId());\n\t\tfor (String key : sessionMap.keySet()) {\n\t\t\tif(sessionMap.get(key).getId().equals(session.getId())) {\n\t\t\t\tsessionMap.remove(key);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// 监听：收到客户端发送的消息 \n\t@OnMessage\n\tpublic void onMessage(Session session, String message) {\n\t\tSystem.out.println(\"sid为：\" + session.getId() + \"，发来：\" + message);\n\t}\n\t\n\t// 监听：发生异常 \n\t@OnError\n\tpublic void onError(Session session, Throwable error) {\n\t\tSystem.out.println(\"sid为：\" + session.getId() + \"，发生错误\");\n\t\terror.printStackTrace();\n\t}\n\t\n\t// ---------\n\t\n\t// 向指定客户端推送消息 \n\tpublic static void sendMessage(Session session, String message) {\n\t\ttry {\n\t\t\tSystem.out.println(\"向sid为：\" + session.getId() + \"，发送：\" + message);\n\t\t\tsession.getBasicRemote().sendText(message);\n\t\t} catch (IOException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\t\n\t// 向指定用户推送消息 \n\tpublic static void sendMessage(long userId, String message) {\n\t\tSession session = sessionMap.get(USER_ID + userId);\n\t\tif(session != null) {\n\t\t\tsendMessage(session, message);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket-spring/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-demo-websocket-spring</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t\n\t<!-- SpringBoot -->\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>2.5.14</version>\n\t\t<!-- <version>1.5.9.RELEASE</version> -->\n\t\t<relativePath/>\n\t</parent>\n\t\n\t<!-- 定义 Sa-Token 版本号 -->\n\t<properties>\n\t\t<sa-token.version>1.45.0</sa-token.version>\n\t</properties>\n\n\t<dependencies>\n\n\t\t<!-- SpringBoot依赖 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- WebScoket 依赖 -->\n       \t<dependency>  \n\t\t\t<groupId>org.springframework.boot</groupId>  \n           \t<artifactId>spring-boot-starter-websocket</artifactId>  \n       \t</dependency>\n\t\t\n\t\t<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-spring-boot-starter</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t\n\t\t<!-- Sa-Token整合 Redis (使用jackson序列化方式) -->\n\t\t<!-- <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-jackson</artifactId>\n            <version>${sa-token.version}</version>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n        </dependency> -->\n        \n\t\t<!-- @ConfigurationProperties -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n\t\n</project>"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java",
    "content": "package com.pj;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport cn.dev33.satoken.SaManager;\n\n/**\n * Sa-Token 整合 WebSocket 鉴权示例 \n * @author click33\n *\n */\n@SpringBootApplication\npublic class SaTokenWebSocketSpringApplication {\n\n\t/*\n\t * 1、访问登录接口，拿到会话Token：\n\t * \t\thttp://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t * \n\t * 2、找一个WebSocket在线测试页面进行连接，\n\t * \t\t例如：\n\t * \t\t\thttps://www.bejson.com/httputil/websocket/\n\t * \t\t然后连接地址：\n\t * \t\t\tws://localhost:8081/ws-connect?satoken=2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5\n\t */\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenWebSocketSpringApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java",
    "content": "package com.pj.test;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\").set(\"token\", StpUtil.getTokenValue());\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java",
    "content": "package com.pj.ws;\n\nimport java.io.IOException;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport org.springframework.web.socket.CloseStatus;\nimport org.springframework.web.socket.TextMessage;\nimport org.springframework.web.socket.WebSocketSession;\nimport org.springframework.web.socket.handler.TextWebSocketHandler;\n\n/**\n * 处理 WebSocket 连接 \n * \n * @author click33\n * @since 2022-2-11\n */\npublic class MyWebSocketHandler extends TextWebSocketHandler {\n\n    /**\n     * 固定前缀 \n     */\n    private static final String USER_ID = \"user_id_\";\n    \n    /**\n     * 存放Session集合，方便推送消息\n     */\n    private static ConcurrentHashMap<String, WebSocketSession> webSocketSessionMaps = new ConcurrentHashMap<>();\n\n    // 监听：连接开启 \n    @Override\n    public void afterConnectionEstablished(WebSocketSession session) throws Exception {\n\n    \t// put到集合，方便后续操作 \n        String userId = session.getAttributes().get(\"userId\").toString();\n        webSocketSessionMaps.put(USER_ID + userId, session);\n        \n\n\t\t// 给个提示 \n\t\tString tips = \"Web-Socket 连接成功，sid=\" + session.getId() + \"，userId=\" + userId;\n\t\tSystem.out.println(tips);\n\t\tsendMessage(session, tips);\n    }\n    \n    // 监听：连接关闭 \n    @Override\n    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {\n    \t// 从集合移除 \n        String userId = session.getAttributes().get(\"userId\").toString();\n        webSocketSessionMaps.remove(USER_ID + userId);\n        \n        // 给个提示 \n        String tips = \"Web-Socket 连接关闭，sid=\" + session.getId() + \"，userId=\" + userId;\n    \tSystem.out.println(tips);\n    }\n\n    // 收到消息 \n    @Override\n    public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {\n    \tSystem.out.println(\"sid为：\" + session.getId() + \"，发来：\" + message);\n    }\n\n    // ----------- \n    \n    // 向指定客户端推送消息 \n \tpublic static void sendMessage(WebSocketSession session, String message) {\n \t\ttry {\n \t\t\tSystem.out.println(\"向sid为：\" + session.getId() + \"，发送：\" + message);\n \t\t\tsession.sendMessage(new TextMessage(message));\n \t\t} catch (IOException e) {\n \t\t\tthrow new RuntimeException(e);\n \t\t}\n \t}\n \t\n \t// 向指定用户推送消息 \n \tpublic static void sendMessage(long userId, String message) {\n \t\tWebSocketSession session = webSocketSessionMaps.get(USER_ID + userId);\n\t\tif(session != null) {\n\t\t\tsendMessage(session, message);\n\t\t}\n \t}\n    \n}\n\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java",
    "content": "package com.pj.ws;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.socket.config.annotation.EnableWebSocket;\nimport org.springframework.web.socket.config.annotation.WebSocketConfigurer;\nimport org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;\n\n/**\n * WebSocket 相关配置 \n * \n * @author click33\n * @since 2022-2-11\n */\n@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\t\n\t// 注册 WebSocket 处理器 \n    @Override\n    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {\n        webSocketHandlerRegistry\n        \t\t// WebSocket 连接处理器 \n                .addHandler(new MyWebSocketHandler(), \"/ws-connect\")\n                // WebSocket 拦截器 \n                .addInterceptors(new WebSocketInterceptor())\n                // 允许跨域 \n                .setAllowedOrigins(\"*\");\n    }\n\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java",
    "content": "package com.pj.ws;\n\nimport java.util.Map;\n\nimport org.springframework.http.server.ServerHttpRequest;\nimport org.springframework.http.server.ServerHttpResponse;\nimport org.springframework.web.socket.WebSocketHandler;\nimport org.springframework.web.socket.server.HandshakeInterceptor;\n\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * WebSocket 握手的前置拦截器 \n * \n * @author click33\n * @since 2022-2-11\n */\npublic class WebSocketInterceptor implements HandshakeInterceptor {\n\n\t// 握手之前触发 (return true 才会握手成功 )\n\t@Override\n\tpublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler,\n\t\t\tMap<String, Object> attr) {\n\t\t\n\t\tSystem.out.println(\"---- 握手之前触发 \" + StpUtil.getTokenValue());\n\t\t\n\t\t// 未登录情况下拒绝握手 \n\t\tif(StpUtil.isLogin() == false) {\n\t\t\tSystem.out.println(\"---- 未授权客户端，连接失败\");\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// 标记 userId，握手成功 \n\t\tattr.put(\"userId\", StpUtil.getLoginIdAsLong());\n\t\treturn true;\n\t}\n\n\t// 握手之后触发 \n\t@Override\n\tpublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,\n\t\t\tException exception) {\n\t\tSystem.out.println(\"---- 握手之后触发 \");\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # 是否输出操作日志 \n    is-log: true\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        \n        \n        \n        "
  },
  {
    "path": "sa-token-dependencies/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n\t<parent>\n\t\t<groupId>cn.dev33</groupId>\n\t\t<artifactId>sa-token-bom</artifactId>\n\t\t<version>${revision}</version>\n\t\t<!-- 寻址 -->\n\t\t<relativePath>../sa-token-bom/pom.xml</relativePath>\n\t</parent>\n\t<packaging>pom</packaging>\n\n    <artifactId>sa-token-dependencies</artifactId>\n    <name>sa-token-dependencies</name>\n    <description>Sa-Token Dependencies</description>\n\n    <properties>\n\t\t<!-- 第三方依赖版本号 -->\n\t\t<jackson-databind.version>2.13.4.1</jackson-databind.version>\t\n\t\t<jackson-datatype-jsr310.version>2.11.2</jackson-datatype-jsr310.version>\n\t\t<jackson3-databind.version>3.1.0</jackson3-databind.version>\n\t\t<servlet-api.version>3.1.0</servlet-api.version>\n\t\t<jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>\n\t\t<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>\n\t\t<freemarker.version>2.3.34</freemarker.version>\n\t\t<solon.version>3.2.1</solon.version>\n\t\t<noear-redisx.version>1.8.2</noear-redisx.version>\n\t\t<noear-snack3.version>3.2.139</noear-snack3.version>\n        <noear-snack4.version>4.0.20</noear-snack4.version>\n\t\t<jfinal.version>4.9.17</jfinal.version>\n\t\t<jboot.version>3.14.4</jboot.version>\n\t\t<loveqq.version>1.1.5-java8</loveqq.version>\n\t\t<commons-pool2.version>2.5.0</commons-pool2.version>\n\t\t<dubbo.version>2.7.21</dubbo.version>\n\t\t<grpc-spring-boot-starter.version>2.10.1.RELEASE</grpc-spring-boot-starter.version>\n\t\t<hutool-jwt.version>5.8.36</hutool-jwt.version>\n\t\t<jjwt.version>0.12.6</jjwt.version>\n\t\t<fastjson.version>1.2.83</fastjson.version>\n\t\t<fastjson2.version>2.0.15</fastjson2.version>\n\t\t<redisson.version>3.45.0</redisson.version>\n\t\t<hutool-cache.version>5.8.36</hutool-cache.version>\n\t\t<caffeine.version>3.2.0</caffeine.version>\n\t\t<forest.version>1.6.4</forest.version>\n\t\t<okhttps.version>4.1.0</okhttps.version>\n\n\t\t<!-- Maven GPG Plugin -->\n\t\t<maven-gpg-plugin.version>3.2.7</maven-gpg-plugin.version>\n\t\t<!-- Maven Central Portal -->\n\t\t<central.publishing.maven.version>0.7.0</central.publishing.maven.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n        \n\t\t\t<!-- ****************** sa-token-starter 相关依赖 ****************** -->\n\t        <!-- Servlet API -->\n\t        <dependency>\n\t\t\t\t<groupId>javax.servlet</groupId>\n\t\t\t\t<artifactId>javax.servlet-api</artifactId>\n\t\t\t\t<version>${servlet-api.version}</version>\n\t\t\t</dependency>\n\t\t\t\n\t\t\t<!-- Jakarta Servlet API -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>jakarta.servlet</groupId>\n\t\t\t\t<artifactId>jakarta.servlet-api</artifactId>\n\t\t\t\t<version>${jakarta-servlet-api.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<!-- jackson2 databind -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t\t<artifactId>jackson-databind</artifactId>\n\t      \t  \t<version>${jackson-databind.version}</version>\t\n\t\t\t</dependency>\n\n\t\t\t<!-- jackson3 databind (tools.jackson) -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>tools.jackson.core</groupId>\n\t\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t\t\t<version>${jackson3-databind.version}</version>\n\t\t\t</dependency>\n\t\t\t\n\t\t\t<!-- solon -->\n\t        <dependency>\n\t            <groupId>org.noear</groupId>\n\t            <artifactId>solon</artifactId>\n\t            <version>${solon.version}</version>\n\t        </dependency>\n\n\t\t\t<!-- snack3 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>org.noear</groupId>\n\t\t\t\t<artifactId>snack3</artifactId>\n\t\t\t\t<version>${noear-snack3.version}</version>\n\t\t\t</dependency>\n\n            <!-- snack4 -->\n            <dependency>\n                <groupId>org.noear</groupId>\n                <artifactId>snack4</artifactId>\n                <version>${noear-snack4.version}</version>\n            </dependency>\n\t\t\t\n\t\t\t<!-- jboot -->\n\t        <dependency>\n\t            <groupId>io.jboot</groupId>\n\t            <artifactId>jboot</artifactId>\n\t            <version>${jboot.version}</version>\n\t        </dependency>\n\t        \n\t        <!-- jfinal -->\n\t        <dependency>\n\t            <groupId>com.jfinal</groupId>\n\t            <artifactId>jfinal</artifactId>\n\t            <version>${jfinal.version}</version>\n\t        </dependency>\n\n\t\t\t<!-- loveqq-core -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>com.kfyty</groupId>\n\t\t\t\t<artifactId>loveqq-core</artifactId>\n\t\t\t\t<version>${loveqq.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<!-- loveqq-mvc-core -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>com.kfyty</groupId>\n\t\t\t\t<artifactId>loveqq-mvc-core</artifactId>\n\t\t\t\t<version>${loveqq.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<!-- loveqq redisson starter -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>com.kfyty</groupId>\n\t\t\t\t<artifactId>loveqq-boot-starter-redisson</artifactId>\n\t\t\t\t<version>${loveqq.version}</version>\n\t\t\t</dependency>\n\t\t\t\n\t\t\t<!-- ****************** sa-token-plugin 相关依赖 ****************** -->\n\n\t\t\t<!-- Redisson 相关操作API -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>org.redisson</groupId>\n\t\t\t\t<artifactId>redisson</artifactId>\n\t\t\t\t<version>${redisson.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<dependency>\n\t\t\t\t<groupId>org.redisson</groupId>\n\t\t\t\t<artifactId>redisson-spring-boot-starter</artifactId>\n\t\t\t\t<version>${redisson.version}</version>\n\t\t\t</dependency>\n\t        \n\t\t\t<!-- jackson-datatype-jsr310 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>com.fasterxml.jackson.datatype</groupId>\n\t\t\t\t<artifactId>jackson-datatype-jsr310</artifactId>\n\t\t\t\t<version>${jackson-datatype-jsr310.version}</version>\n\t\t\t</dependency>\n\t\t\t\n\t        <!-- fastjson -->\n\t        <dependency>\n\t            <groupId>com.alibaba</groupId>\n\t            <artifactId>fastjson</artifactId>\n\t            <version>${fastjson.version}</version>\n\t        </dependency>\n        \n\t        <!-- fastjson2 -->\n\t        <dependency>\n\t\t\t\t<groupId>com.alibaba.fastjson2</groupId>\n\t\t\t\t<artifactId>fastjson2</artifactId>\n\t\t\t\t<version>${fastjson2.version}</version>\n\t\t\t</dependency>\n\t\t\t\n\t\t\t<!-- noear-redisx -->\n\t        <dependency>\n\t            <groupId>org.noear</groupId>\n\t            <artifactId>redisx</artifactId>\n\t            <version>${noear-redisx.version}</version>\n\t        </dependency>\n\t\n\t\t\t<!-- solon-test -->\n\t        <dependency>\n\t            <groupId>org.noear</groupId>\n\t            <artifactId>solon-test</artifactId>\n\t            <version>${solon.version}</version>\n\t        </dependency>\n\t\t\t\n\t        <!-- redis pool -->\n\t\t\t<dependency>\n\t\t\t    <groupId>org.apache.commons</groupId>\n\t\t\t    <artifactId>commons-pool2</artifactId>\n\t\t\t\t<version>${commons-pool2.version}</version>\n\t\t\t</dependency>\n\t\t\t\n\t\t\t<!-- thymeleaf -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>org.thymeleaf</groupId>\n\t\t\t\t<artifactId>thymeleaf</artifactId>\n\t\t\t\t<version>${thymeleaf.version}</version>\n\t\t    </dependency>\n\n\t\t\t<!-- freemarker -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>org.freemarker</groupId>\n\t\t\t\t<artifactId>freemarker</artifactId>\n\t\t\t\t<version>${freemarker.version}</version>\n\t\t\t</dependency>\n\n\t\t\t\n\t\t\t<!-- hutool-jwt -->\n\t        <dependency>\n\t\t\t    <groupId>cn.hutool</groupId>\n\t\t\t    <artifactId>hutool-jwt</artifactId>\n\t\t\t    <version>${hutool-jwt.version}</version>\n\t\t\t</dependency>\n\t\t\t\n\t\t\t<!-- dubbo -->\n\t\t    <dependency>\n\t\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t\t<artifactId>dubbo</artifactId>\n\t\t\t\t<version>${dubbo.version}</version>\n\t\t\t</dependency>\n\t    \n\t    \t<!-- grpc-spring-boot-starter -->\n\t        <dependency>\n\t            <groupId>net.devh</groupId>\n\t            <artifactId>grpc-spring-boot-starter</artifactId>\n\t            <version>${grpc-spring-boot-starter.version}</version>\n\t        </dependency>\n\n\t\t\t<dependency>\n\t\t\t\t<groupId>io.jsonwebtoken</groupId>\n\t\t\t\t<artifactId>jjwt</artifactId>\n\t\t\t\t<version>${jjwt.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<!-- Hutool Cache  -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.hutool</groupId>\n\t\t\t\t<artifactId>hutool-cache</artifactId>\n\t\t\t\t<version>${hutool-cache.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<!-- Caffeine -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>com.github.ben-manes.caffeine</groupId>\n\t\t\t\t<artifactId>caffeine</artifactId>\n\t\t\t\t<version>${caffeine.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<!-- Forest -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>com.dtflys.forest</groupId>\n\t\t\t\t<artifactId>forest-core</artifactId>\n\t\t\t\t<version>${forest.version}</version>\n\t\t\t</dependency>\n\n\t\t\t<!-- Forest -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.zhxu</groupId>\n\t\t\t\t<artifactId>okhttps</artifactId>\n\t\t\t\t<version>${okhttps.version}</version>\n\t\t\t</dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\n</project>"
  },
  {
    "path": "sa-token-doc/README.md",
    "content": "<p align=\"center\">\n\t<img alt=\"logo\" src=\"https://sa-token.cc/logo.png\" width=\"150\" height=\"150\">\n</p>\n<h1 align=\"center\" style=\"margin: 30px 0 30px; font-weight: bold;\">Sa-Token v1.45.0</h1>\n<h5 align=\"center\">✨ 开源、免费、一站式 java 权限认证框架，让鉴权变得简单、优雅！</h5>\n<p align=\"center\" class=\"badge-box\">\n\t<a href=\"https://gitee.com/dromara/sa-token/stargazers\"><img src=\"https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp\"></a>\n\t<a href=\"https://gitee.com/dromara/sa-token/members\"><img src=\"https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp\"></a>\n\t<a href=\"https://atomgit.com/dromara/sa-token/stargazers\"><img src=\"https://atomgit.com/dromara/Sa-Token/star/badge.svg\"></a>\n\t<a href=\"https://github.com/dromara/sa-token/stargazers\"><img src=\"https://img.shields.io/github/stars/dromara/sa-token?style=flat-square&logo=GitHub\"></a>\n\t<a href=\"https://github.com/dromara/sa-token/network/members\"><img src=\"https://img.shields.io/github/forks/dromara/sa-token?style=flat-square&logo=GitHub\"></a>\n\t<!-- <a href=\"https://github.com/dromara/sa-token/watchers\"><img src=\"https://img.shields.io/github/watchers/dromara/sa-token?style=flat-square&logo=GitHub\"></a> -->\n\t<!-- <a href=\"https://github.com/dromara/sa-token/issues\"><img src=\"https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub\"></a> -->\n\t<a href=\"https://github.com/dromara/sa-token/blob/master/LICENSE\"><img src=\"https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square\"></a>\n</p>\n\n---\n\n## 📝 前言：️️\n为了保证新同学不迷路，请允许我唠叨一下：无论您从何处看到本篇文章，最新开发文档永远在：[https://sa-token.cc](https://sa-token.cc)，\n建议收藏在浏览器书签，如果您已经身处本网站下，则请忽略此条说明。\n\n回望 2020 年初，我为 Sa-Token 提交第一行代码之际，彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架，更是一整套清晰、自洽的权限架构设计思想。\n\n因此，这几年间我将大量时间倾注在 Sa-Token 的文档编写，几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨，以求做到最清晰、干练、易懂的表述。用心阅读文档，你学习到的将不止是 Sa-Token 框架本身，更是绝大多数场景下权限设计的最佳实践。\n\n\n## 🛠️ Sa-Token 介绍\n\n**Sa-Token** 是一个轻量级 Java 权限认证框架，主要解决：**登录认证**、**权限认证**、**单点登录**、**OAuth2.0**、**分布式Session会话**、**微服务网关鉴权**\n等一系列权限相关问题。\n\n<!-- ![sa-token-jss](/big-file/index/intro/sa-token-jss--tran.png) -->\n\n<object class=\"sa-token-jss-img\" data=\"/big-file/index/intro/sa-token-jss--tran--onclick.svg\"></object>\n\nSa-Token 旨在以简单、优雅的方式完成系统的权限认证部分，以登录认证为例，你只需要：\n\n``` java\n// 会话登录，参数填登录人的账号id \nStpUtil.login(10001);\n```\n\n无需实现任何接口，无需创建任何配置文件，只需要这一句静态代码的调用，便可以完成会话登录认证。\n\n如果一个接口需要登录后才能访问，我们只需调用以下代码：\n\n``` java\n// 校验当前客户端是否已经登录，如果未登录则抛出 `NotLoginException` 异常\nStpUtil.checkLogin();\n```\n\n在 Sa-Token 中，大多数功能都可以一行代码解决：\n\n踢人下线：\n\n``` java\n// 将账号id为 10077 的会话踢下线 \nStpUtil.kickout(10077);\n```\n\n权限认证：\n\n``` java\n// 注解鉴权：只有具备 `user:add` 权限的会话才可以进入方法\n@SaCheckPermission(\"user:add\")\npublic String insert(SysUser user) {\n    // ... \n    return \"用户增加\";\n}\n```\n\n路由拦截鉴权：\n\n``` java\n// 根据路由划分模块，不同模块不同鉴权 \nregistry.addInterceptor(new SaInterceptor(handler -> {\n\tSaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n\tSaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n\tSaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n\tSaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n\tSaRouter.match(\"/notice/**\", r -> StpUtil.checkPermission(\"notice\"));\n\t// 更多模块... \n})).addPathPatterns(\"/**\");\n```\n\n当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后，你就会明白，相对于这些传统老牌框架，Sa-Token 的 API 设计是多么的简单、优雅！\n\n\n## 🎉 Sa-Token 功能一览\n\nSa-Token 目前主要五大功能模块：登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。\n\n- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。\n- **权限认证** —— 权限认证、角色认证、会话二级认证。\n- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线。\n- **注解式鉴权** —— 优雅的将鉴权与业务代码分离。\n- **路由拦截式鉴权** —— 根据路由拦截鉴权，可适配 restful 模式。\n- **Session会话** —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。\n- **持久层扩展** —— 可集成 Redis，重启数据不丢失。\n- **前后台分离** —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。\n- **Token风格定制** —— 内置六种 Token 风格，还可：自定义 Token 生成策略。\n- **记住我模式** —— 适配 [记住我] 模式，重启浏览器免验证。\n- **二级认证** —— 在已登录的基础上再次认证，保证安全性。 \n- **模拟他人账号** —— 实时操作任意用户状态数据。\n- **临时身份切换** —— 将会话身份临时切换为其它账号。\n- **同端互斥登录** —— 像QQ一样手机电脑同时在线，但是两个手机上互斥登录。\n- **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。\n- **密码加密** —— 提供基础加密算法，可快速 MD5、SHA1、SHA256、AES 加密。\n- **会话查询** —— 提供方便灵活的会话查询接口。\n- **Http Basic认证** —— 一行代码接入 Http Basic、Digest 认证。\n- **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。\n- **全局过滤器** —— 方便的处理跨域，全局设置安全响应头等操作。\n- **多账号体系认证** —— 一个系统多套账号分开鉴权（比如商城的 User 表和 Admin 表）\n- **单点登录** —— 内置三种单点登录模式：同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。\n- **单点注销** —— 任意子系统内发起注销，即可全端下线。\n- **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务，支持openid模式 。\n- **分布式会话** —— 提供共享数据中心分布式会话方案。\n- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。\n- **RPC调用鉴权** —— 网关转发鉴权，RPC调用鉴权，让服务调用不再裸奔\n- **临时Token认证** —— 解决短时间的 Token 授权问题。\n- **独立Redis** —— 将权限缓存与业务缓存分离。\n- **Quick快速登录认证** —— 为项目零代码注入一个登录页面。\n- **标签方言** —— 提供 Thymeleaf 标签方言集成包，提供 beetl 集成示例。\n- **jwt集成** —— 提供三种模式的 jwt 集成方案，提供 token 扩展参数能力。\n- **RPC调用状态传递** —— 提供 dubbo、grpc 等集成包，在RPC调用时登录状态不丢失。\n- **参数签名** —— 提供跨系统API调用签名校验模块，防参数篡改，防请求重放。\n- **自动续签** —— 提供两种Token过期策略，灵活搭配使用，还可自动续签。\n- **开箱即用** —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包，开箱即用。\n- **最新技术栈** —— 适配最新技术栈：支持 SpringBoot 3.x，jdk 17。\n\n功能结构图：\n\n<img class=\"s-w\" src=\"/big-file/index/intro/sa-token-js4.png\" />\n\n\n## 📖❓ 疑问解答\n\n**1、Sa-Token 功能全不全？** \n\n七年磨一剑：五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证，我们提供的是一站式解决方案。\n\n\n**2、Sa-Token 好不好学？** \n\n中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。\n\n\n**3、Sa-Token 用的人多不多？** \n\n截止统计日 (2026-1-25) 起，Sa-Token 在：\n\n- Gitee 关注量达到 48627 Star，位列平台所有推荐项目排行榜第一名。\n- GitHub 关注量达到 18523 Star，是主要竞争框架 Spring Security 的 1.97 倍，Apache Shiro 的 4.19 倍。\n- 25+ 微信粉丝群 (500人)，8+ QQ粉丝群 (1000人 or 2000人) ，在线文档访问量月PV 20万+。\n\n这是众多开发者用脚投票的数据，相信这些数据比任何言语都能证明 Sa-Token 的热度。\n\n\n**4、Sa-Token 有哪些权威认证？** \n\n曾获荣誉包括但不限于：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。\n\n\n**5、Sa-Token 收费吗？** \n\nSa-Token 采用 Apache-2.0 开源协议，承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token，我们也不回避：[赞助链接](https://sa-token.cc/doc.html#/more/sa-token-donate)。\n我们将定期同步赞助者名单到在线文档展示。（您需要注意的一点是：该赞助仅为友情赞助，不提供任何商业交换）\n\n\n**6、Sa-Token 是封装的 SpringSecurity 吗？是套壳 ApacheShiro 吗？** \n\n不是。Sa-Token 不是一个后台模板，也不是针对 xx 框架的二次封装套壳，而是从 0 开始的纯血自研框架，核心包零依赖，完全自主可控的架构内核 + 众多主流框架的集成适配。\n\t\t\t\t\t\n\n\n## 📈 开源仓库 Star 趋势\n\n<p class=\"un-dec-a-pre\"></p>\n\n[![github-chart](https://starchart.cc/dromara/sa-token.svg 'GitHub')](https://starchart.cc/dromara/sa-token)\n\n如果 Sa-Token 帮助到了您，希望您可以为其点上一个 `star`：\n[码云](https://gitee.com/dromara/sa-token)、\n[AtomGit](https://atomgit.com/dromara/sa-token)、\n[GitHub](https://github.com/dromara/sa-token)\n\n\n## 🚀 使用 Sa-Token 的开源项目 \n参考：[Sa-Token 生态](/more/link)\n\n\n\n## 💬 交流群\n加入 Sa-Token 框架 QQ、微信讨论群：[点击加入](/more/join-group.md)\n\n"
  },
  {
    "path": "sa-token-doc/_sidebar.md",
    "content": "<!-- 这是目录树文件 -->\n\n- **开始**\n\t- [框架介绍](/)\n\t- [在 SpringBoot 环境集成](/start/example) \t\n\t- [在 WebFlux 环境集成](/start/webflux-example) \t\n\t- [在 Solon 环境集成](/start/solon-example) \t\n\t- [其它环境集成示例](/start/download)\n\n\n- **基础**\n\t- [登录认证](/use/login-auth) \n\t- [权限认证](/use/jur-auth) \n\t- [踢人下线](/use/kick) \n\t- [注解鉴权](/use/at-check) \n\t- [路由拦截鉴权](/use/route-check) \n\t- [Session会话](/use/session) \n\t- [框架配置](/use/config) \n\n- **深入**\n\t- [集成 Redis](/up/integ-redis)\n\t- [前后端分离](/up/not-cookie) \n\t- [自定义 Token 风格](/up/token-style) \n\t- [Token 提交前缀](/up/token-prefix) \n\t- [同端互斥登录](/up/mutex-login) \n\t- [记住我模式](/up/remember-me)\n\t- [登录参数 & 注销参数](/up/login-parameter) \n\t- [二级认证](/up/safe-auth) \n\t- [模拟他人 & 身份切换](/up/mock-person) \n\t- [账号封禁](/up/disable) \n\t- [密码加密](/up/password-secure) \n\t- [会话查询](/up/search-session) \n\t- [Http Basic/Digest 认证](/up/basic-auth) \n\t- [全局侦听器](/up/global-listener) \n\t- [全局过滤器](/up/global-filter) \n\t- [多账号认证](/up/many-account) \n\n- **单点登录**\n\t- [单点登录简述](/sso/readme)\n\t- [搭建统一认证中心：SSO-Server](/sso/sso-server)\n\t- [SSO-Server 认证中心开放 API 接口](/sso/sso-apidoc)\n\t- [SSO模式一 共享Cookie同步会话](/sso/sso-type1)\n\t- [SSO模式二 URL重定向传播会话](/sso/sso-type2)\n\t- [SSO模式三 Http请求获取会话](/sso/sso-type3)\n\t- [配置域名校验](/sso/sso-check-domain)\n\t- [定制化登录页面](/sso/sso-custom-login)\n\t- [自定义API路由](/sso/sso-custom-api)\n\t- [平台中心跳转模式](/sso/sso-home-jump)\n\t- [匿名 client 接入](/sso/anon-client)\n\t- [单点注销](/sso/signout)\n\t- [前后端分离下的整合方案](/sso/sso-h5)\n\t- [消息推送机制](/sso/message-push)\n\t<!-- - [不同 Client 不同秘钥](/sso/sso-diff-key) -->\n\t- [用户数据同步 / 迁移](/sso/user-data-sync)\n\t- [NoSdk、ReSdk 模式与非 java 项目](/sso/sso-nosdk)\n\t- [SSO 代码 API 参考](/sso/sso-dev)\n\t- [常见问题总结](/sso/sso-questions)\n\t- [Sa-Pro：单点登录商业版](/pro/st_sso)\n\n- **OAuth2.0**\n\t- [OAuth2.0简述](/oauth2/readme)\n\t- [OAuth2-Server搭建](/oauth2/oauth2-server)\n\t- [OAuth2-Server端开放 API 接口](/oauth2/oauth2-apidoc)\n\t- [自定义数据加载器](/oauth2/oauth2-data-loader)\n\t- [配置 client 域名校验 ](/oauth2/oauth2-check-domain)\n\t- [自定义 Scope 权限及处理器](/oauth2/oauth2-custom-scope)\n\t- [为 Scope 划分等级](/oauth2/oauth2-scope-level)\n\t- [自定义 grant_type](/oauth2/oauth2-custom-grant_type)\n\t- [定制化登录页面与授权页面](/oauth2/oauth2-custom-login)\n\t- [自定义 API 路由 ](/oauth2/oauth2-custom-api)\n\t- [OAuth2-Server端前后台分离](/oauth2/oauth2-h5)\n\t- [OpenId 与 UnionId](/oauth2/oauth2-openid)\n\t- [开启 OIDC 协议](/oauth2/oauth2-oidc)\n\t- [使用注解校验 Access-Token](/oauth2/oauth2-at-check)\n\t- [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking)\n\t- [OAuth2 代码 API 参考](/oauth2/oauth2-dev)\n\t- [常见问题总结](/oauth2/oauth2-questions)\n\t- [Sa-Max：统一认证商业版](/pro/st_oauth2)\n\t<!-- - [前后端分离模式整合方案](/oauth2/4) -->\n\t<!-- - [平台中心模式开发](/oauth2/5) -->\n\t<!-- - [jwt 风格 token](/oauth2/6) -->\n\n- **微服务**\n\t- [分布式Session会话](/micro/dcs-session)\n\t- [网关统一鉴权](/micro/gateway-auth)\n\t- [内部服务外网隔离](/micro/same-token)\n\t- [依赖引入说明](/micro/import-intro)\n\n- **插件**\n\t- [AOP注解鉴权](/plugin/aop-at)\n\t- [临时 Token 认证](/plugin/temp-token)\n\t- [Quick-Login快速登录插件](/plugin/quick-login)\n\t- [Alone独立Redis插件](/plugin/alone-redis)\n\t- [缓存层扩展](/plugin/dao-extend)\n\t- [JSON 序列化扩展](/plugin/json-extend)\n\t- [序列化插件扩展包](/plugin/custom-serializer)\n\t- [和 Thymeleaf 集成](/plugin/thymeleaf-extend)\n\t- [和 Freemarker 集成](/plugin/freemarker-extend)\n\t- [注解鉴权 SpEL 表达式](/plugin/spel-at)\n\t- [和 jwt 集成](/plugin/jwt-extend)\n\t- [和 Dubbo 集成](/plugin/dubbo-extend)\n\t- [和 gRPC 集成](/plugin/grpc-extend)\n\t- [API 接口参数签名](/plugin/api-sign)\n\t- [API Key 接口调用秘钥](/plugin/api-key)\n\t- [Sa-Token 插件开发指南](/fun/plugin-dev)\n\t- [自定义 SaTokenContext 指南](/fun/sa-token-context)\n\n\n- **API手册**\n\t- [StpUtil-鉴权工具类](/api/stp-util)\n\t- [SaSession-会话对象](/api/sa-session)\n\t- [SaTokenDao-数据持久接口](/api/sa-token-dao)\n\t- [SaStrategy-全局策略](/api/sa-strategy)\n\t- [全局类、方法](/more/common-action) \n\n\n- **框架设计**\n\t- [仓库目录](/arch/dir-intro)\n\t- [数据结构](/arch/data-structure)\n\n\n- **其它**\n\t- [更新日志](/more/update-log) \n\t- [框架生态](/more/link) \n\t- [框架博客](/more/blog) \n\t- [推荐公众号](/more/tj-gzh) \n\t- [加入讨论群](/more/join-group) \n\t- [Sa-Token 内容合作群](/more/content-cooperation) \n\t- [赞助 Sa-Token](/more/sa-token-donate)\n\t- [需求提交](/more/demand-commit) \n\t- [问卷调查](/more/wenjuan) \n\n- **附录**\n\t- [常见问题排查](/more/common-questions)  \n\t- [框架名词解释](/more/noun-intro)  \n\t- [Sa-Token功能结构图](/fun/auth-flow)\n\t- [全局 Log 输出](/fun/log) \n\t- [异步 & Mock 上下文](/fun/async--mock)\n\t- [未登录场景值详解](/fun/not-login-scene)\n\t- [Token有效期详解](/fun/token-timeout)\n\t- [Session模型详解](/fun/session-model)\n\t- [数据读写三大作用域](/fun/three-scope)  \n\t- [TokenInfo参数详解](/fun/token-info)\n\t- [异常细分状态码](/fun/exception-code)\n\t- [自定义注解](/fun/custom-annotations)\n\t- [防火墙](/fun/firewall)\n\t- [参考：把权限放在缓存里](/fun/jur-cache)\n\t- [参考：把路由拦截鉴权动态化](/fun/dynamic-router-check)\n\t- [解决反向代理 uri 丢失的问题](/fun/curr-domain)\n\t- [解决跨域问题](/fun/cors-filter)\n\t- [技术选型：SSO 与 OAuth2 对比](/fun/sso-vs-oauth2)\n\t- [集成 MongoDB 参考一](/up/integ-spring-mongod-1)\n\t- [集成 MongoDB 参考二](/up/integ-spring-mongod-2)\n\t<!-- - [框架源码所有技术栈](/fun/tech-stack) -->\n\t- [从 Shiro、SpringSecurity、JWT 迁移](/fun/auth-framework-function-test)\n\t- [issue 提问模板](/fun/issue-template)\n\t- [为Sa-Token贡献代码](/fun/git-pr)\n\t- [Sa-Token开源大事记](/fun/timeline)\n\t<!-- - [参考资料](/fun/refer-info) -->\n\t- [团队成员](/fun/team)\n\t- [Sa-Token框架掌握度--在线考试](/fun/sa-token-test)\n\t\n\n\n<br/><br/><br/><br/><br/><br/><br/>\n<p style=\"text-align: center;\">----- 到底线了 -----</p>"
  },
  {
    "path": "sa-token-doc/api/sa-session.md",
    "content": "# SaSession-会话对象\n\nSaSession-会话对象，专业数据缓存组件。\n\n--- \n\n### 1、常量 \n``` java\nSaSession.USER= \"USER\";   // 在 Session 上存储用户对象时建议使用的key \nSaSession.ROLE_LIST = \"ROLE_LIST\";   // 在 Session 上存储角色时建议使用的key \nSaSession.PERMISSION_LIST = \"PERMISSION_LIST\";   // 在 Session 上存储权限时建议使用的key \n```\n\n\n### 2、构建相关 \n``` java\nsession.getId();   // 获取此 Session 的 id \nsession.setId(id);   // 写入此 Session 的 id\nsession.getCreateTime();   // 返回当前会话创建时间（时间戳）\nsession.setCreateTime(createTime);   // 写入此 Session 的创建时间（时间戳）\n```\n\n\n### 3、SaTerminalInfo 相关 \n``` java\nsession.setTerminalList(terminalList);   // 写入登录终端信息列表\nsession.getTerminalList();   // 获取登录终端信息列表\nsession.terminalListCopy();   // 获取 登录终端信息列表 (拷贝副本)\nsession.getTerminalListByDeviceType(deviceType);   // 获取 登录终端信息列表 (拷贝副本)，根据 deviceType 筛选\nsession.getTerminal(tokenValue);   // 查找一个终端信息，根据 tokenValue\nsession.addTerminal(terminal);   // 添加一个终端信息\nsession.removeTerminal(tokenValue);   // 移除一个终端信息\nsession.maxTerminalIndex();   // 获取最大的终端索引值，如无返0\nsession.isTrustDeviceId(\"xxxxxxxxxxxxxxxxxxxxxxxx\");   // 判断指定设备 id 是否为可信任设备\n```\n\n\n### 4、一些操作\n``` java\nsession.update();   // 更新Session（从持久库更新刷新一下）\nsession.logout();   // 注销Session (从持久库删除)\nsession.logoutByTerminalCountToZero();   // 当 Session 上的 SaTerminalInfo 数量为零时，注销会话 \nsession.getTimeout();   // 获取此Session的剩余存活时间 (单位: 秒)\nsession.updateTimeout(timeout);   // 修改此Session的剩余存活时间\nsession.updateMinTimeout(minTimeout);   // 修改此Session的最小剩余存活时间 (只有在 Session 的过期时间低于指定的 minTimeout 时才会进行修改)\nsession.updateMaxTimeout(maxTimeout);   // 修改此Session的最大剩余存活时间 (只有在 Session 的过期时间高于指定的 maxTimeout 时才会进行修改)\nsession.trans(value);   // value为 -1 时返回 Long.MAX_VALUE，否则原样返回 \n```\n\n\n### 5、存取值 \n``` java\nsession.get(key);   // 取值\nsession.get(key, defaultValue);   // 取值 (指定默认值)\nsession.get(key, () -> {});   // 取值 (如果值为 null，则执行 fun 函数获取值，并把函数返回值写入缓存) \nsession.getString(key);   // 取值 (转String类型)\nsession.getInt(key);   // 取值 (转int类型)\nsession.getLong(key);   // 取值 (转long类型)\nsession.getDouble(key);   // 取值 (转double类型)\nsession.getFloat(key);   // 取值 (转float类型)\nsession.getModel(key, clazz);   // 取值 (指定转换类型)\nsession.getModel(key, clazz, defaultValue);   // 取值 (指定转换类型, 并指定值为Null时返回的默认值)\nsession.has(key);   // 是否含有某个key\nsession.set(key, value);   // 写值\nsession.setByNull(key, value);   // 写值 (只有在此 key 原本无值的情况下才会写入)\nsession.delete(key);   // 删值 \nsession.keys();   // 返回当前Session的所有key \nsession.clear();   // 清空所有值 \nsession.getDataMap();   // 获取数据挂载集合（如果更新map里的值，请调用session.update()方法避免产生脏数据 ） \nsession.refreshDataMap(dataMap);   // 写入数据集合 (不改变底层对象，只将此dataMap所有数据进行替换) \n```\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/api/sa-strategy.md",
    "content": "# SaStrategy-全局策略\n\nSaStrategy-全局策略，核心逻辑的代理封装。\n\n--- \n\n### 核心策略\n\n``` java\n/**\n * 创建 Token 的策略 \n * <p> 参数 [账号id, 账号类型] \n */\npublic BiFunction<Object, String, String> createToken = (loginId, loginType) -> {\n\t// 默认，还是uuid \n\treturn \"xxxxx-xxxxx-xxxxx-xxxxx\";\n};\n\n/**\n * 创建 Session 的策略 \n * <p> 参数 [SessionId] \n */\npublic Function<String, SaSession> createSession = (sessionId) -> {\n\treturn new SaSession(sessionId);\n};\n\n/**\n * 反序列化 SaSession 时默认指定的类型\n */\npublic Class<? extends SaSession> sessionClassType = SaSession.class;\n\n/**\n * 判断：集合中是否包含指定元素（模糊匹配） \n * <p> 参数 [集合, 元素] \n */\npublic BiFunction<List<String>, String, Boolean> hasElement = (list, element) -> {\n\treturn false;\n};\n\n\n/**\n * 生成唯一式 token 的算法\n * <p> 参数：元素名称, 最大尝试次数, 创建 token 函数, 检查 token 函数 </p>\n */\npublic SaGenerateUniqueTokenFunction generateUniqueToken = (elementName, maxTryTimes, createTokenFunction, checkTokenFunction) -> {\n\t// ...\n\treturn \"xxxxxx\";\n};\n\n/**\n * 是否自动续期，每次续期前都会执行，可以加入动态判断逻辑\n * <p> 参数 当前 stpLogic 实例对象\n * <p> 返回 true 自动续期 false 不自动续期\n */\npublic Function<StpLogic, Boolean> autoRenew = (stpLogic) -> {\n\treturn stpLogic.getConfigOrGlobal().getAutoRenew();\n};\n\n/**\n * 创建 StpLogic 的算法\n * <p>  参数：账号体系标识  </p>\n * <p>  返回：创建好的 StpLogic 对象  </p>\n */\npublic SaCreateStpLogicFunction createStpLogic = (loginType) -> {\n\treturn new StpLogic(loginType);\n};\n\n/**\n * 路由匹配策略\n * <p>  参数：pattern, path  </p>\n * <p>  返回：是否匹配  </p>\n */\npublic SaRouteMatchFunction routeMatcher = (pattern, path) -> {\n\treturn true;\n};\n\n/**\n * CORS 策略处理函数\n * <p>  参数：请求包装对象, 响应包装对象, 数据读写对象  </p>\n */\npublic SaCorsHandleFunction corsHandle = (req, res, sto) -> {\n\n};\n```\n\n\n### 注解操作相关策略 \n\n``` java\n/**\n * 对一个 [Method] 对象进行注解校验 （注解鉴权内部实现）\n * <p>  参数：Method句柄  </p>\n * <p>  返回：无  </p>\n */\npublic SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> {\n\t// ... \n};\n\n/**\n * 对一个 [Element] 对象进行注解校验 （注解鉴权内部实现）\n * <p>  参数：element元素  </p>\n * <p>  返回：无  </p>\n */\n@SuppressWarnings(\"unchecked\")\npublic SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> {\n\t// ... \n};\n\n/**\n * 从元素上获取注解\n * <p>  参数：element元素，要获取的注解类型  </p>\n * <p>  返回：注解对象  </p>\n */\npublic SaGetAnnotationFunction getAnnotation = (element, annotationClass)->{\n\treturn element.getAnnotation(annotationClass);\n};\n\n/**\n * 判断一个 Method 或其所属 Class 是否包含指定注解\n * <p>  参数：Method、注解  </p>\n * <p>  返回：是否包含  </p>\n */\npublic SaIsAnnotationPresentFunction isAnnotationPresent = (method, annotationClass) -> {\n\t// ...\n\treturn false;\n};\n\n/**\n * SaCheckELRootMap 扩展函数\n * <p>  参数：SaCheckELRootMap 对象 </p>\n */\npublic SaCheckELRootMapExtendFunction checkELRootMapExtendFunction = rootMap -> {\n\t// 默认不做任何处理\n};\n```\n\n\n\n### 防火墙相关策略 \n\n``` java\n/**\n * 防火墙校验函数\n * <p> 参数：请求对象、响应对象、预留扩展参数 </p>\n */\npublic SaFirewallCheckFunction check = (req, res, extArg) -> {\n\t// ... \n};\n\n/**\n * 自定义当请求 path 校验不通过时地处理方案 \n * <p> 参数：防火墙校验异常、请求对象、响应对象、预留扩展参数 </p>\n */\nSaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {\n\t// 自定义处理逻辑 ...\n};\n```\n\n参考：[防火墙](/fun/firewall)\n"
  },
  {
    "path": "sa-token-doc/api/sa-token-dao.md",
    "content": "# SaTokenDao-数据持久接口\n\nSaTokenDao 是数据持久层接口，负责所有会话数据的底层写入和读取。\n\n--- \n\n### 1、常量 \n``` java\nSaTokenDao.NEVER_EXPIRE = -1;   // 常量，表示一个key永不过期 (在一个key被标注为永远不过期时返回此值)\nSaTokenDao.NOT_VALUE_EXPIRE = -2;   // 常量，表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值)  \n```\n\n\n### 2、字符串读写 \n``` java\ndao.get(key);   // 获取Value，如无返空\ndao.set(key, value, timeout);   // 写入Value，并设定存活时间 (单位: 秒)\ndao.update(key, value);   // 更新Value (过期时间不变)\ndao.delete(key);   // 删除Value\ndao.getTimeout(key);   // 获取Value的剩余存活时间 (单位: 秒) \ndao.updateTimeout(key, timeout);   // 修改Value的剩余存活时间 (单位: 秒) \n```\n\n\n### 3、对象读写 \n``` java\ndao.getObject(key);   // 获取Object，如无返空\ndao.setObject(key, value, timeout);   // 写入Object，并设定存活时间 (单位: 秒)\ndao.updateObject(key, value);   // 更新Object (过期时间不变)\ndao.deleteObject(key);   // 删除Object\ndao.getObjectTimeout(key);   // 获取Object的剩余存活时间 (单位: 秒) \ndao.updateObjectTimeout(key, timeout);   // 修改Object的剩余存活时间 (单位: 秒) \n```\n\n\n### 4、Session读写 \n``` java\ndao.getSession(sessionId);   // 获取Session，如无返空\ndao.setSession(session, timeout);   // 写入Session，并设定存活时间 (单位: 秒)\ndao.setSession(session);   // 更新Session (过期时间不变)\ndao.deleteSession(sessionId);   // 删除Session\ndao.getSessionTimeout(sessionId);   // 获取Session的剩余存活时间 (单位: 秒) \ndao.updateSessionTimeout(sessionId, timeout);   // 修改Session的剩余存活时间 (单位: 秒) \n```\n\n\n### 5、会话管理\n``` java\ndao.searchData(prefix, keyword, start, size, sortType);   // 搜索数据 \n```\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/api/stp-util.md",
    "content": "# StpUtil - 鉴权工具类\n\nStpUtil 是 Sa-Token 整体功能的核心，大多数功能均由此工具类提供。\n\n--- \n\n### 1、常规操作 \n``` java\nStpUtil.getStpLogic();   // 获取底层 StpLogic 对象。\nStpUtil.setStpLogic(newStpLogic);   // 安全的重置底层 StpLogic 引用。\nStpUtil.getLoginType();   // 获取账号类型 （例如：login、user、admin、teacher、student等等）。\nStpUtil.getTokenName();   // 获取 Token 的名称 \nStpUtil.getTokenValue();   // 获取本次请求前端提交的 Token。\nStpUtil.getTokenValueNotCut();   // 获取本次请求前端提交的 Token (不裁剪前缀) 。\nStpUtil.setTokenValue(tokenValue);   // 在当前会话中写入 Token 值。\nStpUtil.setTokenValue(tokenValue, timeout);   // 在当前会话中写入 Token 值，并指定 Cookie 有效期。\nStpUtil.setTokenValue(tokenValue, loginParameter);   // 在当前会话中写入 Token 值，并指定登录参数。\nStpUtil.setTokenValueToStorage(tokenValue);   // 将 Token 写入当前请求的 Storage 存储器。\nStpUtil.getTokenInfo();   // 获取当前 Token 的详细参数。\n```\n\n\n### 2、登录相关  \n``` java\nStpUtil.login(10001);   // 会话登录\nStpUtil.login(10001, \"APP\");   // 会话登录，并指定设备类型\nStpUtil.login(10001, true);   // 会话登录，并指定是否 [记住我]\nStpUtil.login(10001, 86400);   // 会话登录，并指定此次 token 有效期（单位：秒）\nStpUtil.login(10001, loginParameter);   // 会话登录，并指定所有登录参数Model\nStpUtil.createLoginSession(10001);   // 创建指定账号id的登录会话，此方法不会将 Token 注入到上下文 \nStpUtil.createLoginSession(10001, loginParameter);   // 创建指定账号id的登录会话，此方法不会将 Token 注入到上下文 \nStpUtil.getOrCreateLoginSession(10001);   // 获取指定账号的登录会话，若不存在则创建并返回\n```\n\n更多 SaLoginParameter 登录参数的用法与示例，请参考 [登录参数示例章节](/up/login-parameter)。\n\n\n### 3、注销相关\n``` java\nStpUtil.logout();   // 会话注销 \nStpUtil.logout(logoutParameter);   // 会话注销，根据注销参数\nStpUtil.logout(10001);   // 会话注销，根据账号id\nStpUtil.logout(10001, \"PC\");   // 会话注销，根据账号id 和 设备类型\nStpUtil.logout(10001, logoutParameter);   // 会话注销，根据账号id 和 注销参数\nStpUtil.logoutByTokenValue(token);   // 指定 Token 强制注销\nStpUtil.logoutByTokenValue(token, logoutParameter);   // 指定 Token 强制注销，带注销参数\nStpUtil.kickout(10001);   // 踢人下线，根据账号id\nStpUtil.kickout(10001, \"PC\");   // 踢人下线，根据账号id 和 设备类型\nStpUtil.kickout(10001, logoutParameter);   // 踢人下线，根据账号id 和 注销参数\nStpUtil.kickoutByTokenValue(token);   // 踢人下线，根据token\nStpUtil.kickoutByTokenValue(token, logoutParameter);   // 踢人下线，根据token，带注销参数\nStpUtil.replacedByTokenValue(token);   // 顶人下线，根据token\nStpUtil.replacedByTokenValue(token, logoutParameter);   // 顶人下线，根据token，带注销参数\nStpUtil.replaced(10001);   // 顶人下线，根据账号id\nStpUtil.replaced(10001, \"PC\");   // 顶人下线，根据账号id 和 设备类型\nStpUtil.replaced(10001, logoutParameter);   // 顶人下线，根据账号id 和 注销参数\n```\n\n更多 SaLogoutParameter 注销参数的用法与示例，请参考 [注销参数示例章节](/up/login-parameter?id=_2、注销参数)。\n\n\n### 4、会话查询\n``` java\nStpUtil.isLogin();   // 当前会话是否已经登录 \nStpUtil.isLogin(10001);   // 判断指定账号是否已登录\nStpUtil.checkLogin();   // 检验当前会话是否已经登录，如未登录，则抛出异常\nStpUtil.getLoginId();   // 获取当前会话账号id, 如果未登录，则抛出异常 \nStpUtil.getLoginId(defaultValue);   // 获取当前会话账号id, 如果未登录，则返回默认值 \nStpUtil.getLoginIdDefaultNull();   // 获取当前会话账号id, 如果未登录，则返回null \nStpUtil.getLoginIdAsString();   // 获取当前会话账号id, 并转换为String类型\nStpUtil.getLoginIdAsInt();   // 获取当前会话账号id, 并转换为int类型\nStpUtil.getLoginIdAsLong();   // 获取当前会话账号id, 并转换为long类型 \nStpUtil.getLoginIdByToken(token);   // 获取指定Token对应的账号id，如果未登录，则返回 null \nStpUtil.getLoginIdByTokenNotThinkFreeze(token);   // 获取指定Token对应的账号id（不考虑冻结状态），如果未登录，则返回 null \nStpUtil.getExtra(key);   // 获取当前 Token 的扩展信息（此函数只在jwt模式下生效）\nStpUtil.getExtra(token, key);   // 获取指定 Token 的扩展信息（此函数只在jwt模式下生效）\n```\n\n\n### 5、Session 相关\n``` java\n// Account-Session 相关 \nStpUtil.getSession();   // 获取当前会话的Session，如果Session尚未创建，则新建并返回 \nStpUtil.getSession(true);   // 获取当前会话的Session, 如果Session尚未创建，isCreate=是否新建并返回\nStpUtil.getSessionByLoginId(10001);   // 获取指定账号id的Session，如果Session尚未创建，则新建并返回\nStpUtil.getSessionByLoginId(10001, true);   // 获取指定账号id的Session, 如果Session尚未创建，isCreate=是否新建并返回\n\n// Token-Session 相关 \nStpUtil.getTokenSession();   // 获取当前会话的Session，如果Session尚未创建，则新建并返回 \nStpUtil.getTokenSessionByToken(token);   // 获取指定Token-Session，如果Session尚未创建，则新建并返回\nStpUtil.getAnonTokenSession();   // 获取当前匿名 Token-Session （可在未登录情况下使用的Token-Session）\n\n// 其它\nStpUtil.getSessionBySessionId(\"xxxx-xxxx-xxxx\");   // 获取指定 sessionId 的 Session，若不存在则返回 null\nStpUtil.isTrustDeviceId(123456, \"xxxxxxxxxxxxxxxxxxxxxxxx\");   // 判断对于指定 loginId 来讲，指定设备 id 是否为可信任设备\n```\n\n\n### 6、Token有效期相关\n``` java\n// Token 最低活跃频率\nStpUtil.getTokenActiveTimeout();   // 获取当前 token 距离被冻结还剩多少时间 (单位: 秒)\nStpUtil.getTokenLastActiveTime();  // 获取当前 token 最后活跃时间\nStpUtil.checkActiveTimeout();   // 检查当前token 是否已经被冻结，如果是则抛出异常  \nStpUtil.updateLastActiveToNow();   // 续签当前token：(将 [最后操作时间] 更新为当前时间戳)   \n\n// Token 有效期\nStpUtil.getTokenTimeout();   // 获取当前登录者的 token 剩余有效时间 (单位: 秒)\nStpUtil.getTokenTimeout(token);   // 获取指定 token 的剩余有效时间 (单位: 秒)\nStpUtil.getSessionTimeout();   // 获取当前登录者的 Account-Session 剩余有效时间 (单位: 秒)\nStpUtil.getTokenSessionTimeout();   // 获取当前 Token-Session 剩余有效时间 (单位: 秒) \nStpUtil.renewTimeout(timeout);   // 对当前 Token 的 timeout 值进行续期 \nStpUtil.renewTimeout(token, timeout);   // 对指定 Token 的 timeout 值进行续期 \n```\n\n\n### 7、角色认证\n``` java\nStpUtil.getRoleList();   // 获取：当前账号的角色集合\nStpUtil.getRoleList(10001);   // 获取：指定账号的角色集合 \nStpUtil.hasRole(role);   // 判断：当前账号是否拥有指定角色, 返回true或false \nStpUtil.hasRole(loginId, role);   // 判断：指定账号是否含有指定角色标识, 返回true或false \nStpUtil.hasRoleAnd(...roleArray);   // 判断：当前账号是否含有指定角色标识 [指定多个，必须全部验证通过] \nStpUtil.hasRoleOr(...roleArray);   // 判断：当前账号是否含有指定角色标识 [指定多个，只要其一验证通过即可] \nStpUtil.checkRole(role);   // 校验：当前账号是否含有指定角色标识, 如果验证未通过，则抛出异常: NotRoleException \nStpUtil.checkRoleAnd(...roleArray);   // 校验：当前账号是否含有指定角色标识 [指定多个，必须全部验证通过] \nStpUtil.checkRoleOr(...roleArray);   // 校验：当前账号是否含有指定角色标识 [指定多个，只要其一验证通过即可] \n```\n\n\n### 8、权限认证\n``` java\nStpUtil.getPermissionList();   // 获取：当前账号的权限集合\nStpUtil.getPermissionList(10001);   // 获取：指定账号的权限集合 \nStpUtil.hasPermission(permission);   // 判断：当前账号是否拥有指定权限, 返回true或false \nStpUtil.hasPermission(loginId, permission);   // 判断：指定账号是否含有指定权限标识, 返回true或false \nStpUtil.hasPermissionAnd(...permissionArray);   // 判断：当前账号是否含有指定权限标识 [指定多个，必须全部验证通过] \nStpUtil.hasPermissionOr(...permissionArray);   // 判断：当前账号是否含有指定权限标识 [指定多个，只要其一验证通过即可] \nStpUtil.checkPermission(permission);   // 校验：当前账号是否含有指定权限标识, 如果验证未通过，则抛出异常: NotPermissionException \nStpUtil.checkPermissionAnd(...permissionArray);   // 校验：当前账号是否含有指定权限标识 [指定多个，必须全部验证通过] \nStpUtil.checkPermissionOr(...permissionArray);   // 校验：当前账号是否含有指定权限标识 [指定多个，只要其一验证通过即可] \n```\n\n\n### 9、id 反查 Token\n``` java\nStpUtil.getTokenValueByLoginId(10001);   // 获取指定账号id的tokenValue \nStpUtil.getTokenValueByLoginId(10001, \"PC\");   // 获取指定账号id指定设备类型端的tokenValue\nStpUtil.getTokenValueListByLoginId(10001);   // 获取指定账号id的tokenValue集合 \nStpUtil.getTokenValueListByLoginId(10001, \"APP\");   // 获取指定账号id指定设备类型端的tokenValue 集合 \nStpUtil.getTerminalListByLoginId(10001);   // 获取指定账号 id 已登录设备信息集合\nStpUtil.getTerminalListByLoginId(10001, \"PC\");   // 获取指定账号 id 指定设备类型端的已登录设备信息集合\nStpUtil.forEachTerminalList(10001, (terminal) -> {});   // 遍历指定账号的已登录设备列表\nStpUtil.getTerminalInfo();   // 获取当前会话的终端信息\nStpUtil.getTerminalInfoByToken(token);   // 获取指定 token 的终端信息\nStpUtil.getLoginDeviceId();   // 返回当前会话的登录设备 id\nStpUtil.getLoginDeviceIdByToken(token);   // 返回指定 token 的登录设备 id\nStpUtil.getLoginDeviceType();   // 返回当前会话的登录设备类型\nStpUtil.getLoginDeviceTypeByToken(token);   // 返回指定 token 的登录设备类型\n```\n\n\n### 10、会话管理\n``` java\nStpUtil.searchTokenValue(keyword, start, size, sortType);   // 根据条件查询Token\nStpUtil.searchSessionId(keyword, start, size, sortType);   // 根据条件查询SessionId \nStpUtil.searchTokenSessionId(keyword, start, size, sortType);   // 根据条件查询Token专属Session的Id \n```\n详细可参考：[会话治理](/up/search-session)\n\n\n### 11、账号封禁\n``` java\nStpUtil.disable(10001, 1200);   // 封禁：指定账号 指定时间(单位s)\nStpUtil.isDisable(10001);   // 判断：指定账号是否已被封禁 (true=已被封禁, false=未被封禁) \nStpUtil.checkDisable(10001);   // 校验：指定账号是否已被封禁，如果被封禁则抛出异常 `DisableServiceException`\nStpUtil.getDisableTime(10001);   // 获取：指定账号剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\nStpUtil.untieDisable(loginId);   // 解封：指定账号\n```\n\n\n### 12、分类封禁 (version >= 1.31.0)\n``` java\nStpUtil.disable(10001, \"<业务标识>\", 86400);   // 封禁：指定账号的指定服务 指定时间(单位s)\nStpUtil.isDisable(10001, \"<业务标识>\");   // 判断：指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) \nStpUtil.checkDisable(10001, \"<业务标识>\");   // 校验：指定账号的指定服务 是否已被封禁，如果被封禁则抛出异常 `DisableServiceException`（支持传入多个业务标识）\nStpUtil.getDisableTime(10001, \"<业务标识>\");   // 获取：指定账号的指定服务 剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\nStpUtil.untieDisable(loginId, \"<业务标识>\");   // 解封：指定账号的指定服务（支持传入多个业务标识）\n```\n\n\n### 13、阶梯封禁 (version >= 1.31.0)\n``` java\nStpUtil.disableLevel(10001, \"comment\", 3, 10000);   // 分类阶梯封禁，参数：封禁账号、封禁服务、封禁级别、封禁时间 \nStpUtil.disableLevel(10001, 3, 10000);   // 阶梯封禁（无业务标识，使用默认服务），参数：封禁账号、封禁级别、封禁时间\nStpUtil.getDisableLevel(10001, \"comment\");   // 获取：指定账号的指定服务 封禁的级别 （如果此账号未被封禁则返回 -2）\nStpUtil.getDisableLevel(10001);   // 获取：指定账号的封禁等级（无业务标识，未封禁返回 -2）\nStpUtil.isDisableLevel(10001, \"comment\", 3);   // 判断：指定账号的指定服务 是否已被封禁到指定级别，返回 true 或 false\nStpUtil.isDisableLevel(10001, 3);   // 判断：指定账号是否被封禁到指定级别（无业务标识）\nStpUtil.checkDisableLevel(10001, \"comment\", 2);   // 校验：指定账号的指定服务 是否已被封禁到指定级别（例如 comment服务 已被3级封禁，这里校验是否达到2级），如果已达到此级别，则抛出异常 \nStpUtil.checkDisableLevel(10001, 2);   // 校验：指定账号是否被封禁到指定级别（无业务标识）\n```\n\n\n### 14、身份切换\n``` java\nStpUtil.switchTo(10044);   // 临时切换身份为指定账号id \nStpUtil.endSwitch();   // 结束临时切换身份\nStpUtil.isSwitch();   // 当前是否正处于[身份临时切换]中 \nStpUtil.switchTo(10044, () -> {});   // 在一个代码段里方法内，临时切换身份为指定账号id\n```\n\n\n### 15、二级认证\n``` java\nStpUtil.openSafe(safeTime);   // 在当前会话 开启二级认证 \nStpUtil.isSafe();   // 当前会话 是否处于二级认证时间内 \nStpUtil.checkSafe();   // 检查当前会话是否已通过二级认证，如未通过则抛出异常 \nStpUtil.getSafeTime();   // 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)\nStpUtil.closeSafe();   // 在当前会话 结束二级认证 \n```\n\n\n### 16、带有业务标识的二级认证\n``` java\nStpUtil.openSafe(\"<业务标识>\", safeTime);   // 在当前会话 指定业务标识开启二级认证 \nStpUtil.isSafe(\"<业务标识>\");   // 当前会话 指定业务标识是否处于二级认证时间内 \nStpUtil.isSafe(tokenValue, \"<业务标识>\");   // 判断指定 token 的指定业务是否处于二级认证时间内\nStpUtil.checkSafe(\"<业务标识>\");   // 检查当前会话，指定业务标识是否已通过二级认证，如未通过则抛出异常 \nStpUtil.getSafeTime(\"<业务标识>\");   // 获取当前会话的指定业务标识二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)\nStpUtil.closeSafe(\"<业务标识>\");   // 在当前会话 结束指定业务标识二级认证 \n```\n\n\n"
  },
  {
    "path": "sa-token-doc/arch/data-structure.md",
    "content": "# 数据结构\n\n\n## 1、登录会话\n\n### 1.1、token -> loginId 映射\n\n``` js\n// ttl = 此 token 的 timeout 有效期\n{tokenName}:{loginType}:token:{tokenValue}  -->  {loginId}\n```\n\n<details>\n<summary>详细</summary>\n\n示例：\n``` js\nsatoken:login:token:47ab0105-2be1-400c-b517-82f81a0cfcf8  -->  10001\n```\n\n异常 value 格式\n\n``` js\n-1       未能从请求中读取到有效 token\n-2       已读取到 token，但是 token 无效\n-3       已读取到 token，但是 token 已经过期 (详)\n-4       已读取到 token，但是 token 已被顶下线\n-5       已读取到 token，但是 token 已被踢下线\n-6       已读取到 token，但是 token 已被冻结\n-7       未按照指定前缀提交 token\n```\n\n</details>\n\n\n### 1.2、active-timeout\n\n``` js\n// ttl = 对应 token 的 timeout 有效期值\n{tokenName}:{loginType}:last-active:{tokenValue}  -->  {13位时间戳}\n```\n\n<details>\n<summary>详细</summary>\n\n示例：\n``` js\nsatoken:login:last-active:06d1f12b-614e-4c00-8d8e-c07fef5f4aa9   -->   1722334954193\n```\n\nvalue 格式分两种：\n```\n1722334954193          // 单值时：此 token 最后访问日期\n1722334954193, 1200    // 双值时：此 token 最后访问日期，此 token 指定的动态 active-timeout 值 \n```\n\n注意：判断一个 token 是否 active-timeout 过期，与 ttl 无关，而是利 value 值计算：\n``` js\n当前时间 - token 最后访问时间 > active-timeout   （true=token 已冻结，false=token 未冻结 ）\n```\n\n</details>\n\n\n\n### 1.3、SaSession\n\n``` js\n{tokenName}:{loginType}:session:{loginId}  -->  {SaSession 对象}        // Account-Session\n{tokenName}:{loginType}:token-session:{loginId}  -->  {SaSession 对象}  // Token-Session\n{tokenName}:custom:session:{sessionId}  -->  {SaSession 对象}           // Custom-Session\n```\n\n<details>\n<summary>详细</summary>\n\nkey 示例 \n``` js\n// Account-Session\nsatoken:login:session:1000001\n\n// Token-Session\nsatoken:login:session:47ab0105-2be1-400c-b517-82f81a0cfcf8\n\n// Custom-Session\nsatoken:custom:session:role-1001\n```\n\nvalue 格式 \n\n``` js\n{\n  \"@class\": \"cn.dev33.satoken.dao.SaSessionForJacksonCustomized\",    // java calss 信息\n  \"id\": \"satoken:login:session:10001\",    // sessionId\n  \"type\": \"Account-Session\",    // session类型：Account-Session / Token-Session / Custom-Session\n  \"loginType\": \"login\",     // 账号类型 \n  \"loginId\": [    // 对应登录id 值（Account-Session才会有值）\n    \"java.lang.Long\",\n    10001\n  ],    \n  \"token\": null,    // 对应 token 值 （Token-Session才会有值）\n  \"createTime\": 1722334954145,    // 此 session 创建时间，13位时间戳 \n  \"dataMap\": {    // 此 session 挂载数据 \n    \"@class\": \"java.util.concurrent.ConcurrentHashMap\", \n    \"name\": \"张三\"    // 此 session 挂载数据 详情\n\t// 更多值 ...\n  },\n  \"terminalList\": [\t\t// 已登录终端信息列表（Account-Session才会有值）\n    \"java.util.Vector\",\n    [\n      {\n        \"@class\": \"cn.dev33.satoken.session.SaTerminalInfo\",\n        \"index\": 1,\n        \"tokenValue\": \"2551663f-bb98-47d7-9af3-e2e6a28dadce\",   // 客户端 token 值\n        \"deviceType\": \"DEF\",  // 登录设备类型 \n        \"deviceId\": \"xxxxxxxxx\",  // 登录设备id \n        \"extraData\": {\n\t\t\t// 扩展信息列表 （手动自定义值）\n\t\t\t\"@class\": \"java.util.LinkedHashMap\",\n\t\t\t\"deviceSimpleTitle\": \"XiaoMi 15 Ultra\",\n\t\t\t\"loginAddress\": \"浙江省杭州市西湖区\",\n\t\t\t\"loginIp\": \"127.0.0.1\",\n\t\t\t\"loginTime\": \"2025-03-08 15:00:02\"\n        },\n        \"createTime\": 1741406340845 // 登录时间 \n      }\n    ]\n  ]\n}\n```\n\n</details>\n\n\n### 1.4、二级认证\n``` js\n{tokenName}:{loginType}:safe:{service}:{tokenValue}  -->  SAFE_AUTH_SAVE_VALUE\n```\nvalue 为常亮值：`SAFE_AUTH_SAVE_VALUE`\n\n\n### 1.5、账号服务封禁\n``` js\n{tokenName}:{loginType}:disable:{service}:{loginId}  -->  {level}\n```\nvalue 为封禁等级，int类型 \n\n\n### 1.6、其它\nSaApplication 全局变量\n``` js\n{tokenName}:var:{变量名}\n```\n\n本次请求新创建 token，在 SaStorage 存储 key \n``` js\nJUST_CREATED_  -->  {token}\n```\n\n本次请求新创建 token，在 SaStorage 存储 key  （无前缀方式）\n``` js\nJUST_CREATED_NOT_PREFIX_  -->  {token}\n```\n\n临时身份切换，使用的key\n``` js\nSWITCH_TO_SAVE_KEY_{loginType}  -->  {loginId}\n```\n\n\n## 2、SSO 单点登录\n\n### 2.1、ticket -> loginId 映射\n``` js\n// ttl = 此 ticket 有效期，下同理 \n{tokenName}:ticket:{ticket}  -->  {loginId}\n```\n\n### 2.2、ticket -> client 映射\n``` js\n{tokenName}:ticket-client:{ticket}  -->  {client}\n```\n\n### 2.3、loginId -> ticket 映射（client + loginId 反查 ticket）\n``` js\n{tokenName}:ticket-index:{client}:{loginId}  -->  {ticket}\n```\n\n\n\n## 3、OAuth2 统一认证 \n\n### 3.1、Code 授权码\n``` js\n{tokenName}:oauth2:code:{code}  -->  {CodeModel 对象}\n```\n\n<details>\n<summary>详细</summary>\n\nvalue 示例：\n\n``` js\n{\n\t\"@class\": \"cn.dev33.satoken.oauth2.model.CodeModel\",    // java class 信息\n\t\"code\": \"AbRVp2HrgyklE0BXYWszskGJWAGY7xhGu6Zaco4zJECzGYagCCFWj0jOlHza\",    // code值\n\t\"scope\": \"\",    // 所申请权限列表，多个用逗号隔开\n\t\"loginId\": \"10001\",    // 对应的loginId\n\t\"redirectUri\": \"\",    // 重定向地址\n}\n```\n\n</details>\n\nclientId + loginId 反查 code\n``` js\n{tokenName}:oauth2:code-index:{clientId}:{loginId}  -->  {code 值}\n```\n\n\n\n### 3.2、Access-Token 资源令牌\n``` js\n{tokenName}:oauth2:access-token:{accessToken}  -->  {AccessTokenModel 对象}\n```\n\n<details>\n<summary>详细</summary>\n\nvalue 示例：\n\n``` js\n{\n  \"@class\": \"cn.dev33.satoken.oauth2.data.model.AccessTokenModel\",    // java class 信息\n  \"accessToken\": \"Pu3t55dJIgvkmVoHz50FqaVQOJ6Flggjr2eHTiS74Ooai8e3nNyYPq78K80P\",    // 资源令牌值\n  \"refreshToken\": \"baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m\",    // 刷新令牌值\n  \"expiresTime\": 1738280553695,    // 资源令牌到期时间\n  \"refreshExpiresTime\": 1740865353760,    // 刷新令牌到期时间\n  \"clientId\": \"1001\",    // 对应的应用id\n  \"loginId\": \"10001\",    // 对应的loginId\n  \"scopes\": [   // 所具有的权限列表 \n    \"java.util.ArrayList\",\n    [\n      \"userinfo\",\n      \"userid\",\n      \"openid\",\n      \"unionid\",\n      \"oidc\"\n    ]\n  ],\n  \"tokenType\": \"bearer\",   // tokenType \n  \"grantType\": \"authorization_code\",   // 授权方式 \n  \"extraData\": {   // 扩展数据  \n    \"@class\": \"java.util.LinkedHashMap\",\n    \"userid\": \"10001\",\n    \"openid\": \"ded91dc189a437dd1bac2274be167d50\",\n    \"unionid\": \"11d48faa74c4e5f19355ccc53c1c5c7a\",\n    \"id_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g\"\n  },\n  \"createTime\": 1738273353694,   // 创建时间   \n  \"expiresIn\": 7199    // 资源令牌剩余有效时间，单位秒\n  \"refreshExpiresIn\": 2592000,    // 刷新令牌剩余有效时间，单位秒\n}\n\n```\n\n</details>\n\nclientId + loginId 反查 Access-Token\n``` js\n{tokenName}:oauth2:access-token-index:{clientId}:{loginId}  -->  {access_token 值}\n```\n\n\n### 3.3、Refresh-Token 资源令牌\n``` js\n{tokenName}:oauth2:refresh-token:{refreshToken}  -->  {RefreshTokenModel 对象}\n```\n\n<details>\n<summary>详细</summary>\n\nvalue 示例：\n\n``` js\n{\n  \"@class\": \"cn.dev33.satoken.oauth2.data.model.RefreshTokenModel\",    // java class 信息\n  \"refreshToken\": \"baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m\",    // 刷新令牌值\n  \"expiresTime\": 1740865353760,   // 刷新令牌到期时间\n  \"clientId\": \"1001\",    // 对应的应用id\n  \"loginId\": \"10001\",    // 对应的loginId\n  \"scopes\": [    // 所具有的权限列表 \n    \"java.util.ArrayList\",\n    [\n      \"userinfo\",\n      \"userid\",\n      \"openid\",\n      \"unionid\",\n      \"oidc\"\n    ]\n  ],\n  \"extraData\": {   // 扩展数据  \n    \"@class\": \"java.util.LinkedHashMap\",\n    \"userid\": \"10001\",\n    \"openid\": \"ded91dc189a437dd1bac2274be167d50\",\n    \"unionid\": \"11d48faa74c4e5f19355ccc53c1c5c7a\",\n    \"id_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g\"\n  },\n  \"createTime\": 1738273353760,   // 创建时间   \n  \"expiresIn\": 2591999    // 刷新令牌剩余有效时间，单位秒\n}\n```\n\n</details>\n\nclientId + loginId 反查 Refresh-Token\n``` js\n{tokenName}:oauth2:refresh-token-index:{clientId}:{loginId}  -->  {refresh_token 值}\n```\n\n\n### 3.4、Client-Token 应用令牌\n``` js\n{tokenName}:oauth2:client-token:{clientToken}  -->  {ClientTokenModel 对象}\n```\n\n<details>\n<summary>详细</summary>\n\nvalue 示例：\n\n``` js\n{\n  \"@class\": \"cn.dev33.satoken.oauth2.data.model.ClientTokenModel\",    // java class 信息\n  \"clientToken\": \"lIpS3fKEACKMFauEWVpR7Zmzh7SoFetPVuB9aDzISnqzHKu8R3OwpWFy5nLv\",    // 应用令牌值 \n  \"expiresTime\": 1738280930646,    // 应用令牌到期时间\n  \"clientId\": \"1001\",    // 对应的应用id\n  \"scopes\": [    // 所具有的权限列表 \n    \"java.util.ArrayList\",\n    [\n      \"userinfo\",\n      \"userid\",\n      \"openid\",\n      \"unionid\",\n      \"oidc\"\n    ]\n  ],\n  \"tokenType\": \"bearer\",   // tokenType   \n  \"grantType\": \"client_credentials\",   // 授权类型    \n  \"extraData\": {   // 扩展数据\n    \"@class\": \"java.util.LinkedHashMap\"\n  },\n  \"createTime\": 1738273730646,   // 创建时间   \n  \"expiresIn\": 7199    // 应用令牌剩余有效时间，单位秒\n}\n```\n\n</details>\n\nclientId 反查 Client-Token\n``` js\n{tokenName}:oauth2:client-token-index:{clientId}  -->  {client_token 值}\n```\n\nLower-Client-Token 次级应用令牌索引\n``` js\n{tokenName}:oauth2:lower-client-token-index:{clientId}  -->  {client_token 值}\n```\n\n### 3.5、用户授权记录\n``` js\n{tokenName}:oauth2:grant-scope:{clientId}:{loginId}  -->  {scope列表}\n```\n值为 scope 列表，多个用逗号隔开，例如：`userinfo,openid,userid`。\n\n\n\n\n## 4、插件\n\n### 4.1、临时 token 会话 \n\ntemp-token -> value\n\n``` js\n// namespace 默认值为 \"temp-token\"\n{tokenName}:{namespace}:{temp-token}  -->  {value}\n```\n\nvalue 反查 temp-token\n\n``` js\n{tokenName}:raw-session:{namespace}:{value}  -->  {Raw#SaSession 对象}\n```\n\n- 在 SaSession 以 `__HD_TEMP_TOKEN_MAP` 为 key 存储 temp-token 索引列表。值类型为 Map。\n- 其中：Map 的 key = temp-token 值，Map 的 value = 此 temp-token 到期时间戳。\n\n\n\n### 4.2、 Same-Token \n\nSame-Token \n``` js\n{tokenName}:var:same-token  -->  {same-token 值}\n```\n\nPast-Same-Token \n``` js\n{tokenName}:var:past-same-token  -->  {same-token 值}\n```\n\n\n### 4.3、Sign 签名\n\n随机字符串\n``` js\n// nonce 值 默认为 32位随机字符\n{tokenName}:sign:nonce:{nonce}  -->  {nonce 值}\n```\n\n### 4.4、API Key\n\n``` js\n// namespace 默认值为 \"apikey\" (全小写), ttl = 此 API Key 剩余有效期 \n{tokenName}:{namespace}:{apikey}  -->  {ApiKeyModel 对象}\n```\n\n<details>\n<summary>详细</summary>\n\nkey 示例：\n\n``` js\nsatoken:apikey:AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R\n```\n\nvalue 示例：\n``` js\n{\n\t\"@class\": \"cn.dev33.satoken.apikey.model.ApiKeyModel\",   // java class 信息\n\t\"title\": \"test\",   // API Key 名称 \n\t\"intro\": null,   // 用途介绍\n\t\"apiKey\": \"AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R\",   // API Key 值\n\t\"loginId\": \"10001\",   // 所属用户 id\n\t\"createTime\": 1766509019137,   // 创建时间戳\n\t\"expiresTime\": 1769101019136,   // 到期时间戳\n\t\"isValid\": true,   // 是否有效\n\t\"scopes\": [   // 含有权限\n\t\t\"java.util.ArrayList\",\n\t\t[\n\t\t  \"userinfo\",\n\t\t  \"user-update\"\n\t\t]\n\t],\n\t\"extraData\": null   // 扩展数据：Map<String, Object> 类型 \n}\n```\n\n</details>\n\n\nvalue 反查 API Key \n\n``` js\n{tokenName}:raw-session:{namespace}:{value}  -->  {Raw#SaSession 对象}\n```\n\n- 在 SaSession 以 `__HD_API_KEY_LIST` 为 key 存储 API Key 索引列表。值类型为 List (API Key 列表)。\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/arch/dir-intro.md",
    "content": "# 仓库目录介绍\n\n--- \n\n### 1、仓库根目录介绍：\n\n``` js\n── sa-token\n\t├── sa-token-core                         // [核心] Sa-Token 核心模块\n\t├── sa-token-dependencies                 // [依赖] Sa-Token 依赖版本信息\n\t├── sa-token-special-dependencies         // [依赖] Sa-Token 特殊依赖（SpringBoot2/3/4 版本隔离）\n\t├── sa-token-bom                          // [核心] Sa-Token bom 包\n\t├── sa-token-starter                      // [整合] Sa-Token 与其它框架整合\n\t├── sa-token-plugin                       // [插件] Sa-Token 插件合集\n\t├── sa-token-demo                         // [示例] Sa-Token 示例合集\n\t├── sa-token-test                         // [测试] Sa-Token 单元测试合集\n\t├── sa-token-doc                          // [文档] Sa-Token 开发文档 \n\t├── MEMO                                  // [备忘] 内部备忘录、开发记录\n\t├── pom.xml                               // [依赖] 顶级pom文件 \n\t├── LICENSE                               // 开源协议\n\t├── mvn clean.bat                         // 一键 mvn clean 核心包+所有示例包\n\t├── mvn test.bat                          // 一键单元测试 \n\t├── preview-doc.bat                       // 一键预览开发文档\n\t├── README.md                             // 仓库自述文件 \n```\n\n\n### 2、所有目录详细介绍：\n\n``` js\n── sa-token\n\t├── sa-token-core                         // [核心] Sa-Token 核心模块\n\t├── sa-token-dependencies                 // [依赖] Sa-Token 依赖版本信息\n\t├── sa-token-special-dependencies         // [依赖] Sa-Token 特殊依赖（SpringBoot2/3/4 版本隔离）\n\t├── sa-token-bom                          // [核心] Sa-Token bom 包\n\t├── sa-token-starter                      // [整合] Sa-Token 与其它框架整合\n\t\t├── sa-token-servlet                      // [整合] Sa-Token 整合 Servlet 容器实现类包\n\t\t├── sa-token-jakarta-servlet              // [整合] Sa-Token 整合 Jakarta-Servlet 容器实现类包\n\t\t├── sa-token-spring-boot-webmvc-reactor-v2v3v4-common  // [整合] Sa-Token SpringBoot WebMvc+Reactor 公共包 (2/3/4)\n\t\t├── sa-token-spring-boot-reactor-v2v3v4-common         // [整合] Sa-Token SpringBoot Reactor 公共包 (2/3/4)\n\t\t├── sa-token-spring-boot-starter                       // [整合] Sa-Token 整合 SpringBoot2 快速集成 \n\t\t├── sa-token-spring-boot-webmvc-v3v4-common            // [整合] Sa-Token SpringBoot WebMvc 公共包 (3/4)\n\t\t├── sa-token-spring-boot3-starter         // [整合] Sa-Token 整合 SpringBoot3 快速集成 \n\t\t├── sa-token-spring-boot4-starter         // [整合] Sa-Token 整合 SpringBoot4 快速集成 \n\t\t├── sa-token-reactor-spring-boot-starter  // [整合] Sa-Token 整合 SpringBoot2 Reactor 响应式编程 快速集成 \n\t\t├── sa-token-reactor-spring-boot3-starter // [整合] Sa-Token 整合 SpringBoot3 Reactor 响应式编程 快速集成 \n\t\t├── sa-token-reactor-spring-boot4-starter // [整合] Sa-Token 整合 SpringBoot4 Reactor 响应式编程 快速集成 \n\t\t├── sa-token-solon-plugin                 // [整合] Sa-Token 整合 Solon 快速集成 \n\t\t├── sa-token-jfinal-plugin                // [整合] Sa-Token 整合 JFinal 快速集成 \n\t\t├── sa-token-jboot-plugin                 // [整合] Sa-Token 整合 jboot 快速集成 \n\t\t├── sa-token-loveqq-boot-starter          // [整合] Sa-Token 整合 LoveQQ-Boot 快速集成 \n\t├── sa-token-plugin                       // [插件] Sa-Token 插件合集\n\t\t├── sa-token-jackson                      // [插件] Sa-Token 整合 Jackson (json序列化插件) \n\t\t├── sa-token-jackson3                     // [插件] Sa-Token 整合 Jackson3 (json序列化插件) \n\t\t├── sa-token-fastjson                     // [插件] Sa-Token 整合 Fastjson (json序列化插件) \n\t\t├── sa-token-fastjson2                    // [插件] Sa-Token 整合 Fastjson2 (json序列化插件) \n\t\t├── sa-token-snack3                       // [插件] Sa-Token 整合 Snack3 (json序列化插件) \n\t\t├── sa-token-snack4                       // [插件] Sa-Token 整合 Snack4 (json序列化插件) \n\t\t├── sa-token-hutool-timed-cache           // [插件] Sa-Token 整合 Hutool 缓存组件 Timed-Cache（基于内存） (数据缓存插件) \n\t\t├── sa-token-caffeine                     // [插件] Sa-Token 整合 Caffeine 缓存组件（基于内存） (数据缓存插件) \n\t\t├── sa-token-thymeleaf                    // [插件] Sa-Token 整合 Thymeleaf (自定义标签方言) \n\t\t├── sa-token-freemarker                   // [插件] Sa-Token 整合 Freemarker (自定义标签方言) \n\t\t├── sa-token-dubbo                        // [插件] Sa-Token 整合 Dubbo (RPC 调用鉴权、状态传递) \n\t\t├── sa-token-dubbo3                       // [插件] Sa-Token 整合 Dubbo3 (RPC 调用鉴权、状态传递) \n\t\t├── sa-token-temp-jwt                     // [插件] Sa-Token 整合 jjwt (临时 Token) \n\t\t├── sa-token-jwt                          // [插件] Sa-Token 整合 jjwt (JWT 登录认证) \n\t\t├── sa-token-sso                          // [插件] Sa-Token 实现 SSO 单点登录\n\t\t├── sa-token-oauth2                       // [插件] Sa-Token 实现 OAuth2.0 认证\n\t\t├── sa-token-apikey                       // [插件] Sa-Token 实现 API Key 认证\n\t\t├── sa-token-sign                         // [插件] Sa-Token 实现 API 参数签名  \n\t\t├── sa-token-redisson                     // [插件] Sa-Token 整合 Redisson (数据缓存插件) \n\t\t├── sa-token-redisx                       // [插件] Sa-Token 整合 Redisx (数据缓存插件) \n\t\t├── sa-token-serializer-features          // [插件] Sa-Token 序列化实现扩展 \n\t\t├── sa-token-redis-template               // [插件] Sa-Token 整合 RedisTemplate (数据缓存插件) \n\t\t├── sa-token-redis-template-jdk-serializer // [插件] Sa-Token 整合 RedisTemplate - 使用 jdk 序列化算法 (数据缓存插件) \n\t\t├── sa-token-redis-jackson                // [插件] Sa-Token 整合 RedisTemplate - 使用 Jackson 序列化算法 (数据缓存插件) \n\t\t├── sa-token-alone-redis                  // [插件] Sa-Token 独立 Redis 插件，实现 [ 权限缓存与业务缓存分离 ]\n\t\t├── sa-token-spring-aop                   // [插件] Sa-Token 整合 SpringAOP 注解鉴权\n\t\t├── sa-token-spring-el                    // [插件] Sa-Token 实现 SpringEL 表达式注解鉴权\n\t\t├── sa-token-grpc                         // [插件] Sa-Token 整合 gRPC (RPC 调用鉴权、状态传递) \n\t\t├── sa-token-quick-login                  // [插件] Sa-Token 快速注入登录页插件 \n\t\t├── sa-token-redisson-spring-boot-starter // [插件] Sa-Token 整合 Redisson - SpringBoot 自动配置包 (数据缓存插件) \n\t\t├── sa-token-forest                       // [插件] Sa-Token 整合 Forest，http 请求处理器 \n\t\t├── sa-token-okhttps                      // [插件] Sa-Token 整合 OkHttps，http 请求处理器 \n\t├── sa-token-demo                         // [示例] Sa-Token 示例合集\n\t\t├── sa-token-demo-alone-redis             // [示例] Sa-Token 集成 alone-redis 模块\n\t\t├── sa-token-demo-alone-redis-cluster     // [示例] Sa-Token 集成 alone-redis 模块、集群模式\n\t\t├── sa-token-demo-apikey                  // [示例] Sa-Token API Key 模块示例\n\t\t├── sa-token-demo-async                   // [示例] Sa-Token 异步场景示例\n\t\t├── sa-token-demo-beetl                   // [示例] Sa-Token 集成 beetl 示例\n\t\t├── sa-token-demo-bom-import              // [示例] Sa-Token bom 包导入示例 \n\t\t├── sa-token-demo-case                    // [示例] Sa-Token 各模块示例\n\t\t├── sa-token-demo-device-lock             // [示例] Sa-Token 设备锁登录示例 - 后端\n\t\t├── sa-token-demo-device-lock-h5          // [示例] Sa-Token 设备锁登录示例 - 前端 \n\t\t├── sa-token-demo-dubbo                   // [示例] Sa-Token 集成 dubbo\n\t\t\t├── sa-token-demo-dubbo-consumer          // [示例] Sa-Token 集成 dubbo 鉴权，消费端（调用端）\n\t\t\t├── sa-token-demo-dubbo-provider          // [示例] Sa-Token 集成 dubbo 鉴权，生产端（被调用端）\n\t\t\t├── sa-token-demo-dubbo3-consumer         // [示例] Sa-Token 集成 dubbo3 鉴权，消费端（调用端）\n\t\t\t├── sa-token-demo-dubbo3-provider         // [示例] Sa-Token 集成 dubbo3 鉴权，生产端（被调用端）\n\t\t├── sa-token-demo-freemarker              // [示例] Sa-Token 集成 Freemarker 标签方言\n\t\t├── sa-token-demo-grpc                    // [示例] Sa-Token 集成 grpc 鉴权\n\t\t\t├── client                                // [示例] Sa-Token 集成 grpc 鉴权，client 端\n\t\t\t├── server                                // [示例] Sa-Token 集成 grpc 鉴权，server 端\n\t\t├── sa-token-demo-hutool-timed-cache      // [示例] Sa-Token 集成 hutool timed-cache\n\t\t├── sa-token-demo-caffeine                // [示例] Sa-Token 集成 Caffeine\n\t\t├── sa-token-demo-jwt                     // [示例] Sa-Token 集成 jwt 登录认证 \n\t\t├── sa-token-demo-oauth2                  // [示例] Sa-Token 集成 OAuth2.0\n\t\t\t├── sa-token-demo-oauth2-client           // [示例] Sa-Token 集成 OAuth2.0 (客户端)\n\t\t\t├── sa-token-demo-oauth2-client-h5        // [示例] Sa-Token OAuth2 前端测试页 \n\t\t\t├── sa-token-demo-oauth2-server           // [示例] Sa-Token 集成 OAuth2.0 (服务端)\n\t\t\t├── sa-token-demo-oauth2-server-h5        // [示例] Sa-Token 集成 OAuth2.0 (服务端 - 前后台分离示例)\n\t\t├── sa-token-demo-quick-login             // [示例] Sa-Token 集成 quick-login 模块 \n\t\t├── sa-token-demo-quick-login-sb3         // [示例] Sa-Token 集成 quick-login 模块 (SpringBoot3)\n\t\t├── sa-token-demo-remember-me             // [示例] Sa-Token 实现 [ 记住我 ] 模式\n\t\t\t├── page_project                          // [示例] Sa-Token 实现 [ 记住我 ] 模式、前端页面\n\t\t\t├── sa-token-demo-remember-me-server      // [示例] Sa-Token 实现 [ 记住我 ] 模式、后端接口\n\t\t├── sa-token-demo-solon                   // [示例] Sa-Token 集成 Solon \n\t\t├── sa-token-demo-solon-reisson           // [示例] Sa-Token 集成 Solon、Reisson\n\t\t├── sa-token-demo-springboot              // [示例] Sa-Token 整合 SpringBoot \n\t\t├── sa-token-demo-springboot3-redis       // [示例] Sa-Token 整合 SpringBoot3 整合 Redis \n\t\t├── sa-token-demo-springboot4-redis       // [示例] Sa-Token 整合 SpringBoot4 整合 Redis \n\t\t├── sa-token-demo-springboot-low-version  // [示例] Sa-Token 整合 SpringBoot2 低版本\n\t\t├── sa-token-demo-springboot-redis        // [示例] Sa-Token 整合 SpringBoot 整合 Redis \n\t\t├── sa-token-demo-springboot-redisson     // [示例] Sa-Token 整合 SpringBoot 整合 redisson \n\t\t├── sa-token-demo-sse                     // [示例] 在 SSE 中使用 Sa-Token \n\t\t├── sa-token-demo-ssm                     // [示例] 在 SSM 中使用 Sa-Token \n\t\t├── sa-token-demo-sso                     // [示例] Sa-Token 集成 SSO 单点登录\n\t\t\t├── sa-token-demo-sso-server              // [示例] Sa-Token 集成 SSO单点登录-Server认证中心\n\t\t\t├── sa-token-demo-sso1-client             // [示例] Sa-Token 集成 SSO单点登录-模式一 应用端 (同域、同Redis)\n\t\t\t├── sa-token-demo-sso2-client             // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 (跨域、同Redis)\n\t\t\t├── sa-token-demo-sso3-client             // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (跨域、跨Redis)\n\t\t\t├── sa-token-demo-sso3-client-nosdk       // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (不使用sdk，纯手动对接)\n\t\t\t├── sa-token-demo-sso3-client-resdk       // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (ReSdk 模式，重写部分方法对接任意技术栈)\n\t\t\t├── sa-token-demo-sso3-client-anon        // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (匿名应用接入示例)\n\t\t\t├── sa-token-demo-sso-server-h5           // [示例] Sa-Token 集成 SSO单点登录-Server认证中心 (前后端分离)\n\t\t\t├── sa-token-demo-sso-client-h5           // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-原生h5 版本)\n\t\t\t├── sa-token-demo-sso-server-vue2         // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-Vue2 版本)\n\t\t\t├── sa-token-demo-sso-client-vue3         // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离-Vue3 版本)\n\t\t├── sa-token-demo-sso-for-solon           // [示例] Sa-Token 集成 SSO 单点登录（Solon 版）\n\t\t\t├── sa-token-demo-sso-server-solon        // [示例] Sa-Token 集成 SSO单点登录-Server认证中心\n\t\t\t├── sa-token-demo-sso1-client-solon       // [示例] Sa-Token 集成 SSO单点登录-模式一 应用端 (同域、同Redis)\n\t\t\t├── sa-token-demo-sso2-client-solon       // [示例] Sa-Token 集成 SSO单点登录-模式二 应用端 (跨域、同Redis)\n\t\t\t├── sa-token-demo-sso3-client-solon       // [示例] Sa-Token 集成 SSO单点登录-模式三 应用端 (跨域、跨Redis)\n\t\t├── sa-token-demo-test                    // [示例] Sa-Token 整合测试项目\n\t\t├── sa-token-demo-thymeleaf               // [示例] Sa-Token 集成 Thymeleaf 标签方言\n\t\t├── sa-token-demo-webflux                 // [示例] Sa-Token 整合 WebFlux \n\t\t├── sa-token-demo-webflux-springboot3    // [示例] Sa-Token 整合 WebFlux （SpringBoot3）\n\t\t├── sa-token-demo-webflux-springboot4    // [示例] Sa-Token 整合 WebFlux （SpringBoot4）\n\t\t├── sa-token-demo-websocket               // [示例] Sa-Token 集成 Web-Socket 鉴权示例\n\t\t├── sa-token-demo-websocket-spring        // [示例] Sa-Token 集成 Web-Socket（Spring封装版） 鉴权示例\n\t\t├── sa-token-demo-loveqq-boot             // [示例] Sa-Token 集成 LoveQQ-Boot\n\t\t├── pom.xml                               // 示例 pom 文件，用于帮助在 idea 中一键导入所有 demo \n\t├── sa-token-test                         // [测试] Sa-Token 单元测试合集\n\t\t├── sa-token-easy-test                  // [测试] Sa-Token 简易测试\n\t\t├── sa-token-springboot-test            // [测试] Sa-Token SpringBoot 整合测试\n\t\t├── sa-token-jwt-test                   // [测试] Sa-Token jwt 整合测试\n\t\t├── sa-token-temp-jwt-test              // [测试] Sa-Token temp-jwt 整合测试\n\t\t├── sa-token-json-test                  // [测试] Sa-Token json 序列化测试\n\t\t├── sa-token-jackson3-test              // [测试] Sa-Token Jackson3 整合测试\n\t\t├── sa-token-serializer-test            // [测试] Sa-Token 序列化测试\n\t├── sa-token-doc                          // [文档] Sa-Token 开发文档 \n\t├── MEMO                                  // [备忘] 内部备忘录、开发记录\n\t├── pom.xml                               // [依赖] 顶级pom文件 \n\t├── LICENSE                               // 开源协议\n\t├── mvn clean.bat                         // 一键 mvn clean 核心包+所有示例包\n\t├── mvn test.bat                          // 一键单元测试 \n\t├── preview-doc.bat                       // 一键预览开发文档\n\t├── README.md                             // 仓库自述文件 \n```\n\n\n\n其它（[sa-tokens](https://gitee.com/sa-tokens) 组织下相关仓库）：\n\n- [Awesome-Sa-Token](https://gitee.com/sa-tokens/awesome-sa-token)：集成 Sa-Token 的优秀开源案例收集。\n- [sa-token-rust](https://gitee.com/sa-tokens/sa-token-rust)：Sa-Token 的 Rust 版本，轻量级 Rust 权限认证框架。\n- [sa-token-go](https://gitee.com/sa-tokens/sa-token-go)：Sa-Token 的 Go 版本，轻量级 Go 权限认证框架。\n- [Sa-Token-Study](https://gitee.com/sa-tokens/Sa-Token-Study)：Sa-Token 涉及技术点学习笔记与实战。\n- [Sa-Token-Login-Demos](https://gitee.com/sa-tokens/Sa-Token-Login-Demos)：各种登录方式示例集合，一站式学习 Sa-Token 登录认证。\n- [sa-token-doc-big-file](https://gitee.com/sa-tokens/sa-token-doc-big-file)：sa-token-doc 文档中的图片资源文件。\n- [sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin)：Sa-Token 第三方插件合集。\n- [sa-token-demo-cross](https://gitee.com/sa-tokens/sa-token-demo-cross)：Sa-Token 处理跨域场景示例。\n- [auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test)：Java 权限认证框架功能 测试 / 对比 / 迁移。\n"
  },
  {
    "path": "sa-token-doc/doc/index-backup.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<meta charset=\"UTF-8\">\n\t\t<title>Sa-Token</title>\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n\t\t<meta name=\"description\" content=\"Sa-Token是一个java权限认证框架，功能全面，上手简单，登录认证、权限认证、Session会话、踢人下线、账号封禁、集成Redis、前后端分离、分布式会话、微服务网关鉴权、单点登录、OAuth2.0、临时Token验证、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成、WebFlux集成...，有了sa-token，你所有的权限认证问题，都不再是问题\">\n\t\t<meta name=\"keywords\" content=\"sa-token,sa-token框架,sa-token文档,java权限认证\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n\t\t<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"../logo.png\">\n\t</head>\n\t<body>\n\t\t<div>\n\t\t\t<h3 style=\"margin-top: 5vh; text-align: center;\">\n\t\t\t\tSa-Token 文档已迁移，请打开：<a class=\"new-link\" href=\"../doc.html\">最新地址</a>\n\t\t\t</h3>\n\t\t</div>\n\t\t<script>\n\t\t\tdocument.querySelector('.new-link').setAttribute('href', '../doc.html' + location.hash);\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-doc/doc/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<meta charset=\"UTF-8\">\n\t\t<title>Sa-Token</title>\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n\t\t<meta name=\"description\" content=\"Sa-Token是一个java权限认证框架，功能全面，上手简单，登录认证、权限认证、Session会话、踢人下线、账号封禁、集成Redis、前后端分离、分布式会话、微服务网关鉴权、单点登录、OAuth2.0、临时Token验证、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成、WebFlux集成...，有了sa-token，你所有的权限认证问题，都不再是问题\">\n\t\t<meta name=\"keywords\" content=\"sa-token,sa-token框架,sa-token文档,java权限认证\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n\t\t<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"../logo.png\">\n\t</head>\n\t<body>\n\t\t<div>\n\t\t\t<h3 style=\"margin-top: 5vh; text-align: center;\">\n\t\t\t\tSa-Token 文档已迁移，请打开：<a class=\"new-link\" href=\"../doc.html\">最新地址</a>\n\t\t\t</h3>\n\t\t</div>\n\t\t<script>\n\t\t\tdocument.querySelector('.new-link').setAttribute('href', '../doc.html' + location.hash);\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-doc/doc.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<meta charset=\"UTF-8\">\n\t\t<title>Sa-Token</title>\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n\t\t<meta name=\"description\"\n\t\t\tcontent=\"Sa-Token是一个java权限认证框架，功能全面，上手简单，登录认证、权限认证、Session会话、踢人下线、账号封禁、集成Redis、前后端分离、分布式会话、微服务网关鉴权、单点登录、OAuth2.0、临时Token验证、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成、WebFlux集成...，有了sa-token，你所有的权限认证问题，都不再是问题\">\n\t\t<meta name=\"keywords\" content=\"sa-token,sa-token框架,sa-token文档,java权限认证\">\n\t\t<meta name=\"viewport\"\n\t\t\tcontent=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n\t\t<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"logo.png\">\n\t\t<link rel=\"stylesheet\" href=\"./static/doc.css\">\n\t\t<link rel=\"stylesheet\" href=\"./static/vue.css\">\n\t</head>\n\t<body>\n\t\t<div class=\"doc-header\">\n\t\t\t<div class=\"nav-left\">\n\t\t\t\t<a href=\"doc.html\">\n\t\t\t\t\t<div class=\"logo-box\">\n\t\t\t\t\t\t<img src=\"logo.png\" title=\"logo\" />\n\t\t\t\t\t\t<h1 class=\"logo-text\">Sa-Token</h1>\n\t\t\t\t\t\t<sub>v1.45.0</sub>\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t\t<nav class=\"nav-right\">\n\t\t\t\t<div class=\"sear-box p-none\" tabindex=\"-1\">\n\t\t\t\t\t<!-- 加载中…… -->\n\t\t\t\t</div>\n\t\t\t\t<select class=\"select-version p-none\" onchange=\"location.href=this.value\">\n\t\t\t\t\t<option value=\"doc.html\">最新版</option>\n\t\t\t\t\t<option value=\"v/v1.44.0/doc.html\">v1.44.0</option>\n\t\t\t\t\t<option value=\"v/v1.43.0/doc.html\">v1.43.0</option>\n\t\t\t\t\t<option value=\"v/v1.42.0/doc.html\">v1.42.0</option>\n\t\t\t\t\t<option value=\"v/v1.41.0/doc.html\">v1.41.0</option>\n\t\t\t\t\t<option value=\"v/v1.40.0/doc.html\">v1.40.0</option>\n\t\t\t\t\t<option value=\"v/v1.39.0/doc.html\">v1.39.0</option>\n\t\t\t\t\t<option value=\"v/v1.38.0/doc.html\">v1.38.0</option>\n\t\t\t\t\t<option value=\"v/v1.37.0/doc.html\">v1.37.0</option>\n\t\t\t\t\t<option value=\"v/v1.36.0/doc.html\">v1.36.0</option>\n\t\t\t\t\t<option value=\"v/v1.35.0/doc.html\">v1.35.0</option>\n\t\t\t\t\t<option value=\"v/v1.34.0/doc.html\">v1.34.0</option>\n\t\t\t\t\t<option value=\"v/v1.33.0/doc.html\">v1.33.0</option>\n\t\t\t\t\t<option value=\"v/v1.32.0/doc.html\">v1.32.0</option>\n\t\t\t\t\t<option value=\"v/v1.31.0/doc.html\">v1.31.0</option>\n\t\t\t\t\t<option value=\"v/v1.30.0/doc/index.html\">v1.30.0</option>\n\t\t\t\t\t<option value=\"v/v1.29.0/doc/index.html\">v1.29.0</option>\n\t\t\t\t\t<option value=\"v/v1.28.0/doc/index.html\">v1.28.0</option>\n\t\t\t\t\t<option value=\"v/v1.27.0/doc/index.html\">v1.27.0</option>\n\t\t\t\t\t<option value=\"v/v1.26.0/doc/index.html\">v1.26.0</option>\n\t\t\t\t\t<option value=\"v/v1.25.0/doc/index.html\">v1.25.0</option>\n\t\t\t\t\t<option value=\"v/v1.24.0/doc/index.html\">v1.24.0</option>\n\t\t\t\t\t<option value=\"v/v1.23.0/doc/index.html\">v1.23.0</option>\n\t\t\t\t\t<option value=\"v/v1.22.0/doc/index.html\">v1.22.0</option>\n\t\t\t\t\t<option value=\"v/v1.21.0/doc/index.html\">v1.21.0</option>\n\t\t\t\t\t<option value=\"v/v1.20.0/doc/index.html\">v1.20.0</option>\n\t\t\t\t\t<option value=\"v/v1.19.0/doc/index.html\">v1.19.0</option>\n\t\t\t\t\t<option value=\"v/v1.18.0/doc/index.html\">v1.18.0</option>\n\t\t\t\t\t<option value=\"v/v1.17.0/doc/index.html\">v1.17.0</option>\n\t\t\t\t\t<option value=\"v/v1.16.0/doc/index.html\">v1.16.0</option>\n\t\t\t\t\t<option value=\"v/v1.15.0/doc/index.html\">v1.15.0</option>\n\t\t\t\t\t<option value=\"v/v1.14.0/doc/index.html\">v1.14.0</option>\n\t\t\t\t\t<option value=\"v/v1.13.0/doc/index.html\">v1.13.0</option>\n\t\t\t\t\t<option value=\"v/v1.12.1/doc/index.html\">v1.12.1</option>\n\t\t\t\t\t<option value=\"v/v1.12.0/doc/index.html\">v1.12.0</option>\n\t\t\t\t\t<option value=\"v/v1.11.0/doc/index.html\">v1.11.0</option>\n\t\t\t\t\t<option value=\"v/v1.10.0/doc/index.html\">v1.10.0</option>\n\t\t\t\t\t<option value=\"v/v1.9.0/doc/index.html\">v1.9.0</option>\n\t\t\t\t\t<option value=\"v/v1.8.0/doc/index.html\">v1.8.0</option>\n\t\t\t\t\t<option value=\"v/v1.7.0/doc/index.html\">v1.7.0</option>\n\t\t\t\t\t<option value=\"v/v1.6.0/doc/index.html\">v1.6.0</option>\n\t\t\t\t\t<option value=\"v/v1.5.1/doc/index.html\">v1.5.1</option>\n\t\t\t\t\t<option value=\"v/v1.4.0/doc/index.html\">v1.4.0</option>\n\t\t\t\t\t<option value=\"v/v1.3.0/doc/index.html\">v1.3.0</option>\n\t\t\t\t\t<option value=\"v/v1.2.0/doc/index.html\">v1.2.0</option>\n\t\t\t\t\t<option value=\"v/v1.1.0/doc/index.html\">v1.1.0</option>\n\t\t\t\t\t<option value=\"v/v1.0.0/doc/index.html\">v1.0.0</option>\n\t\t\t\t\t<option value=\"/\">首页</option>\n\t\t\t\t</select>\n\t\t\t\t<div class=\"zk-box p-none\">\n\t\t\t\t\t<a class=\"wzi\" href=\"javascript:;\">\n\t\t\t\t\t\t<!-- <span>背景 </span> -->\n\t\t\t\t\t\t<img class=\"theme-btn\" src=\"static/icon/theme.svg\">\n\t\t\t\t\t\t<!-- <span class=\"zk-icon\"></span> -->\n\t\t\t\t\t</a>\n\t\t\t\t\t<div class=\"zk-context theme-box\">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div style=\"height: 5px;\"></div>\n\t\t\t\t\t\t\t<span style=\"background-color: #FFFFFF;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #f5f5f5;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #f5e5f5;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #F1FAFA;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #f5f5d5;\"></span>\n\n\t\t\t\t\t\t\t<span style=\"background-color: #E8E8FF;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #f0f9eb;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #d5f5f5;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #ebe5dd;\"></span>\n\t\t\t\t\t\t\t<span style=\"background-color: #e8f4ff;\"></span>\n\n\t\t\t\t\t\t\t<!-- <span style=\"background-color: #F0DAD2;\"></span> -->\n\t\t\t\t\t\t\t<!-- <span style=\"background-color: #f5d5d5;\"></span> -->\n\t\t\t\t\t\t\t<!-- <span style=\"background-color: #FFFFE0;\"></span> -->\n\t\t\t\t\t\t\t<!-- <span style=\"background-color: #eeeeee;\"></span> -->\n\t\t\t\t\t\t\t<!-- <span style=\"background-color: #f5fafe;\"></span> -->\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<a class=\"wzi\" href=\"index.html\">首页</a>\n\t\t\t\t<a class=\"wzi\" href=\"doc.html\">文档</a>\n\t\t\t\t<div class=\"zk-box\">\n\t\t\t\t\t<a class=\"wzi\" href=\"javascript:;\">\n\t\t\t\t\t\t<span>视频 </span>\n\t\t\t\t\t\t<span class=\"zk-icon\"></span>\n\t\t\t\t\t</a>\n\t\t\t\t\t<div class=\"zk-context\">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1fsUVBWEyH/\" target=\"_blank\">朱老师的小课堂（7集）</a>\n\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1NF1FBpEe6/\" target=\"_blank\">王清江唷 SSO篇（29集）</a>\n\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1uZUpYVEst/\" target=\"_blank\">fox说技术（7集）</a>\n\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1eFtRezERp?p=87\" target=\"_blank\">架构驿站（11集）</a>\n\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1Zt421u7gk/\" target=\"_blank\">王清江唷（99集）</a>\n\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1kG411o7Ms/\" target=\"_blank\">筑梦信仰-joy（20集）</a>\n\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV11u4y197JL/\" target=\"_blank\">达达-Java（26集）</a>\n\t\t\t\t\t\t\t<a href=\"https://space.bilibili.com/473679148/video\" target=\"_blank\">晒太阳的盐（22集）</a>\n\t\t\t\t\t\t\t<div class=\"zk-fengexian\"></div>\n\t\t\t\t\t\t\t<a href=\"doc.html#/more/content-cooperation\">[ + 课程提交 ]</a>\n\t\t\t\t\t\t\t<!-- <a href=\"javascript: layer.alert('如您有 Sa-Token 相关课程录制，请联系官网文档右侧 < sa-token 小助手 > 进行提交');\">\n\t\t\t\t\t\t\t\t[ + 课程提交 ]\n\t\t\t\t\t\t\t</a> -->\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<a class=\"p-none wzi\" href=\"#/more/link\">案例</a>\n\t\t\t\t<a class=\"p-none wzi\" href=\"#/more/join-group\">加入讨论群</a>\n\t\t\t\t<a class=\"p-none wzi\" href=\"#/more/demand-commit\">需求提交</a>\n\t\t\t\t<a class=\"p-none wzi\" href=\"#/more/blog\">博客</a>\n\t\t\t\t<a class=\"p-none wzi\" href=\"#/more/sa-token-donate\">赞助</a>\n\t\t\t\t<a class=\"p-none wzi\" href=\"#/pro/st_doc_top\">🔥 SSO/OAuth2 商业版</a>\n\t\t\t\t<div class=\"zk-box\">\n\t\t\t\t\t<a class=\"wzi\" href=\"javascript:;\">\n\t\t\t\t\t\t<span>相关资源 </span>\n\t\t\t\t\t\t<span class=\"zk-icon\"></span>\n\t\t\t\t\t</a>\n\t\t\t\t\t<div class=\"zk-context\">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<!-- <a href=\"#/more/sa-token-donate\">❤️ &nbsp;赞助</a> -->\n\t\t\t\t\t\t\t<a href=\"#/more/update-log\">更新日志</a>\n\t\t\t\t\t\t\t<a href=\"#/more/common-questions\">常见报错</a>\n\t\t\t\t\t\t\t<a href=\"#/more/tj-gzh\">推荐公众号</a>\n\t\t\t\t\t\t\t<a href=\"#/more/blog\">相关博客</a>\n\t\t\t\t\t\t\t<div class=\"zk-fengexian\"></div>\n\t\t\t\t\t\t\t<!-- <a href=\"http://sa-app.dev33.cn/wall.html?name=sa-token\" target=\"_blank\">需求墙</a> -->\n\t\t\t\t\t\t\t<a href=\"#/fun/sa-token-test\">在线考试</a>\n\t\t\t\t\t\t\t<a href=\"#/fun/issue-template\">在线提问</a>\n\t\t\t\t\t\t\t<!-- <a href=\"https://wj.qq.com/s2/10852322/0d8b/\" target=\"_blank\">需求提交</a> -->\n\t\t\t\t\t\t\t<a href=\"#/more/wenjuan\">问卷调查</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</nav>\n\t\t</div>\n\t\t<!-- <a href=\"/\">\n\t\t\t<div class=\"logo-box\">\n\t\t\t\t<img src=\"logo.png\" title=\"logo\" />\n\t\t\t\t<h1 class=\"logo-text\">Sa-Token</h1>\n\t\t\t</div>\n\t\t</a> -->\n\n\t\t<div class=\"main-box\">\n\t\t\t<!-- 内容区 -->\n\t\t\t<div id=\"app\">加载中...</div>\n\n\t\t\t<!-- 右边盒子 -->\n\t\t\t<div class=\"doc-right-bj-box\">\n\t\t\t\t<div class=\"doc-right-bj-box-title\">目录</div>\n\t\t\t\t<div class=\"doc-right-more-item\">\n\n\t\t\t\t\t<!-- ad盒子 -->\n\t\t\t\t\t<div class=\"ad-box\">\n\n\t\t\t\t\t\t<div class=\"ad-title\">\n\t\t\t\t\t\t\t<span class=\"ad-tips\">推广信息：</span>\n\t\t\t\t\t\t\t<span class=\"ad-tips ad-close\">关闭</span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<!-- ssp -->\n\t\t\t\t\t\t<div class=\"top-ad-box\" style=\"margin-bottom: 12px;\">\n\t\t\t\t\t\t\t<a href=\"https://sa-pro.yun94.cn?way=st_r\" target=\"_blank\">\n\t\t\t\t\t\t\t\t<div class=\"mad-bg-box\">\n\t\t\t\t\t\t\t\t\t<div class=\"mad-context-box\">\n\t\t\t\t\t\t\t\t\t\t<img class=\"mad-img\" src=\"/big-file/contact/sspx-ad-11.png\" />\n\t\t\t\t\t\t\t\t\t\t<span class=\"mad-text\">\n\t\t\t\t\t\t\t\t\t\t\t<b>轻松搭建：SSO 单点登录、OAuth2.0 统一认证、API Key、用户数据同步。全源码交付、可二开。</b>\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<!-- 万维广告div -->\n\t\t\t\t\t\t<div class=\"wwads-cn wwads-horizontal\" data-id=\"88\"\n\t\t\t\t\t\t\tstyle=\"min-height: 0px; border: 1px #eee solid; margin-bottom: 12px;\"></div>\n\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<!-- help 按钮 -->\n\t\t\t\t\t<!-- <div class=\"help-btn\">❤️ 技术求助</div> -->\n\t\t\t\t\t<!-- ew-wa -->\n\t\t\t\t\t<div class=\"ew-wa\">\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<a href=\"https://pan.quark.cn/s/d5abda720e88\" target=\"_blank\">离线版文档</a>\n\t\t\t\t\t\t\t<a href=\"https://pan.quark.cn/s/fea7e5ec72ee\" target=\"_blank\">历史所有版本文档</a>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- ew-wa -->\n\t\t\t\t\t<div class=\"ew-wa\">\n\t\t\t\t\t\t<p>如果 Sa-Token 帮助到了你，希望你可以向同事、朋友推荐了解本框架，这对我们非常重要，感谢支持！ <!-- 🤗 --> </p>\n\t\t\t\t\t\t<p>加油，工程师！</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- 万维广告div -->\n\t\t<!-- <div style=\"position: fixed; right: 0; bottom: 0; z-index: 10000; border: 0px #aaa solid;\">\n\t\t\t<div class=\"wwads-cn wwads-vertical\" data-id=\"88\" style=\"max-width:150px\"></div>\n\t\t</div> -->\n\n\t\t<!-- 小助手div -->\n\t\t<!-- <div class=\"p-none help-btn-box\" style=\"position: fixed; right: 40px; bottom: 330px; z-index: 10000; border: 0px #aaa solid;\">\n\t\t\t<div class=\"help-tips\" style=\"position: relative; left: -30px; top: -10px;\"></div>\n\t\t\t<div class=\"help-btn\" style=\"width: 60px; height: 60px; text-align: center; border-radius: 50%; background-color: #42b983; cursor: pointer;\">\n\t\t\t\t<span style=\"font-size: 18px; color: #FFF; line-height: 60px;\">Help</span>\n\t\t\t</div>\n\t\t</div> -->\n\n\t\t<!-- UI逐渐显现 -->\n\t\t<style type=\"text/css\">\n\t\t\tbody {\n\t\t\t\topacity: 0.01;\n\t\t\t\ttransition: opacity 0.5s;\n\t\t\t\tbackground-color: #FFF;\n\t\t\t}\n\t\t</style>\n\t\t<script type=\"text/javascript\">\n\t\t\tsetTimeout(function() {\n\t\t\t\tdocument.body.style.opacity = 1;\n\t\t\t}, 1);\n\t\t</script>\n\n\t\t<!-- jqeury -->\n\t\t<script src=\"static/jquery.min.js\"></script>\n\t\t<script src=\"static/layer-v3.1.1/layer.js\"></script>\n\n\t\t<!--  -->\n\t\t<script src=\"./static/docsify-plugin.js?v=7\"></script>\n\t\t<script src=\"./static/is-star-plugin.js?v=7\"></script>\n\t\t<!-- <script src=\"./static/is-fill-in-wj-plugin.js?v=7\"></script> -->\n\t\t<link rel=\"stylesheet\" href=\"./static/custom-docsify-plugins/doc-lock-plugin.css\">\n\t\t<!-- <script src=\"./static/custom-docsify-plugins/doc-lock-plugin.js\"></script> -->\n\t\t<!-- <script src=\"./static/custom-docsify-plugins/doc-lock-by-gzh-plugin.js\"></script> -->\n\t\t<script>\n\t\t\tvar saTokenTopVersion = '1.45.0'; // Sa-Token最新版本 \n\t\t\tvar name = '<img style=\"width: 60px; height: 60px; vertical-align: middle;\" src=\"logo.png\" alt=\"logo\" /> ';\n\t\t\tname += '<b style=\"font-size: 28px; vertical-align: middle;\">Sa-Token</b> <sub>v' + saTokenTopVersion + '</sub>';\n\t\t\twindow.$docsify = {\n\t\t\t\t// name: name, // 名字 \n\t\t\t\trepo: 'https://github.com/dromara/sa-token', // github地址 \n\t\t\t\t// themeColor: '#06A3D7', // 主题颜色  \n\t\t\t\tbasePath: location.pathname.substr(0, location.pathname.lastIndexOf('/') + 1), // 自动计算项目名字 \n\t\t\t\t// basePath: '/sa-token-doc/',\t\t// 设置文件加载的父路径, 这在一些带项目名部署的文件中非常有效\n\t\t\t\tauto2top: true, // 是否在切换页面后回到顶部 \n\t\t\t\t// coverpage: true, // 开启封面 \n\t\t\t\tsubMaxLevel: 4, // 标题解析层级, 写几就在目录树中解析到几级标题 ,一般写2吧也就 \n\t\t\t\tloadSidebar: true, // 加载自定义侧边栏 , 目录定制在: _sidebar.md 文件 (需要创建 .nojekyll 的空文件，阻止 GitHub Pages 忽略命名是下划线开头的文件)\n\t\t\t\tcopyCode: { // 复制插件 \n\t\t\t\t\tbuttonText: '复制到剪贴板',\n\t\t\t\t\terrorText: '错误',\n\t\t\t\t\tsuccessText: '复制成功'\n\t\t\t\t},\n\t\t\t\ttopMargin: 90, // 锚点距离顶部的距离 \n\t\t\t\t// sidebarDisplayLevel : 1 ,  // 设置侧边栏显示级别\n\t\t\t\t// search: 'auto', // 搜索功能 \n\t\t\t\talias: {\n\t\t\t\t\t// '/sso/_sidebar.md': '/sso/_sidebar.md',\n\t\t\t\t\t'/.*/_sidebar.md': '/_sidebar.md'\n\t\t\t\t},\n\t\t\t\t// tab选项卡\n\t\t\t\ttabs: {\n\t\t\t\t\tpersist: true, // 是否在刷新页面时重置选项卡\n\t\t\t\t\tsync: false, // 页面上的多个tab是否同步切换\n\t\t\t\t\ttheme: 'classic', // 主题：'classic', 'material', false\n\t\t\t\t\ttabComments: true, // 用注释来标注选项卡标题，例如：<!-- tab:SpringBoot  -->\n\t\t\t\t\ttabHeadings: true // 用标题+粗体来定制选项卡\n\t\t\t\t},\n\t\t\t\t// 阅读进度\n\t\t\t\tprogress: {\n\t\t\t\t\tposition: \"top\",\n\t\t\t\t\tcolor: \"var(--theme-color,#42b983)\",\n\t\t\t\t\theight: \"3px\",\n\t\t\t\t},\n\t\t\t\t// 信息提示框\n\t\t\t\t'flexible-alerts': {\n\t\t\t\t\tstyle: 'flat', // 默认风格 callout=浅色，flat=深色 \n\t\t\t\t\tnote: {\n\t\t\t\t\t\tlabel: {}\n\t\t\t\t\t},\n\t\t\t\t\ttip: {\n\t\t\t\t\t\tlabel: {},\n\t\t\t\t\t},\n\t\t\t\t\twarning: {\n\t\t\t\t\t\tlabel: {}\n\t\t\t\t\t},\n\t\t\t\t\tattention: {\n\t\t\t\t\t\tlabel: {}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// 自定义插件 \n\t\t\t\tplugins: [ \n\t\t\t\t\tmyDocsifyPlugin,         // 基础插件  \n\t\t\t\t\t// window.isStarPlugin,     // 是否 star \n\t\t\t\t\t// window.isFillInWjPlugin  // 问卷填写 \n\t\t\t\t\t// window.docLockPlugin,  // 章节锁 \n\t\t\t\t],\n\t\t\t}\n\t\t</script>\n\t\t<script src=\"static/docsify.min.js\"></script>\n\t\t<script src=\"static/docsify-copy-code.min.js\"></script>\n\n\t\t<!-- 语言合集：https://cdn.jsdelivr.net/npm/prismjs@1/components/ -->\n\t\t<script src=\"static/prism/prism-java.min.js\"></script>\n\t\t<script src=\"static/prism/prism-gradle.min.js\"></script>\n\t\t<script src=\"static/prism/prism-yaml.min.js\"></script>\n\t\t<script src=\"static/prism/prism-properties.min.js\"></script>\n\n\t\t<!-- 文档阅读进度条 -->\n\t\t<!-- <script src=\"static/docsify-plugins/progress.update.js\"></script> -->\n\n\t\t<!-- 右上角次级导航栏 -->\n\t\t<script src=\"static/docsify-plugins/sub-nav-draw.js\"></script>\n\n\t\t<!-- 搜索框 -->\n\t\t<script src=\"static/search.min.js\"></script>\n\t\t<!-- 多 tab 切换 -->\n\t\t<script src=\"static/docsify-tabs.min.js\"></script>\n\t\t<!-- img点击放大 -->\n\t\t<script src=\"static/zoom-image.min.js\"></script>\n\t\t<!-- 好看的提示框 -->\n\t\t<script src=\"static/docsify-plugins/docsify-plugin-flexible-alerts.min-1.1.1.js\"></script>\n\n\t\t<!-- docsify 里一个 md 引入另一个 md-->\n\t\t<script src=\"static/docsify-plugins/docsify-betterembed-1.1.1.js\"></script>\n\n\t\t<!-- sidebar折叠 -->\n\t\t<!-- <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/docsify-sidebar-collapse/dist/sidebar.min.css\" />\n\t\t<script src=\"https://cdn.jsdelivr.net/npm/docsify-sidebar-collapse/dist/docsify-sidebar-collapse.min.js\"></script> -->\n\n\n\t\t<!-- 渲染赞助数据 -->\n\t\t<script src=\"static/donate/donate-list.js\"></script>\n\t\t<script src=\"static/donate/donate-fun.js\"></script>\n\n\t\t<!-- 广告盒子 -->\n\t\t<script>\n\t\t\t(function() {\n\t\t\t\t// 功能6：标题下面的广告\n\t\t\t\tif ($(window).width() >= 800) {\n\t\t\t\t\t// 如果一天内用户点击过关闭广告，则不再展现\n\t\t\t\t\tlet allowJg = 1000 * 60 * 60 * 24 * 1;\n\t\t\t\t\t// allowJg = 1000 * 10;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst closeAdTime = localStorage.closeAdTime;\n\t\t\t\t\t\tif (closeAdTime) {\n\t\t\t\t\t\t\t// 点击广告关闭的时间，和当前时间的差距\n\t\t\t\t\t\t\tconst closeAdJg = new Date().getTime() - parseInt(closeAdTime);\n\n\t\t\t\t\t\t\t// 差距小于1天，不再展示 \n\t\t\t\t\t\t\tif (closeAdJg < allowJg) {\n\t\t\t\t\t\t\t\tconsole.log('not show ad ...');\n\t\t\t\t\t\t\t\t$('.ad-box').remove();\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tconsole.error(e);\n\t\t\t\t\t}\n\n\t\t\t\t\t// 添加关闭事件\n\t\t\t\t\t$('.ad-close').click(function() {\n\t\t\t\t\t\tconsole.log('关闭广告');\n\t\t\t\t\t\t// $('.top-ad-box').slideUp(); // 折叠收起\n\t\t\t\t\t\tlayer.confirm('关闭后，一天内不再展现此信息', function() {\n\t\t\t\t\t\t\t$(\".ad-box\").fadeOut(1000); // 淡出效果\n\t\t\t\t\t\t\tlayer.msg('关闭成功');\n\t\t\t\t\t\t\tlocalStorage.closeAdTime = new Date().getTime();\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})();\n\t\t</script>\n\n\t\t<!-- 搜索引擎自动提交 -->\n\t\t<script>\n\t\t\t(function() {\n\t\t\t\tvar bp = document.createElement('script');\n\t\t\t\tvar curProtocol = window.location.protocol.split(':')[0];\n\t\t\t\tif (curProtocol === 'https') {\n\t\t\t\t\tbp.src = 'https://zz.bdstatic.com/linksubmit/push.js';\n\t\t\t\t} else {\n\t\t\t\t\tbp.src = 'http://push.zhanzhang.baidu.com/push.js';\n\t\t\t\t}\n\t\t\t\tvar s = document.getElementsByTagName(\"script\")[0];\n\t\t\t\ts.parentNode.insertBefore(bp, s);\n\t\t\t})();\n\t\t</script>\n\n\t\t<!-- 万维广告 -->\n\t\t<script data-mode=\"hash\" type=\"text/javascript\" src=\"https://cdn.wwads.cn/js/makemoney.js\" async></script>\n\n\t\t<!-- 百度统计 -->\n\t\t<script>\n\t\t\tvar _hmt = _hmt || [];\n\t\t\t(function() {\n\t\t\t\tvar hm = document.createElement(\"script\");\n\t\t\t\thm.src = \"https://hm.baidu.com/hm.js?35ad501304eae758ac6139a22a9830f5\";\n\t\t\t\tvar s = document.getElementsByTagName(\"script\")[0];\n\t\t\t\ts.parentNode.insertBefore(hm, s);\n\t\t\t})();\n\t\t</script>\n\n\t\t<script type=\"text/javascript\">\n\t\t\t// 预览版提示 \n\t\t\tif (location.host === 'rc.sa-token.cc') {\n\t\t\t\tconst newTips =\n\t\t\t\t\t'<b>当前文档为RC预览版文档，仅做学习测试使用，正式项目请使用正式版：<a href=\"https://sa-token.cc/\" target=\"_blank\">https://sa-token.cc/</a></b>';\n\t\t\t\tlayer.alert(newTips);\n\t\t\t}\n\t\t</script>\n\n\t\t<!-- 小助手提示 -->\n\t\t<script>\n\t\t\t$('.help-btn').click(function() {\n\t\t\t\tvar str = `\n\t\t\t\t\t<div class=\"xiaozhushou-intro\">\n\t\t\t\t\t\t<h2>报错了？搞不懂？别急、莫慌</h2>\n\t\t\t\t\t\t<div style=\"margin-top: 20px; color: green;\">\n\t\t\t\t\t\t\t<p>👉 你的问题可能很多人都碰到过了！这有一份高频报错速查文档：<a href=\"doc.html#/more/common-questions\" onclick=\"layer.closeAll()\">常见问题排查</a></p>\n\t\t\t\t\t\t\t<p>👉 几乎每个功能点都有对应的最简示例 Demo，或许可以给你一份参考：<a href=\"https://gitee.com/dromara/sa-token/tree/master/sa-token-demo\" target=\"_blank\" >sa-token-demo</a></p>\n\t\t\t\t\t\t\t<p>👉 复杂功能玩不转？来看看这些优秀开源案例是怎么集成 Sa-Token 的：<a href=\"https://gitee.com/sa-tokens/awesome-sa-token\" target=\"_blank\" >awesome-sa-token</a></p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div style=\"margin-top: 20px; color: red;\">上述方案没有解决你的问题？那你可以来“麻烦”一下我们的小助手了：</div>\n\t\t\t\t\t\t<p>1、你在使用 Sa-Token 时遇到任何技术难题，可以向 < sa-token 小助手 > 求助咨询。</p>\n\t\t\t\t\t\t<p>2、该小助手不属于商业运营，求助咨询完全免费。</p>\n\t\t\t\t\t\t<p>3、目前该小助手属于试运营阶段，每天只能提供大约 1 小时的求助时间。</p>\n\t\t\t\t\t\t<p>4、根据运营效果反馈，我们日后可能会提高求助时间，但也可能关闭此功能。</p>\n\t\t\t\t\t\t<p>5、该小助手由企业微信提供平台支持，感谢企业微信。</p>\n\t\t\t\t\t\t<p>6、不是 AI 是真人，不是 AI 是真人，不是 AI 是真人，重说三！</p>\n\t\t\t\t\t\t<p style=\"margin-top: 30px;\">打开方式：</p>\n\t\t\t\t\t\t<p>1、如果你是使用 PC 端微信，请点此链接：<a href=\"https://work.weixin.qq.com/kfid/kfcdd45c432fee9655f\" target=\"_blank\">https://work.weixin.qq.com/kfid/kfcdd45c432fee9655f</a></p>\n\t\t\t\t\t\t<p>2、如果你是使用手机端微信，请扫码：</p>\n\t\t\t\t\t\t<p><img src=\"/big-file/contact/sa-token-xiaozhushou.jpg\" width=\"200px\"></p>\n\t\t\t\t\t\t<p>如果您的问题已解决，我们希望您能够花费一点时间将解决方案发布在：<a href=\"https://gitee.com/dromara/sa-token/issues/I9I9CY\" target=\"_blank\">踩坑记录征集</a>，帮助以后遇到同样问题的开发者快速排查，感激不尽！🌹🌹🌹</p>\n\t\t\t\t\t</div>\n\t\t\t\t`;\n\t\t\t\tlayer.alert(str, {\n\t\t\t\t\ttitle: '技术求助',\n\t\t\t\t\tarea: '680px',\n\t\t\t\t\toffset: '7%',\n\t\t\t\t})\n\t\t\t})\n\t\t\t// setTimeout(function(){\n\t\t\t// \ttry{\n\t\t\t// \t\t// 给个小提示\n\t\t\t// \t\tconst index = layer.tips('框架技术支持，点此求助', '.help-btn', {\n\t\t\t// \t\t\ttips: [1, '#000'] ,//还可配置颜色\n\t\t\t// \t\t\t// time: 5000,\n\t\t\t// \t\t});\n\t\t\t// \t\t// 改为 fixed 定位，否则它会随着滚动条移动，样式就跑偏了\n\t\t\t// \t\t$('#layui-layer' + index).css('position', 'fixed');\n\t\t\t// \t}catch(e){\n\t\t\t// \t\tconsole.error(e);\n\t\t\t// \t}\n\t\t\t// }, 500)\n\t\t</script>\n\n\t\t<!-- 修改背景颜色 水滴波纹特效 -->\n\t\t<link rel=\"stylesheet\" href=\"static/water-change-theme/water-change-theme.css\" />\n\t\t<script src=\"static/water-change-theme/gsap-3.12.2.min.js\"></script>\n\t\t<script src=\"static/water-change-theme/water-change-theme.js\"></script>\n\t\t\n\n\t\t<!-- 赞助页的展开和收缩 -->\n\t\t<script>\n\t\t\t// 展开\n\t\t\tfunction expandZanZhu() {\n\t\t\t\t$('.zk-btn--1').hide();\n\t\t\t\t$('.zk-btn--2').show();\n\t\t\t\t$('.zanzhu-box').height($('.zanzhu-box table').height());\n\t\t\t}\n\t\t\t// 折叠\n\t\t\tfunction foldZanZhu() {\n\t\t\t\t$('.zanzhu-box').height(500);\n\t\t\t\t$('.zk-btn--2').hide();\n\t\t\t\t$('.zk-btn--1').show();\n\t\t\t}\n\t\t</script>\n\n\t\t<!-- 自定义滚动条颜色 -->\n\t\t<!-- <style>\n\t\t    /* 自定义body滚动条样式 */\n\t\t    body::-webkit-scrollbar { width: 8px; }\n\t\t\t/* 滚动条颜色 */\n\t\t    body::-webkit-scrollbar-thumb { background-color: green; border-radius: 3px; }\n\t\t\t/* 滚动条上面的和下面的颜色 */\n\t\t    body::-webkit-scrollbar-track {\n\t\t        background: linear-gradient(to bottom, \n\t\t            #42B983 0%, \n\t\t            #42B983 var(--scroll-progress, 0%), \n\t\t            #FCFCFC var(--scroll-progress, 0%), \n\t\t            #FCFCFC 100%);\n\t\t    }\n\t\t</style>\n\t\t<script>\n\t\t    // 动态更新滚动条颜色\n\t\t    window.addEventListener('scroll', function() {\n\t\t        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;\n\t\t        const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;\n\t\t        const scrollProgress = (scrollTop / scrollHeight) * 100;\n\t\t        \n\t\t        document.body.style.setProperty('--scroll-progress', scrollProgress + '%');\n\t\t    });\n\t\t    // 初始化滚动条状态\n\t\t    window.dispatchEvent(new Event('scroll'));\n\t\t</script> -->\n\n\n\t</body>\n</html>"
  },
  {
    "path": "sa-token-doc/fun/async--mock.md",
    "content": "# 异步 & Mock 上下文\n\n### 异步上下文\n\n有一些方法（例如`StpUtil.isLogin()`）只可以在同步的 Web 上下文中才可以调用，如果在异步上下文中调用则会抛出异常：\n\n``` text\ncn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化\n```\n\n这是因为这些方法需要从前端的 `HttpServletRequest` 中读取 Token 参数，而异步上下文通常不是一次“请求”，不具有 `HttpServletRequest` 的概念，所以无法成功调用。\n\n一般哪些场景属于异步上下文？\n- 通过 `new Thread(() -> { ... }).start()` 启动子线程。\n- 通过 `taskExecutor.execute(() -> { ... })` 线程池启动异步任务。\n- 通过 `@Async` 注解标注的方法。\n- 通过 `@Scheduled(cron = \"\")` 启动的定时任务。\n- 消息队列中消费消息的函数。\n- ...\n\n凡是不通过 web 请求调用触发的线程，在 Sa-Token 中均属于异步上下文，也可以称作 “非 Web 上下文”。\n\n此时调用 `StpUtil.isLogin()`、`StpUtil.getLoginId()` 等需要 Web 上下文的 API，就会抛出上述异常。\n\n如果你需要在 非 Web 上下文 中调用上述 API，则需要手动 mock 一个上下文，才可以调用成功：\n\n例如：\n``` java\n// 【异步】new Thread  \n@RequestMapping(\"isLogin2\")\npublic SaResult isLogin2() {\n\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\tString tokenValue = StpUtil.getTokenValue();\n\tnew Thread(() -> {\n\t\tSaTokenContextMockUtil.setMockContext(()->{\n\t\t\tStpUtil.setTokenValueToStorage(tokenValue);\n\t\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\t});\n\t}).start();\n\treturn SaResult.data(StpUtil.getTokenValue());\n}\n```\n\n参考上述方法，你需要先调用 `SaTokenContextMockUtil.setMockContext(() -> { ... })` Mock 出一个 Web 上下文填充到上下文管理器中，\n然后在 Mock 上下文范围内调用 `StpUtil.setTokenValueToStorage(tokenValue)` 指定当前上下文的 token 值，其效果等同于在 web 上下文中前端提交了此 token 值。\n\n更多使用姿势请参考仓库示例：[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)\n\n\n### 响应式上下文\n\n在 WebFlux / Spring Cloud 等响应式环境下调用 Sa-Token 的同步 API 也有可能发生上下文异常：\n\n```\ncn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化\n\tat cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff.getModelBox(SaTokenContextForThreadLocalStaff.java:73) ~[classes/:na]\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaReactorFilter [DefaultWebFilterChain]\n\t*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor [DefaultWebFilterChain]\n\t*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor [DefaultWebFilterChain]\n\t*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor [DefaultWebFilterChain]\n\t*__checkpoint ⇢ HTTP GET \"/test/isLogin\" [ExceptionHandlingWebHandler]\n```\n\n如果是在自定义 Filter 中报的这个错，需要你在调用 Sa-Token 的同步 API 之前手动 set 一下上下文：\n``` java\n// 自定义过滤器\n@Component\npublic class MyFilter implements WebFilter {\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\t\ttry {\n\t\t\t// 先 set 上下文，再调用 Sa-Token 同步 API，并在 finally 里清除上下文\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSystem.out.println(StpUtil.isLogin());\n\t\t}\n\t\tfinally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\t\treturn chain.filter(exchange);\n\t}\n}\n```\n\n参考：[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)\n\n在 Controller 里同理：\n\n``` java\n@RequestMapping(\"isLogin2\")\npublic SaResult isLogin2(ServerWebExchange exchange) {\n\tSaResult res = SaReactorSyncHolder.setContext(exchange, ()->{\n\t\tSystem.out.println(\"是否登录：\" + StpUtil.isLogin());\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t});\n\treturn SaResult.data(res);\n}\n```\n\n更多示例请参考：\n[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)"
  },
  {
    "path": "sa-token-doc/fun/auth-flow.md",
    "content": "# Sa-Token 功能结构图\n\n--- \n\n### Sa-Token 功能结构图：\n\n<img class=\"s-w\" src=\"/big-file/doc/fun/sa-token-js4--2.png\" alt=\"sa-token-rz\" />\n\n### Sa-Token 认证流程图：\n\n<img class=\"s-w\" src=\"/big-file/doc/fun/sa-token-rz2.png\" alt=\"sa-token-rz\" />\n\n<!-- ![sa-token-rz](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/sa-token-rz.png 's-w') -->\n\nPS：鼠标右键选择 **`[在新窗口打开图片]`** 即可高清模式查看图片。\n"
  },
  {
    "path": "sa-token-doc/fun/auth-framework-function-test.md",
    "content": "# Java 权限认证框架功能 测试 / 对比 / 迁移。\n\n对比以下框架的常见功能，为项目技术栈迁移提供代码示例\n\n- Sa-Token\n- Apache Shiro\n- Spring Security\n- JWT\n\n\n> [!TIP| label:注意事项] \n> - 因个人精力&能力有限，本篇只展示部分常见功能的对比，也欢迎大家一起贡献案例，提交pr。\n> - 代码案例仓库：[https://gitee.com/sa-tokens/auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test)\n> - 注：本篇主要展示一些常见功能不同框架的实现差异，而非每个框架的所含功能点对比。\n \n\n--- \n\n\n### 依赖引入\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\n``` xml\n<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc/ -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot3-starter</artifactId>\n\t<version>1.39.0</version>\n</dependency>\n```\n\n<!------------- tab:Shiro ------------->\n``` xml\n<!-- Shiro 安全控制 -->\n<dependency>\n\t<groupId>org.apache.shiro</groupId>\n\t<artifactId>shiro-spring-boot-web-starter</artifactId>\n\t<version>1.13.0</version>\n</dependency>\n```\n\n<!------------- tab:SpringSecurity ------------->\n``` xml\n<!-- SpringSecurity -->\n<dependency>\n\t<groupId>org.springframework.boot</groupId>\n\t<artifactId>spring-boot-starter-security</artifactId>\n\t<version>3.3.2</version>\n</dependency>\n```\nSpringBoot 项目下一般不用特别指定 SpringSecurity 版本号\n\n<!------------- tab:JWT ------------->\n``` xml\n<!-- Hutool 工具类框架，其中包含 jwt 实现 -->\n<dependency>\n\t<groupId>cn.hutool</groupId>\n\t<artifactId>hutool-all</artifactId>\n\t<version>5.8.29</version>\n</dependency>\n```\n\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 会话登录 & 会话状态查询\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\n测试 Controller \n``` java \n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\t\n\t@Autowired\n\tSysUserDao sysUserDao;\n\t\t\n\t// 测试登录 \n\t@RequestMapping(\"doLogin\")\n\tpublic AjaxJson doLogin(String username, String password) {\n\t\t// 校验\n\t\tSysUser user = sysUserDao.findByUsername(username);\n\t\tif(user == null) {\n\t\t\treturn AjaxJson.getError(\"用户不存在\");\n\t\t}\n\t\tif(!user.getPassword().equals(password)) {\n\t\t\treturn AjaxJson.getError(\"密码错误\");\n\t\t}\n\t\t// 登录\n\t\tStpUtil.login(user.getId());\n\t\tStpUtil.getSession().set(\"user\", user);\n\t\treturn AjaxJson.getSuccess(\"登录成功\");\n\t}\n\t\n\t// 查询登录状态 \n\t@RequestMapping(\"isLogin\")\n\tpublic AjaxJson isLogin() {\n\t\tif(StpUtil.isLogin()) {\n\t\t\treturn AjaxJson.getSuccess(\"已登录，账号id：\" + StpUtil.getLoginId());\n\t\t}\n\t\treturn AjaxJson.getError(\"未登录\");\n\t}\n\n}\n```\n\n<!------------- tab:Shiro ------------->\n自定义 Realm \n``` java\npublic class MyRealm extends AuthorizingRealm {\n\n    @Autowired\n    private SysUserDao sysUserDao;\n\n    // 加载用户信息\n    @Override\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {\n        String username = (String)token.getPrincipal();\n        SysUser sysUser = sysUserDao.findByUsername(username);\n        if(sysUser == null){\n            return null;\n        }\n        return new SimpleAuthenticationInfo(\n                sysUser,\n                sysUser.getPassword(),\n                getName()\n        );\n    }\n\t\n    // 加载权限信息\n    @Override\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        return null;\n    }\n\n}\n```\n\nShiro 配置类\n``` java\n@Configuration\npublic class ShiroConfigure {\n\n    @Bean\n    public MyRealm myRealm() {\n        return new MyRealm();\n    }\n\n    @Bean\n    public DefaultWebSecurityManager securityManager() {\n        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();\n        manager.setRealm(myRealm());\n        return manager;\n    }\n\n    @Bean\n    public ShiroFilterFactoryBean shiroFilterFactoryBean() {\n        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();\n        bean.setSecurityManager(securityManager());\n        return bean;\n    }\n\n}\n```\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8082/acc/doLogin?username=zhang&password=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic AjaxJson doLogin(String username, String password) {\n\t\tSubject subject = SecurityUtils.getSubject();\n\t\ttry {\n\t\t\tsubject.login(new UsernamePasswordToken(username, password));\n\t\t\treturn AjaxJson.getSuccess(\"登录成功!\");\n\t\t} catch (AuthenticationException e) {\n\t\t\te.printStackTrace();\n\t\t\treturn AjaxJson.getError(e.getMessage());\n\t\t}\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8082/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic AjaxJson isLogin() {\n\t\tSubject subject = SecurityUtils.getSubject();\n\t\tif(subject.isAuthenticated()) {\n\t\t\tSysUser sysUser = (SysUser)subject.getPrincipal();\n\t\t\treturn AjaxJson.getSuccess(\"已登录，账号id：\" + sysUser.getId());\n\t\t}\n\t\treturn AjaxJson.getError(\"未登录\");\n\t}\n\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\n\n定义 SpringSecurity 配置类\n``` java\n@Configuration\npublic class SpringSecurityConfigure {\n\n    /**\n     * Spring Security的核心过滤器链配置\n     */\n    @Bean\n    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {\n        // 定义安全请求拦截规则\n        httpSecurity.authorizeHttpRequests(router -> {\n            router\n                    // 放行接口\n                    .requestMatchers(\"/acc/doLogin\", \"/acc/isLogin\").permitAll()\n\n\t\t\t\t\t// 所有请求都需要认证\n                    .anyRequest().authenticated(); \n                    ;\n                });\n\n        // 默认的表单登录\n        httpSecurity.formLogin(withDefaults());\n\n        // 是否启用 csrf 防御\n        httpSecurity.csrf( csrf -> csrf.disable() );\n\n        // 一些安全相关的全局响应头\n        httpSecurity.headers(httpSecurityHeadersConfigurer -> {\n            httpSecurityHeadersConfigurer.cacheControl(HeadersConfigurer.CacheControlConfig::disable);\n            httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);\n        });\n\t\t\n        return httpSecurity.build();\n    }\n\n    /**\n     * Spring Security 认证管理器\n     */\n    @Bean\n    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {\n        return config.getAuthenticationManager();\n    }\n\n}\n```\n\n定义 SpringSecurity UserDetails 管理器\n``` java\n/**\n * 自定义 SpringSecurity UserDetails 管理器\n *\n * @author click33\n * @since 2024/8/8\n */\n@Component\npublic class CustomUserDetailsManager implements UserDetailsManager {\n\n    @Autowired\n    SysUserDao sysUserDao;\n\n    @Override\n    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n        SysUser sysUser = sysUserDao.findByUsername(username);\n        if(sysUser == null){\n            throw new UsernameNotFoundException(\"用户不存在\");\n        }\n        return User.withUsername(sysUser.getUsername())\n                .password(\"{noop}\" + sysUser.getPassword())\n                .build();\n    }\n\n    @Override\n    public void createUser(UserDetails user) {\n\n    }\n\n    @Override\n    public void updateUser(UserDetails user) {\n\n    }\n\n    @Override\n    public void deleteUser(String username) {\n\n    }\n\n    @Override\n    public void changePassword(String oldPassword, String newPassword) {\n\n    }\n\n    @Override\n    public boolean userExists(String username) {\n        return false;\n    }\n\n}\n```\n\n测试 Controller \n\n``` java\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t@Autowired\n\tAuthenticationManager authenticationManager;\n\n\t@Autowired\n\tSysUserDao sysUserDao;\n\n\t// 测试登录  ---- http://localhost:8083/acc/doLogin?username=zhang&password=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic AjaxJson doLogin(String username, String password, HttpServletRequest request) {\n\t\ttry {\n\t\t\t// 验证账号密码\n\t\t\tUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);\n\t\t\tusernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username));\n\t\t\tAuthentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);\n\t\t\t// 存入上下文\n\t\t\tSecurityContextHolder.getContext().setAuthentication(authentication);\n\t\t\trequest.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());\n\t\t\t// 返回\n\t\t\treturn AjaxJson.getSuccess(\"登录成功!\");\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t\treturn AjaxJson.getError(e.getMessage());\n\t\t}\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8083/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic AjaxJson isLogin() {\n\t\tAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();\n\t\treturn AjaxJson.getSuccess(\"是否登录：\" + !(authentication instanceof AnonymousAuthenticationToken))\n\t\t\t\t.set(\"principal\", authentication.getPrincipal())\n\t\t\t\t.set(\"details\", authentication.getDetails());\n\t}\n\n}\n\n```\n\n<!------------- tab:JWT ------------->\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t@Autowired\n\tSysUserDao sysUserDao;\n\n\t// 测试登录 \n\t@RequestMapping(\"doLogin\")\n\tpublic AjaxJson doLogin(String username, String password) {\n\t\t// 校验\n\t\tSysUser user = sysUserDao.findByUsername(username);\n\t\tif(user == null) {\n\t\t\treturn AjaxJson.getError(\"用户不存在\");\n\t\t}\n\t\tif(!user.getPassword().equals(password)) {\n\t\t\treturn AjaxJson.getError(\"密码错误\");\n\t\t}\n\t\t// 登录\n\t\tString token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2);\n\t\treturn AjaxJson.getSuccess(\"登录成功\").set(\"token\", token);\n\t}\n\n\t// 查询登录状态 \n\t@RequestMapping(\"isLogin\")\n\tpublic AjaxJson isLogin(HttpServletRequest request) {\n\t\ttry{\n\t\t\tString token = request.getHeader(\"token\");\n\t\t\tJWT jwt = JwtUtil.parseToken(token);\n\t\t\treturn AjaxJson.getSuccess(\"已登录\")\n\t\t\t\t\t.set(\"id\", jwt.getPayload(\"userId\"))\n\t\t\t\t\t.set(\"user\", jwt.getPayload(\"user\"));\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t\treturn AjaxJson.getError(\"未登录\");\n\t\t}\n\t}\n\n}\n```\n\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 会话注销\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\n``` java \n@RequestMapping(\"logout\")\npublic AjaxJson logout() {\n\tStpUtil.logout();\n\treturn AjaxJson.getSuccess(\"注销成功\");\n}\n```\n\n<!------------- tab:Shiro ------------->\n\n``` java\n@RequestMapping(\"logout\")\npublic AjaxJson logout() {\n\tSecurityUtils.getSubject().logout();\n\treturn AjaxJson.getSuccess(\"注销成功\");\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\n``` java\n@Bean\npublic SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {\n\t\n\t// 其它配置 ...\n\t\n\t// 注销相关配置\n\thttpSecurity.logout(logout -> {\n\t\tlogout.logoutUrl(\"/acc/logout\");\n\t\tlogout.logoutSuccessHandler((request, response, authentication) -> {\n\t\t\tresponse.setStatus(200);\n\t\t\tresponse.setCharacterEncoding(\"UTF-8\");\n\t\t\tresponse.setContentType(\"application/json; charset=utf-8\");\n\t\t\tString jsonStr = new ObjectMapper().writeValueAsString(AjaxJson.getSuccess(\"注销成功!\"));\n\t\t\tresponse.getWriter().write(jsonStr);\n\t\t});\n\t});\n\n\treturn httpSecurity.build();\n}\n```\n\n<!------------- tab:JWT ------------->\nJWT 无法注销已经颁发的 token 。\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 账号密码登录（MD5 加 salt）\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\n测试 Controller \n``` java \n@RequestMapping(\"doLogin\")\npublic AjaxJson doLogin(String username, String password) {\n\t// 校验\n\tSysUser user = sysUserDao.findByUsername(username);\n\tif(user == null) {\n\t\treturn AjaxJson.getError(\"用户不存在\");\n\t}\n\tString salt = \"abc\";\n\tif(!user.getPassword().equals(SaSecureUtil.md5(salt + password))) {\n\t\treturn AjaxJson.getError(\"密码错误\");\n\t}\n\t// 登录\n\tStpUtil.login(user.getId());\n\tStpUtil.getSession().set(\"user\", user);\n\treturn AjaxJson.getSuccess(\"登录成功\");\n}\n```\n\n<!------------- tab:Shiro ------------->\n自定义 Realm Bean 设定密码凭证器\n``` java\n@Bean\npublic MyRealm myRealm() {\n\tMyRealm realm = new MyRealm();\n\t// 设定凭证匹配器\n\tHashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();\n\tcredentialsMatcher.setHashAlgorithmName(\"md5\");\n\trealm.setCredentialsMatcher(credentialsMatcher);\n\t// 返回\n\treturn realm;\n}\n```\n\n自定义 Realm 实现类 doGetAuthenticationInfo 方法返回 slat 信息\n``` java\n// 加载用户信息\n@Override\nprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {\n\tString username = (String)token.getPrincipal();\n\tSysUser sysUser = sysUserDao.findByUsername(username);\n\tif(sysUser == null){\n\t\treturn null;\n\t}\n\n\treturn new SimpleAuthenticationInfo(\n\t\t\tsysUser,\n\t\t\tsysUser.getPassword(),\n\t\t\tByteSource.Util.bytes(\"abc\"), // 指定 slat 信息 \n\t\t\tgetName()\n\t);\n}\n```\n\n登录代码照旧 \n\n<!------------- tab:SpringSecurity ------------->\n\nCustomUserDetailsManager 的 loadUserByUsername 指定 MD5 算法\n\n``` java\n@Override\npublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n\tSysUser sysUser = sysUserDao.findByUsername(username);\n\tif(sysUser == null){\n\t\tthrow new UsernameNotFoundException(\"用户不存在\");\n\t}\n\treturn User.withUsername(sysUser.getUsername())\n\t\t\t.password(\"{MD5}\" + sysUser.getPassword())\n\t\t\t.build();\n}\n```\n\n登录时指定 salt \n\n``` java\n@RequestMapping(\"doLogin\")\npublic AjaxJson doLogin(String username, String password, HttpServletRequest request) {\n\ttry {\n\t\t// 验证账号密码\n\t\tString salt = \"abc\";\n\t\tUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, salt + password);\n\t\t// 其它代码照旧 ... \n\t\t\n\t\t// 返回\n\t\treturn AjaxJson.getSuccess(\"登录成功!\");\n\t} catch (Exception e) {\n\t\te.printStackTrace();\n\t\treturn AjaxJson.getError(e.getMessage());\n\t}\n}\n```\n\n\n<!------------- tab:JWT ------------->\n测试 Controller \n``` java \n@RequestMapping(\"doLogin\")\npublic AjaxJson doLogin(String username, String password) {\n\t// 校验\n\tSysUser user = sysUserDao.findByUsername(username);\n\tif(user == null) {\n\t\treturn AjaxJson.getError(\"用户不存在\");\n\t}\n\tString salt = \"abc\";\n\tif(!user.getPassword().equals(SecureUtil.md5(salt + password))) {\n\t\treturn AjaxJson.getError(\"密码错误\");\n\t}\n\t// 登录\n\tString token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2);\n\treturn AjaxJson.getSuccess(\"登录成功\").set(\"token\", token);\n}\n```\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 从上下文获取当前登录 User 信息\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n``` java\n// 从上下文获取当前登录 User 信息 \n@RequestMapping(\"getCurrUser\")\npublic AjaxJson getCurrUser() {\n\treturn AjaxJson.getSuccess()\n\t\t\t.set(\"id\", StpUtil.getLoginId())\n\t\t\t.set(\"user\", StpUtil.getSession().get(\"user\"));\n}\n```\n\n<!------------- tab:Shiro ------------->\n\n``` java\n// 从上下文获取当前登录 User 信息 \n@RequestMapping(\"getCurrUser\")\npublic AjaxJson getCurrUser() {\n\tSubject subject = SecurityUtils.getSubject();\n\tSysUser sysUser = (SysUser)subject.getPrincipal();\n\treturn AjaxJson.getSuccess()\n\t\t\t.set(\"id\", sysUser.getId())\n\t\t\t.set(\"user\", sysUser);\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\n``` java\n// 从上下文获取当前登录 User 信息 \n@RequestMapping(\"getCurrUser\")\npublic AjaxJson getCurrUser() {\n\tAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();\n\tif(!(authentication instanceof AnonymousAuthenticationToken)) {\n\t\tSysUser sysUser = (SysUser)authentication.getDetails();\n\t\treturn AjaxJson.getSuccess()\n\t\t\t\t\t\t.set(\"id\", sysUser.getId())\n\t\t\t\t\t\t.set(\"user\", sysUser);\n\t}\n\treturn AjaxJson.getError(\"未登录\");\n}\n```\n\n<!------------- tab:JWT ------------->\n``` java\n// 从上下文获取当前登录 User 信息 \n@RequestMapping(\"getCurrUser\")\npublic AjaxJson getCurrUser(HttpServletRequest request) {\n\ttry{\n\t\tString token = request.getHeader(\"token\");\n\t\tJWT jwt = JwtUtil.parseToken(token);\n\t\tSysUser sysUser = jwt.getPayloads().get(\"user\", SysUser.class);\n\t\treturn AjaxJson.getSuccessData(sysUser);\n\t} catch (Exception e) {\n\t\te.printStackTrace();\n\t\treturn AjaxJson.getError(\"未登录\");\n\t}\n}\n```\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 从会话上下文上存取值\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n``` java\n// 测试从从会话上下文存取值 \n@RequestMapping(\"testSession\")\npublic AjaxJson test() {\n\tSaSession session = StpUtil.getSession();\n\n\tSystem.out.println(\"从 session 上取值：\" + session.get(\"name\"));\n\tsession.set(\"name\", \"zhang\");\n\tSystem.out.println(\"从 session 上取值：\" + session.get(\"name\"));\n\n\treturn AjaxJson.getSuccess();\n}\n```\n\n<!------------- tab:Shiro ------------->\n\n``` java\n// 测试从从会话上下文存取值\n@RequestMapping(\"testSession\")\npublic AjaxJson test() {\n\tSubject subject = SecurityUtils.getSubject();\n\tSession session = subject.getSession();\n\n\tSystem.out.println(\"从 session 上取值：\" + session.getAttribute(\"name\"));\n\tsession.setAttribute(\"name\", \"zhang\");\n\tSystem.out.println(\"从 session 上取值：\" + session.getAttribute(\"name\"));\n\n\treturn AjaxJson.getSuccess();\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\n\n``` java\n// 测试从从会话上下文存取值\n@RequestMapping(\"testSession\")\npublic AjaxJson testSession(HttpServletRequest request) {\n\tHttpSession session = request.getSession();\n\n\tSystem.out.println(\"从 session 上取值：\" + session.getAttribute(\"name\"));\n\tsession.setAttribute(\"name\", \"zhang\");\n\tSystem.out.println(\"从 session 上取值：\" + session.getAttribute(\"name\"));\n\treturn AjaxJson.getSuccess();\n}\n```\n\n<!------------- tab:JWT ------------->\n无\n\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n\n\n\n### 角色认证 & 权限认证\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\n自定义 StpInterface 实现类 \n\n``` java\n@Component\npublic class StpInterfaceImpl implements StpInterface {\n\n\t// 加载角色信息\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\treturn Arrays.asList(\"admin\", \"super-admin\", \"ceo\");\n\t}\n\n\t// 加载权限信息\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\treturn Arrays.asList(\"user:add\", \"user:delete\", \"user:update\");\n\t}\n\n}\n```\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/jur/\")\npublic class JurController {\n\n\t// 角色判断  ---- http://localhost:8082/jur/assertRole\n\t@RequestMapping(\"assertRole\")\n\tpublic AjaxJson assertRole() {\n\t\t// is 模式，返回 true 或 false\n\t\tSystem.out.println(\"单个角色判断：\" + StpUtil.hasRole(\"admin\"));\n\t\tSystem.out.println(\"多个角色判断(and)：\" + StpUtil.hasRoleAnd(\"admin\", \"dev-admin\"));\n\t\tSystem.out.println(\"多个角色判断(or)：\" + StpUtil.hasRoleOr(\"admin\", \"dev-admin\"));\n\n\t\t// check 模式，无角色时抛出异常\n\t\tStpUtil.checkRole(\"admin\");  // 单个 check\n\t\tStpUtil.checkRoleAnd(\"admin\", \"dev-admin\"); // 多个 check (and)\n\t\tStpUtil.checkRoleOr(\"admin\", \"dev-admin\"); // 多个 check (or)\n\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 权限判断  ---- http://localhost:8082/jur/assertPermission\n\t@RequestMapping(\"assertPermission\")\n\tpublic AjaxJson assertPermission() {\n\t\t// is 模式，返回 true 或 false\n\t\tSystem.out.println(\"单个权限判断：\" + StpUtil.hasPermission(\"user:add\"));\n\t\tSystem.out.println(\"多个权限判断(and)：\" + StpUtil.hasPermissionAnd(\"user:add\", \"user:delete22\"));\n\t\tSystem.out.println(\"多个权限判断(or)：\" + StpUtil.hasPermissionOr(\"user:add\", \"user:delete22\"));\n\n\t\t// check 模式，无权限时抛出异常\n\t\tStpUtil.checkPermission(\"user:add\");  // 单个 check\n\t\tStpUtil.checkPermissionAnd(\"user:add\", \"user:delete22\"); // 多个 check (and)\n\t\tStpUtil.checkPermissionOr(\"user:add\", \"user:delete22\"); // 多个 check (or)\n\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n}\n```\n\n\n<!------------- tab:Shiro ------------->\n\n自定义 Realm 里重写方法 doGetAuthorizationInfo\n\n``` java\n@Override\nprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n\tSimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();\n\t// 加载角色信息\n\tauthorizationInfo.addRoles(Arrays.asList(\"admin\", \"super-admin\", \"ceo\"));\n\t// 加载权限信息\n\tauthorizationInfo.addStringPermissions(Arrays.asList(\"user:add\", \"user:delete\", \"user:update\"));\n\treturn authorizationInfo;\n}\n```\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/jur/\")\npublic class JurController {\n\n\t// 角色判断 \n\t@RequestMapping(\"assertRole\")\n\tpublic AjaxJson assertRole() {\n\t\tSubject subject = SecurityUtils.getSubject();\n\n\t\t// is 模式，返回 true 或 false\n\t\tSystem.out.println(\"单个角色判断：\" + subject.hasRole(\"admin\"));\n\t\tSystem.out.println(\"多个角色判断(and)：\" + subject.hasAllRoles(Arrays.asList(\"admin\", \"dev-admin\")));\n\t\tSystem.out.println(\"多个角色判断(or)：\" + (subject.hasRole(\"admin\") || subject.hasRole(\"dev-admin\")));\n\n\t\t// check 模式，无角色时抛出异常\n\t\tsubject.checkRole(\"admin\");  // 单个 check\n\t\tsubject.checkRoles(\"admin\", \"dev-admin\"); // 多个 check (and)\n\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 权限判断 \n\t@RequestMapping(\"assertPermission\")\n\tpublic AjaxJson assertPermission() {\n\t\tSubject subject = SecurityUtils.getSubject();\n\n\t\t// is 模式，返回 true 或 false\n\t\tSystem.out.println(\"单个权限判断：\" + subject.isPermitted(\"user:add\"));\n\t\tSystem.out.println(\"多个权限判断(and)：\" + subject.isPermittedAll(\"user:add\", \"user:delete22\"));\n\t\tSystem.out.println(\"多个权限判断(or)：\" + (subject.isPermitted(\"user:add\") || subject.isPermitted(\"user:delete22\")));\n\n\t\t// check 模式，无权限时抛出异常\n\t\tsubject.checkPermission(\"user:add\");  // 单个 check\n\t\tsubject.checkPermissions(\"user:add\", \"user:delete22\"); // 多个 check (and)\n\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\n\nCustomUserDetailsManager 的 loadUserByUsername 里返回用户的 角色 或 权限 信息\n``` java\n@Override\npublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n\tSysUser sysUser = sysUserDao.findByUsername(username);\n\tif(sysUser == null){\n\t\tthrow new UsernameNotFoundException(\"用户不存在\");\n\t}\n\n    // 不可以同时返回 roles 和 authorities，因为会相互覆盖，SpringSecurity 源码有bug\n\treturn User.withUsername(sysUser.getUsername())\n\t\t\t.password(\"{noop}\" + sysUser.getPassword())\n\t\t\t// .roles(\"admin\", \"super-admin\", \"ceo\")\n\t\t\t.authorities(\"user:add\", \"user:delete\", \"user:update\")\n\t\t\t.build();\n}\n```\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/jur/\")\npublic class JurController {\n\n\t// 角色判断  \n\t@RequestMapping(\"assertRole\")\n\tpublic AjaxJson assertRole() {\n\t\tSecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};\n\n\t\tSystem.out.println(\"单个角色判断：\" + securityExpressionRoot.hasRole(\"admin\"));\n\t\tSystem.out.println(\"多个角色判断(and)：\" + (securityExpressionRoot.hasRole(\"admin\") && securityExpressionRoot.hasRole(\"dev-admin\")));\n\t\tSystem.out.println(\"多个角色判断(or)：\" + securityExpressionRoot.hasAnyRole(\"admin\", \"dev-admin\"));\n\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n\t// 权限判断 \n\t@RequestMapping(\"assertPermission\")\n\tpublic AjaxJson assertPermission() {\n\t\tSecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};\n\n\t\tSystem.out.println(\"单个权限判断：\" + securityExpressionRoot.hasAuthority(\"user:add\"));\n\t\tSystem.out.println(\"多个权限判断(and)：\" + (securityExpressionRoot.hasAuthority(\"user:add\") && securityExpressionRoot.hasAuthority(\"user:delete2\")));\n\t\tSystem.out.println(\"多个权限判断(or)：\" + securityExpressionRoot.hasAnyAuthority(\"user:add\", \"user:delete2\"));\n\n\t\treturn AjaxJson.getSuccess();\n\t}\n\n}\n```\n\n<!------------- tab:JWT ------------->\n无\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n\n### 注解鉴权\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\nSaTokenConfigure 配置注解拦截器\n``` java\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n}\n```\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/at-check/\")\npublic class AtCheckController {\n\n    // 登录校验 \n    @SaCheckLogin\n    @RequestMapping(\"checkLogin\")\n    public AjaxJson checkLogin() {\n        return AjaxJson.getSuccess();\n    }\n\n    // 角色校验 \n    @SaCheckRole(\"admin\")\n    @RequestMapping(\"checkRole\")\n    public AjaxJson checkRole() {\n        return AjaxJson.getSuccess();\n    }\n\n    // 权限校验 \n    @SaCheckPermission(\"user:add\")\n    @RequestMapping(\"checkPermission\")\n    public AjaxJson checkPermission() {\n        return AjaxJson.getSuccess();\n    }\n\n    // 忽略认证校验 \n    @SaIgnore\n    @SaCheckLogin\n    @RequestMapping(\"ignoreCheck\")\n    public AjaxJson ignoreCheck() {\n        return AjaxJson.getSuccess();\n    }\n\n}\n```\n\n\n<!------------- tab:Shiro ------------->\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/at-check/\")\npublic class AtCheckController {\n\n    // 登录校验 \n    @RequiresAuthentication\n    @RequestMapping(\"checkLogin\")\n    public AjaxJson checkLogin() {\n        return AjaxJson.getSuccess();\n    }\n\n    // 角色校验  \n    @RequiresRoles(\"admin\")\n    @RequestMapping(\"checkRole\")\n    public AjaxJson checkRole() {\n        return AjaxJson.getSuccess();\n    }\n\n    // 权限校验 \n    @RequiresPermissions(\"user:add\")\n    @RequestMapping(\"checkPermission\")\n    public AjaxJson checkPermission() {\n        return AjaxJson.getSuccess();\n    }\n\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\n\n`SpringSecurityConfigure` 配置类加上 `@EnableMethodSecurity` 注解\n``` java\n@Configuration\n@EnableMethodSecurity\npublic class SpringSecurityConfigure {\n\t// ...\n}\n```\n\n测试 Controller \n``` java\n@RestController\n@RequestMapping(\"/at-check/\")\npublic class AtCheckController {\n\n    // 登录校验  \n    @PreAuthorize(\"isAuthenticated()\")\n    @RequestMapping(\"checkLogin\")\n    public AjaxJson checkLogin() {\n        return AjaxJson.getSuccess();\n    }\n\n    // 角色校验  \n    @PreAuthorize(\"hasRole('admin')\")\n    @RequestMapping(\"checkRole\")\n    public AjaxJson checkRole() {\n        return AjaxJson.getSuccess();\n    }\n\n    // 权限校验 \n    @PreAuthorize(\"hasAuthority('user:add')\")\n    @RequestMapping(\"checkPermission\")\n    public AjaxJson checkPermission() {\n        return AjaxJson.getSuccess();\n    }\n\n}\n```\n\n<!------------- tab:JWT ------------->\n无\n\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 路由拦截鉴权\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\nSaTokenConfigure 配置 \n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\t// 注册 Sa-Token 拦截器打开注解鉴权功能 \n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\tSaRouter.match(\"/route-check/getInfo1\").stop(); // 不拦截\n\t\tSaRouter.match(\"/route-check/getInfo2\").check(r -> StpUtil.checkLogin()); // 需要登录\n\t\tSaRouter.match(\"/route-check/getInfo3\").check(r -> StpUtil.checkRole(\"admin2\")); // 需要角色\n\t\tSaRouter.match(\"/route-check/getInfo4\").check(r -> StpUtil.checkPermission(\"user:add3\")); // 需要权限\n\t})).addPathPatterns(\"/**\");\n}\n```\n\n<!------------- tab:Shiro ------------->\n\n过滤器配置 \n``` java\n@Bean\npublic ShiroFilterFactoryBean shiroFilterFactoryBean() {\n\tShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();\n\tbean.setSecurityManager(securityManager());\n\n\t// 路由拦截鉴权\n\tMap<String,String> filterMap = new LinkedHashMap<>();\n\tfilterMap.put(\"/route-check/getInfo\", \"anon\"); // 不拦截\n\tfilterMap.put(\"/route-check/getInfo2\", \"authc\"); // 需要登录\n\tfilterMap.put(\"/route-check/getInfo3\", \"perms[admin2]\"); // 需要角色\n\tfilterMap.put(\"/route-check/getInfo4\", \"perms[user:add3]\"); // 需要权限\n\tbean.setFilterChainDefinitionMap(filterMap);\n\tbean.setLoginUrl(\"/401\");  // 未登录时跳转的 url\n\tbean.setUnauthorizedUrl(\"/403\");  // 未授权时跳转的 url\n\n\treturn bean;\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\nSpringSecurityConfigure 配置 \n\n``` java\n@Bean\npublic SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {\n\t// 定义安全请求拦截规则\n\thttpSecurity.authorizeHttpRequests(router -> {\n\t\trouter\n\t\t\t.requestMatchers(\"/route-check/getInfo1\").permitAll()    // 不拦截\n\t\t\t.requestMatchers(\"/route-check/getInfo2\").authenticated()    // 需要登录\n\t\t\t.requestMatchers(\"/route-check/getInfo3\").hasRole(\"admin\")    // 需要 admin 角色\n\t\t\t.requestMatchers(\"/route-check/getInfo4\").hasAuthority(\"user:add\")    // 需要 user:add 权限\n\t\t\t.anyRequest().permitAll(); // 所有请求都放行\n\t\t});\n\n\treturn httpSecurity.build();\n}\n```\n\n<!------------- tab:JWT ------------->\n无\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 鉴权未通过的处理方案\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\n定义全局异常处理类 \n``` java\n@RestControllerAdvice\npublic class GlobalException {\n\n\t@ExceptionHandler(NotLoginException.class)\n\tpublic AjaxJson handlerException(NotLoginException e) {\n\t\treturn AjaxJson.get(401, \"未登录\");\n\t}\n\n\t@ExceptionHandler(NotRoleException.class)\n\tpublic AjaxJson handlerException(NotRoleException e) {\n\t\treturn AjaxJson.get(403, \"缺少角色：\" + e.getRole());\n\t}\n\n\t@ExceptionHandler(NotPermissionException.class)\n\tpublic AjaxJson handlerException(NotPermissionException e) {\n\t\treturn AjaxJson.get(403, \"缺少权限：\" + e.getPermission());\n\t}\n\n}\n```\n\n<!------------- tab:Shiro ------------->\n\n过滤器配置 \n``` java\n@Bean\npublic ShiroFilterFactoryBean shiroFilterFactoryBean() {\n\tShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();\n\t\n\t// ... \n\t\n\tbean.setLoginUrl(\"/401\");  // 未登录时跳转的 url\n\tbean.setUnauthorizedUrl(\"/403\");  // 未授权时跳转的 url\n\n\treturn bean;\n}\n```\n\n定义路由 \n``` java\n@RestController\npublic class ShiroErrorController {\n\n    @RequestMapping(\"/401\")\n    public Object error401(HttpServletRequest request, HttpServletResponse response) {\n        response.setStatus(200);\n        return AjaxJson.get(401, \"not login\");\n    }\n\n    @RequestMapping(\"/403\")\n    public Object error403(HttpServletRequest request, HttpServletResponse response) {\n        response.setStatus(200);\n        return AjaxJson.get(403, \"鉴权未通过\");\n    }\n\n}\n```\n\n<!------------- tab:SpringSecurity ------------->\n\n实现 `AccessDeniedHandler`, `AuthenticationEntryPoint` 接口\n\n``` java\n@Component\npublic class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable {\n\n    // 未登录异常\n    @Override\n    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {\n        //验证为未登陆状态会进入此方法，认证错误\n        response.setStatus(401);\n        response.setCharacterEncoding(\"UTF-8\");\n        response.setContentType(\"application/json; charset=utf-8\");\n        PrintWriter printWriter = response.getWriter();\n        String body = \"请先进行登录\";\n        printWriter.write(body);\n        printWriter.flush();\n    }\n\n    // 权限不足\n    @Override\n    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {\n        // 登陆状态下，权限不足执行该方法\n        response.setStatus(200);\n        response.setCharacterEncoding(\"UTF-8\");\n        response.setContentType(\"application/json; charset=utf-8\");\n        PrintWriter printWriter = response.getWriter();\n        String body = \"权限不足\";\n        printWriter.write(body);\n        printWriter.flush();\n    }\n}\n```\n\n注入 `SecurityFilterChain`\n\n``` java\n// 未登录处理逻辑、权限不足处理逻辑\n@Autowired\nprivate CustomAccessDeniedHandler accessDeniedHandler;\n\n@Bean\npublic SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {\n\t\n\t// 异常处理\n\thttpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {\n\t\t// 权限不足处理方案\n\t\thttpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler);\n\t\t// 未登录 处理逻辑\n\t\thttpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler);\n\t});\n\n\treturn httpSecurity.build();\n}\n```\n\n<!------------- tab:JWT ------------->\n使用 `try-catch` 捕获，或定义全局异常处理\n``` java\n@RestControllerAdvice\npublic class GlobalException {\n\t// 全局异常拦截（拦截项目中的所有异常）\n\t@ExceptionHandler\n\tpublic AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {\n\n\t\t// 打印堆栈，以供调试\n\t\tSystem.out.println(\"全局异常---------------\");\n\t\te.printStackTrace();\n\n\t\t// 返回给前端\n\t\treturn AjaxJson.getError(e.getMessage());\n\t}\n}\n```\n\n\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 和 Thymeleaf 集成\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\n`pom.xml` 依赖 \n``` xml\n<!-- 在 thymeleaf 标签中使用 Sa-Token -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-dialect-thymeleaf</artifactId>\n\t<version>${sa-token.version}</version>\n</dependency>\n```\n\n`SaTokenConfigure` 增加配置 `Sa-Token` 标签方言对象 \n\n``` java\n// Sa-Token 标签方言 (Thymeleaf版)\n@Bean\npublic SaTokenDialect getSaTokenDialect() {\n\treturn new SaTokenDialect();\n}\n```\n\n新建 `ThymeleafConfigure` 注入全局变量\n``` java\n@Configuration\npublic class ThymeleafConfigure {\n    // 为 Thymeleaf 注入全局变量，以便在页面中调用 Sa-Token 的方法\n    @Autowired\n    public void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) {\n        viewResolver.addStaticVariable(\"stp\", StpUtil.stpLogic);\n    }\n}\n```\n\n新建 `Controller` \n``` java\n@Controller\npublic class HomeController {\n    @RequestMapping(\"/\")\n    public Object index(HttpServletRequest request) {\n        request.setAttribute(\"isLogin\", StpUtil.isLogin());\n        return new ModelAndView(\"index.html\");\n    }\n}\n```\n\t\n新建 `templates/index.html`\n``` html\n<!DOCTYPE html>\n<html lang=\"zh\" xmlns:sa=\"http://www.thymeleaf.org/extras/sa-token\">\n<head>\n    <title>Sa-Token 集成 Thymeleaf 标签方言</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n</head>\n<body>\n<div class=\"view-box\" style=\"padding: 30px;\">\n    <h2>Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面</h2>\n    <p>当前是否登录：<span th:text=\"${stp.isLogin()}\"></span></p>\n    <p>\n        <a href=\"login\" target=\"_blank\">登录</a>\n        <a href=\"logout\" target=\"_blank\">注销</a>\n    </p>\n\n    <p>登录之后才能显示：<span sa:login>value</span></p>\n    <p>不登录才能显示：<span sa:notLogin>value</span></p>\n\n    <p>具有角色 admin 才能显示：<span sa:hasRole=\"admin\">value</span></p>\n    <p>同时具备多个角色才能显示：<span sa:hasRoleAnd=\"admin, ceo, cto\">value</span></p>\n    <p>只要具有其中一个角色就能显示：<span sa:hasRoleOr=\"admin, ceo, cto\">value</span></p>\n    <p>不具有角色 admin 才能显示：<span sa:notRole=\"admin\">value</span></p>\n\n    <p>具有权限 user-add 才能显示：<span sa:hasPermission=\"user-add\">value</span></p>\n    <p>同时具备多个权限才能显示：<span sa:hasPermissionAnd=\"user-add, user-delete, user-get\">value</span></p>\n    <p>只要具有其中一个权限就能显示：<span sa:hasPermissionOr=\"user-add, user-delete, user-get\">value</span></p>\n    <p>不具有权限 user-add 才能显示：<span sa:notPermission=\"user-add\">value</span></p>\n\n    <p th:if=\"${stp.isLogin()}\">\n        从SaSession中取值：\n        <span th:text=\"${stp.getSession().get('name', '')}\"></span>\n    </p>\n\n</div>\n</body>\n</html>\n```\n\n\n<!------------- tab:Shiro ------------->\n\n`pom.xml` 依赖 \n``` xml\n<!-- Shiro 整合 Thymeleaf 依赖 -->\n<dependency>\n\t<groupId>com.github.theborakompanioni</groupId>\n\t<artifactId>thymeleaf-extras-shiro</artifactId>\n\t<version>2.1.0</version>\n</dependency>\n```\n\n`ShiroConfigure` 增加配置 `Shiro` 方言对象 \n``` java\n@Bean\npublic ShiroDialect shiroDialect() {\n\treturn new ShiroDialect();\n}\n```\n\n新建 `Controller` \n``` java\n@Controller\npublic class HomeController {\n    @RequestMapping(\"/\")\n    public Object index(HttpServletRequest request) {\n        Subject subject = SecurityUtils.getSubject();\n        request.setAttribute(\"isLogin\", subject.isAuthenticated());\n        return new ModelAndView(\"index.html\");\n    }\n}\n```\n\n新建 `templates/index.html`\n\n``` html\n<!DOCTYPE html>\n<html lang=\"zh\" xmlns:shiro=\"http://www.pollix.at/thymeleaf/shiro\">\n<head>\n    <title>Shiro 集成 Thymeleaf 标签方言</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n</head>\n<body>\n<div class=\"view-box\" style=\"padding: 30px;\">\n    <h2>Shiro 集成 Thymeleaf 标签方言 —— 测试页面</h2>\n    <p>当前是否登录：<span th:text=\"${isLogin}\"></span></p>\n    <p>\n        <a href=\"/acc/doLogin?username=zhang&password=123456\" target=\"_blank\">登录</a>\n        <a href=\"/acc/logout\" target=\"_blank\">注销</a>\n    </p>\n    <p>登录之后才能显示：<span shiro:authenticated>value</span></p>\n    <p>不登录才能显示：<span shiro:guest >value</span></p>\n\n    <p>具有角色 admin 才能显示：<span shiro:hasRole=\"admin\">value</span></p>\n    <p>同时具备多个角色才能显示：<span shiro:hasAllRoles=\"admin, ceo, cto\">value</span></p>\n    <p>只要具有其中一个角色就能显示：<span shiro:hasAnyRoles=\"admin, ceo, cto\">value</span></p>\n    <p>不具有角色 admin 才能显示：<span shiro:lacksRole=\"admin\">value</span></p>\n\n    <p>具有权限 user-add 才能显示：<span shiro:hasPermission=\"user-add\">value</span></p>\n    <p>同时具备多个权限才能显示：<span shiro:hasAllPermissions=\"user-add, user-delete, user-get\">value</span></p>\n    <p>只要具有其中一个权限就能显示：<span shiro:hasAnyPermissions=\"user-add, user-delete, user-get\">value</span></p>\n    <p>不具有权限 user-add 才能显示：<span shiro:lacksPermission=\"user-add\">value</span></p>\n\n    <p shiro:authenticated>\n      当前登录账号：<span shiro:principal></span>\n    </p>\n\n</div>\n</body>\n</html>\n```\n\n<!------------- tab:SpringSecurity ------------->\n\n`pom.xml` 引入依赖 \n``` xml\n<!-- SpringSecurity 整合 Thymeleaf 依赖 -->\n<dependency>\n\t<groupId>org.thymeleaf.extras</groupId>\n\t<artifactId>thymeleaf-extras-springsecurity6</artifactId>\n\t<version>3.1.2.RELEASE</version>\n</dependency>\n```\n\t\t\n新建 `Controller` \n``` java\n@RestController\npublic class HomeController {\n    // 首页  \n    @RequestMapping(\"/\")\n    public Object index(HttpServletRequest request) {\n        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n        request.setAttribute(\"isLogin\", !(authentication instanceof AnonymousAuthenticationToken));\n        return new ModelAndView(\"index.html\");\n    }\n}\n```\n\n新建 `templates/index.html`\n\n``` html\n<!DOCTYPE html>\n<html lang=\"zh\" xmlns:sec=\"http://www.thymeleaf.org/thymeleaf-extras-springsecurity6\">\n<head>\n    <title>Shiro 集成 Thymeleaf 标签方言</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n</head>\n<body>\n<div class=\"view-box\" style=\"padding: 30px;\">\n    <h2>Shiro 集成 Thymeleaf 标签方言 —— 测试页面</h2>\n    <p>当前是否登录：<span th:text=\"${isLogin}\"></span></p>\n\n    <p>\n        <a href=\"/acc/doLogin?username=zhang&password=123456\" target=\"_blank\">登录</a>\n        <a href=\"/acc/logout\" target=\"_blank\">注销</a>\n    </p>\n    <p>登录之后才能显示：<span sec:authorize=\"isAuthenticated()\">value</span></p>\n    <p>不登录才能显示：<span sec:authorize=\"!isAuthenticated()\" >value</span></p>\n\n    <p>具有角色 admin 才能显示：<span sec:authorize=\"hasRole('admin')\">value</span></p>\n    <p>同时具备多个角色才能显示：<span sec:authorize=\"hasRole('admin') && hasRole('ceo') && hasRole('cto')\">value</span></p>\n    <p>只要具有其中一个角色就能显示：<span sec:authorize=\"hasAnyRole('admin', 'ceo', 'cto')\">value</span></p>\n    <p>不具有角色 admin 才能显示：<span sec:authorize=\"!hasRole('admin')\">value</span></p>\n\n    <p>具有权限 user-add 才能显示：<span sec:authorize=\"hasAuthority('user-add')\">value</span></p>\n    <p>同时具备多个权限才能显示：<span sec:authorize=\"hasAuthority('user-add') && hasAuthority('user-delete') && hasAuthority('user-get')\">value</span></p>\n    <p>只要具有其中一个权限就能显示：<span sec:authorize=\"hasAnyAuthority('user-add', 'user-delete', 'user-get')\">value</span></p>\n    <p>不具有权限 user-add 才能显示：<span sec:authorize=\"!hasAuthority('user-add')\">value</span></p>\n\n    <p sec:authorize=\"isAuthenticated()\">\n        当前登录账号：<span sec:authentication=\"details\"></span>\n    </p>\n\n</div>\n</body>\n</html>\n```\n\n<!------------- tab:JWT ------------->\n无\n\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 前后端分离\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n1、在登录时，将 token 信息返回到前端 \n``` java\n// 测试登录 \n@RequestMapping(\"doLogin\")\npublic AjaxJson doLogin(String username, String password) {\n\t// 校验\n\tSysUser user = sysUserDao.findByUsername(username);\n\t// user 信息校验代码不再赘述 ... \n\t\n\t// 登录\n\tStpUtil.login(user.getId());\n\tStpUtil.getSession().set(\"user\", user);\n\treturn AjaxJson.getSuccess(\"登录成功\").set(\"satoken\", StpUtil.getTokenValue());  // ⚠️ 关键代码 \n}\n```\n\n2、前端改造\n- 1、在登录请求时，将返回的 token 保存到本地 `localStorage.setItem('satoken', res.satoken)`。\n- 2、在后续每次请求中，读取本地保存的 satoken 塞到请求 header 中\n\n``` js\nconst header = {};\nif(localStorage.satoken) {\n\theader.satoken = localStorage.satoken;\n}\n// 后续提交请求...\n```\n\n\n<!------------- tab:Shiro ------------->\n\n1、自定义 SessionManager，从请求 header 里读取前端提交的 token\n``` java\npublic class MySessionManager extends DefaultWebSessionManager {\n\n    private static final String TOKEN = \"token\";\n\n    private static final String REFERENCED_SESSION_ID_SOURCE = \"Stateless request\";\n\n    public MySessionManager() {\n        super();\n    }\n\n    @Override\n    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {\n        String id = WebUtils.toHttp(request).getHeader(TOKEN);\n        // 如果请求头中有 token 则其值为sessionId\n        if (!StringUtils.isEmpty(id)) {\n            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);\n            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);\n            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);\n            return id;\n        } else {\n            //否则按默认规则从cookie取sessionId\n            return super.getSessionId(request, response);\n        }\n    }\n}\n```\n\n2、注入到 SecurityManager 中 \n``` java\n@Configuration\npublic class ShiroConfigure {\n\n\t// 省略其它次要代码 ... \n\n    @Bean\n    public DefaultWebSecurityManager securityManager() {\n        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();\n        manager.setRealm(myRealm());\n        manager.setSessionManager(sessionManager());\n        return manager;\n    }\n\n    @Bean\n    public ShiroFilterFactoryBean shiroFilterFactoryBean() {\n        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();\n        bean.setSecurityManager(securityManager());\n        return bean;\n    }\n\n    // 自定义sessionManager\n    @Bean\n    public SessionManager sessionManager() {\n        MySessionManager mySessionManager = new MySessionManager();\n        return mySessionManager;\n    }\n}\n```\n\n3、测试 Controller，登录时将 token 信息返回到前端 \n``` java\n// 测试登录 \n@RequestMapping(\"doLogin\")\npublic AjaxJson doLogin(String username, String password) {\n\tSubject subject = SecurityUtils.getSubject();\n\ttry {\n\t\tsubject.login(new UsernamePasswordToken(username, password));\n\t\tString token = subject.getSession().getId().toString();    // ⚠️ 关键代码\n\t\treturn AjaxJson.getSuccess(\"登录成功!\").set(\"token\", token);    // ⚠️ 关键代码\n\t} catch (AuthenticationException e) {\n\t\te.printStackTrace();\n\t\treturn AjaxJson.getError(e.getMessage());\n\t}\n}\n```\n\n4、前端改造\n- 1、在登录请求时，将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。\n- 2、在后续每次请求中，读取本地保存的 token 塞到请求 header 中\n\n``` js\nconst header = {};\nif(localStorage.token) {\n\theader.token = localStorage.token;\n}\n// 后续提交请求...\n```\n\n\n<!------------- tab:SpringSecurity ------------->\n见下方 “集成 Redis” 部分，同时做到：集成 Redis + 前后端分离。\n\n\n<!------------- tab:JWT ------------->\n`JWT` 不依赖 `Cookie` 保存/传输 token，因此无需特殊定制即可原生支持前后端分离模式。\n\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n### 集成 Redis\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:Sa-Token ------------->\n\npom.xml 引入依赖\n``` xml\n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa-token.version}</version>\n</dependency>\n\n<!-- 提供Redis连接池 -->\n<dependency>\n\t<groupId>org.apache.commons</groupId>\n\t<artifactId>commons-pool2</artifactId>\n</dependency>\n```\n\napplication.yml 新增连接配置 \n``` yaml\nspring:\n    data:\n        # redis配置\n        redis:\n            # Redis数据库索引（默认为0）\n            database: 1\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # 连接超时时间\n            timeout: 10s\n            lettuce:\n                pool:\n                    # 连接池最大连接数\n                    max-active: 200\n                    # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                    max-wait: -1ms\n                    # 连接池中的最大空闲连接\n                    max-idle: 10\n                    # 连接池中的最小空闲连接\n                    min-idle: 0\n```\n\n其它代码照旧\n\n\n<!------------- tab:Shiro ------------->\npom.xml 引入依赖 \n\n``` xml\n<!-- Shiro 集成 Redis -->\n<dependency>\n\t<groupId>org.crazycake</groupId>\n\t<artifactId>shiro-redis</artifactId>\n\t<version>3.3.1</version>\n</dependency>\n```\n\napplication.yml 新增连接配置 \n``` yaml\n\nspring:\n    redis:\n        shiro:\n            # Redis服务器地址\n            host: 127.0.0.1:6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # Redis数据库索引（默认为0）\n            database: 2\n            # 连接超时时间\n            timeout: 1800\n        \n```\n\n\nShiroConfigure 注入相关 Bean \n``` java\n@Configuration\npublic class ShiroConfigure {\n\n    // 自定义 securityManager\n    @Bean\n    public DefaultWebSecurityManager securityManager() {\n        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();\n        // manager.setRealm(myRealm());\n\n        // 自定义session管理 使用redis\n        manager.setSessionManager(sessionManager());\n        // 自定义缓存实现 使用redis\n        manager.setCacheManager(cacheManager());\n\n        return manager;\n    }\n\n    @Bean\n    public ShiroFilterFactoryBean shiroFilterFactoryBean() {\n        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();\n        bean.setSecurityManager(securityManager());\n        return bean;\n    }\n\n    // -------- 以下为 shiro redis 相关 --------\n\n    // Shiro redis 连接信息\n    @Value(\"${spring.redis.shiro.host}\")\n    private String host;\n    @Value(\"${spring.redis.shiro.database}\")\n    private int database;\n    @Value(\"${spring.redis.shiro.timeout}\")\n    private int timeout;\n    @Value(\"${spring.redis.shiro.password}\")\n    private String password;\n\n    /**\n     * 配置shiro redisManager\n     */\n    public RedisManager redisManager() {\n        RedisManager redisManager = new RedisManager();\n        redisManager.setHost(host);\n        if(StringUtils.hasText(password)){\n            redisManager.setPassword(password);\n        }\n        redisManager.setDatabase(database);\n        redisManager.setTimeout(timeout);\n        return redisManager;\n    }\n\n    /**\n     * cacheManager 缓存 redis 实现\n     */\n    @Bean\n    public RedisCacheManager cacheManager() {\n        RedisCacheManager redisCacheManager = new RedisCacheManager();\n        redisCacheManager.setRedisManager(redisManager());\n        return redisCacheManager;\n    }\n\n    /**\n     * RedisSessionDAO redis 实现\n     */\n    @Bean\n    public RedisSessionDAO redisSessionDAO() {\n        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();\n        redisSessionDAO.setRedisManager(redisManager());\n        return redisSessionDAO;\n    }\n\n    // 自定义sessionManager\n    @Bean\n    public SessionManager sessionManager() {\n        MySessionManager mySessionManager = new MySessionManager();\n        mySessionManager.setSessionDAO(redisSessionDAO());\n        return mySessionManager;\n    }\n\n}\n```\n\nSysUser 实体类要实现 Serializable 接口\n``` java\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\npublic class SysUser implements Serializable {\n\t// ... \n}\n```\n\n其它代码照旧 \n\n\n<!------------- tab:SpringSecurity ------------->\n\n（结合上部分，同时做到集成 Redis + 前后端分离）\n\n1、`pom.xml` 引入依赖 \n``` xml\n<!-- HttpSession 存储到 Redis -->\n<dependency>\n\t<groupId>org.springframework.session</groupId>\n\t<artifactId>spring-session-data-redis</artifactId>\n</dependency>\n<dependency>\n\t<groupId>org.springframework.boot</groupId>\n\t<artifactId>spring-boot-starter-data-redis</artifactId>\n</dependency>\n```\n\n2、`yml` 增加配置\n``` yml\nspring:\n    session:\n        store-type: redis\n        timeout: 8H\n        redis:\n            namespace: spring:session\n\n    data:\n        # redis配置\n        redis:\n            # Redis数据库索引（默认为0）\n            database: 3\n            # Redis服务器地址\n            host: 127.0.0.1\n            # Redis服务器连接端口\n            port: 6379\n            # Redis服务器连接密码（默认为空）\n            password:\n            # 连接超时时间\n            timeout: 10s\n            lettuce:\n                pool:\n                    # 连接池最大连接数\n                    max-active: 200\n                    # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                    max-wait: -1ms\n                    # 连接池中的最大空闲连接\n                    max-idle: 10\n                    # 连接池中的最小空闲连接\n                    min-idle: 0\n```\n\n3、在 `CustomAccessDeniedHandler` 自定义认证异常处理类中，返回 `json` 格式数据\n\n``` java\n@Component\npublic class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable {\n\n    // 未登录异常\n    @Override\n    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {\n        //验证为未登陆状态会进入此方法，认证错误\n        response.setStatus(401);\n        response.setCharacterEncoding(\"UTF-8\");\n        response.setContentType(\"application/json; charset=utf-8\");\n        PrintWriter printWriter = response.getWriter();\n        String body = new ObjectMapper().writeValueAsString(AjaxJson.get(401, \"请先进行登录\"));\n        printWriter.write(body);\n        printWriter.flush();\n    }\n\n    // 权限不足\n    @Override\n    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {\n        // 登陆状态下，权限不足执行该方法\n        response.setStatus(200);\n        response.setCharacterEncoding(\"UTF-8\");\n        response.setContentType(\"application/json; charset=utf-8\");\n        PrintWriter printWriter = response.getWriter();\n        String body = new ObjectMapper().writeValueAsString(AjaxJson.get(403, \"权限不足\"));\n        printWriter.write(body);\n        printWriter.flush();\n    }\n    \n}\n```\n\n4、别忘了注入到 `SecurityFilterChain` 过滤器链 \n``` java\n // 异常处理\nhttpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {\n\t// 权限不足处理方案\n\thttpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler);\n\t// 未登录 处理逻辑\n\thttpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler);\n});\n```\n\n5、在登录时，返回对应 token 信息\n``` java\n// 测试登录 \n@RequestMapping(\"doLogin\")\npublic AjaxJson doLogin(String username, String password, HttpServletRequest request) {\n\ttry {\n\t\t// 验证账号密码\n\t\tUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);\n\t\tusernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username));\n\t\tAuthentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);\n\t\t// 存入上下文\n\t\tSecurityContextHolder.getContext().setAuthentication(authentication);\n\t\trequest.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());\n\t\t// 返回\n\t\tString token = request.getSession().getId();\n\t\treturn AjaxJson.getSuccess(\"登录成功!\").set(\"token\", token);\n\t} catch (Exception e) {\n\t\te.printStackTrace();\n\t\treturn AjaxJson.getError(e.getMessage());\n\t}\n}\n```\n\n6、前端改造\n- 1、在登录请求时，将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。\n- 2、在后续每次请求中，读取本地保存的 token 塞到请求 header 中\n\n``` js\nconst header = {};\nif(localStorage.token) {\n\theader.token = localStorage.token;\n}\n// 后续提交请求...\n```\n\n7、新建 `HttpSessionConfigure` 配置重写 `HttpSessionId` 读取策略，改为从 `header` 头读取 `token` 参数作为 `SessionId`\n``` java\n@Configuration\npublic class HttpSessionConfigure {\n    // HttpSession 读取策略，从 header 头读取 token 参数作为 session id\n    @Bean\n    public HeaderHttpSessionIdResolver httpSessionStrategy() {\n        System.out.println(\"----------------- 自定义 HttpSession Id 读取方式\");\n        return new HeaderHttpSessionIdResolver(\"token\");\n    }\n}\n```\n\n\n<!------------- tab:JWT ------------->\n无\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/cors-filter.md",
    "content": "# 解决跨域问题 \n\n<!-- 参考：[https://blog.csdn.net/shengzhang_/article/details/119928794](https://blog.csdn.net/shengzhang_/article/details/119928794) -->\n\n参考1: [https://juejin.cn/post/7491603065944129590](https://juejin.cn/post/7491603065944129590)\n\n参考2: [https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g](https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g)\n\n参考3: [https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg](https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg)"
  },
  {
    "path": "sa-token-doc/fun/curr-domain.md",
    "content": "# 解决反向代理 uri 丢失的问题\n\n--- \n\n使用 `request.getRequestURL()` 可获取当前程序所在外网的访问地址，在 Sa-Token 中，其 `SaHolder.getRequest().getUrl()` 也正是借助此API完成，\n有很多模块都用到了这个能力，比如SSO单点登录。\n\n我们可以使用如下代码测试此API\n``` java\n// 显示当前程序所在外网的都访问地址\n@RequestMapping(\"test\")\npublic String test() {\n\treturn \"您访问的是：\" + SaHolder.getRequest().getUrl();\n}\n```\n\n从浏览器访问此接口，我们可以看到：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/fun/test-curr-domain.png\" alt=\"test-curr-domain.png\" />\n\n此 API 在本地开发时一般可以正常工作，然而如果我们在部署时使用 Nginx 做了一层反向代理后，其最终结果可能会和我们预想的有一点偏差：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/fun/test-curr-domain-fxdl.png\" alt=\"test-curr-domain-fxdl.png\" />\n\n不仅是 Nginx，所有包含路由转发的地方都有可能导致上述丢失 uri 的现象，解决方案也很简单，既然程序无法自动识别，我们改成手动获取即可，Sa-Token 提供两个方案：\n\n\n### 方案一：Nginx转发时追加 header 参数\n\n##### 1、首先在 Nginx 代理转发的地方增加参数\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/fun/nginx-add-header.png\" alt=\"nginx-add-header.png\" />\n\n重点是这一句：`proxy_set_header Public-Network-URL http://$http_host$request_uri;`\n\n##### 2、在程序中新增类 `CustomSaTokenContextForSpring.java`，重写获取uri的逻辑\n\n``` java\n@Primary\n@Component\npublic class CustomSaTokenContextForSpring extends SaTokenContextForSpring {\n\t\n\t@Override\n\tpublic SaRequest getRequest() {\n\t\treturn new SaRequestForServlet(SpringMVCUtil.getRequest()) {\n\t\t\t@Override\n\t\t\tpublic String getUrl() {\n\t\t\t\tif(request.getHeader(\"Public-Network-URL\") != null) {\n\t\t\t\t\treturn request.getHeader(\"Public-Network-URL\");\n\t\t\t\t}\n\t\t\t\treturn request.getRequestURL().toString();\n\t\t\t}\n\t\t};\n\t}\n\n}\n```\n\n其它逻辑保持不变，框架即可正确获取 uri 地址\n\n> [!ATTENTION| label:风险警告] \n> 注意：步骤一与步骤二需要同步存在，否则可能有前端假传 header 参数造成安全问题 \n\n\n### 方案二：直接在yml中配置当前项目的网络访问地址\n\n在 `application.yml` 中增加配置：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n    # 配置当前项目的网络访问地址\n    curr-domain: http://local.dev33.cn:8902/api\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 配置当前项目的网络访问地址\nsa-token.curr-domain=http://local.dev33.cn:8902/api\n```\n<!---------------------------- tabs:end ---------------------------->\n\n即可避免路由转发过程中丢失 uri 的问题 \n"
  },
  {
    "path": "sa-token-doc/fun/custom-annotations.md",
    "content": "# 自定义注解\n\n如果框架内置的注解无法满足你的业务需求，你还可以自定义注解注入到框架中。\n\n---\n\n### 1、自定义注解\n\n假设有以下业务需求\n\n> [!INFO| label:需求场景] \n> 自定义一个注解 `@CheckAccount`，具有 `name`、`pwd` 两个字段，在标注一个方法上时，要求前端必须提交相应的账号密码参数才能访问方法。\n\n\n#### 1.1、第一步，创建一个注解\n\n``` java\n/**\n * 账号校验：在标注一个方法上时，要求前端必须提交相应的账号密码参数才能访问方法。\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface CheckAccount {\n\n    /**\n     * 需要校验的账号\n     */\n    String name();\n\n    /**\n     * 需要校验的密码\n     */\n    String pwd();\n\n}\n```\n\n#### 1.2、第二步，创建注解处理器 \n\n实现 `SaAnnotationHandlerInterface` 接口，指定泛型为刚才自定义的注解 \n\n``` java\n/**\n * 注解 CheckAccount 的处理器\n */\n@Component\npublic class CheckAccountHandler implements SaAnnotationHandlerInterface<CheckAccount> {\n\n    // 指定这个处理器要处理哪个注解\n    @Override\n    public Class<CheckAccount> getHandlerAnnotationClass() {\n        return CheckAccount.class;\n    }\n\n    // 每次请求校验注解时，会执行的方法\n    @Override\n    public void checkMethod(CheckAccount at, AnnotatedElement element) {\n        // 获取前端请求提交的参数\n        String name = SaHolder.getRequest().getParamNotNull(\"name\");\n        String pwd = SaHolder.getRequest().getParamNotNull(\"pwd\");\n\n        // 与注解中指定的值相比较\n        if(name.equals(at.name()) && pwd.equals(at.pwd()) ) {\n            // 校验通过，什么也不做\n        } else {\n            // 校验不通过，则抛出异常\n            throw new SaTokenException(\"账号或密码错误，未通过校验\");\n        }\n    }\n\n}\n```\n\n参考上述代码，实现类上指定了 `@Component` 注解，使其可以在 ioc 环境下（如 Spring）被自动扫描注册 Sa-Token 中，\n如果你的项目属于非 ioc 环境，则需要手动将其注册到 Sa-Token 框架中：\n``` java\nSaAnnotationStrategy.instance.registerAnnotationHandler(new CheckAccountHandler());\n```\n\n#### 1.3、测试自定义的注解\n\n我们在一个请求接口上指定这个注解，来测试一下效果 \n\n``` java\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@RequestMapping(\"test\")\n\t@CheckAccount(name = \"sa\", pwd = \"123456\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"------------进来了\");\n\t\treturn SaResult.ok(); \n\t}\n\t\n}\n```\n\n启动项目，使用浏览器访问此接口。\n\n先来个错误的账号密码访问测试一下：[http://localhost:8081/test/test?name=sa&pwd=123](http://localhost:8081/test/test?name=sa&pwd=123)\n\n返回结果：\n\n``` js\n{\n  \"code\": 500,\n  \"msg\": \"账号或密码错误，未通过校验\",\n  \"data\": null\n}\n```\n\n使用正确账号密码测试访问：[http://localhost:8081/test/test?name=sa&pwd=123456](http://localhost:8081/test/test?name=sa&pwd=123456)\n\n返回结果：\n\n``` js\n{\n  \"code\": 200,\n  \"msg\": \"ok\",\n  \"data\": null\n}\n```\n\n\n\n### 2、使用自定义注解优化多账号鉴权\n\n在之前的 [ 多账号鉴权 ] 章节，我们介绍了利用 “spring 注解处理器” 达到注解合并的目的，从而简化多账号体系下的注解鉴权写法。\n\n此种方案比较简单，但是也有一些缺点。\n- 1、强依赖 Spring，无法在非 Spring 环境中使用。\n- 2、注解递归检查可能会造成一些性能下降。\n- 3、扩展性较低，只能略微简化框架内置好的注解写法，无法灵活扩展功能。\n\n此处我们再演示一种方案，使用自定义注解的方式达到相同的目的。\n\n\n#### 2.1、首先定义注解\n\n``` java\n/**\n * 登录认证(User版)：只有登录之后才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上） \n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckLogin {\n\t\n}\n```\n\n#### 2.2、定义注解处理器\n``` java\n/**\n * 注解 SaUserCheckLogin 的处理器\n */\n@Component\npublic class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface<SaUserCheckLogin> {\n\n    @Override\n    public Class<SaUserCheckLogin> getHandlerAnnotationClass() {\n        return SaUserCheckLogin.class;\n    }\n\n    @Override\n    public void checkMethod(SaUserCheckLogin at, AnnotatedElement element) {\n        SaCheckLoginHandler._checkMethod(StpUserUtil.TYPE);\n    }\n\n}\n```\n\n#### 2.3、使用新注解\n接下来就可以使用我们的自定义注解了：\n\n``` java\n// 使用 @SaUserCheckLogin 的效果等同于使用：@SaCheckLogin(type = \"user\")\n@SaUserCheckLogin\n@RequestMapping(\"info\")\npublic String info() {\n    return \"查询用户信息\";\n}\n```\n\n注：其它注解 `@SaCheckRole(\"xxx\")`、`@SaCheckPermission(\"xxx\")` 同理， 完整示例参考 Gitee 代码：\n[自定义注解](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation)。\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/dynamic-router-check.md",
    "content": "# 参考：把路由拦截鉴权动态化\n\n框架提供的示例是硬代码写死的，不过稍微做一下更改，你就可以让他动态化\n\n--- \n\n参考如下：\n\n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\tSaRouter\n\t\t\t.match(\"/**\")\n\t\t\t.notMatch(excludePaths())\n\t\t\t.check(r -> StpUtil.checkLogin());\n\t})).addPathPatterns(\"/**\");\n}\n\n// 动态获取哪些 path 可以忽略鉴权 \npublic List<String> excludePaths() {\n\t// 此处仅为示例，实际项目你可以写任意代码来查询这些path\n\treturn Arrays.asList(\"/path1\", \"/path2\", \"/path3\");\n}\n```\n\n如果不仅仅是登录校验，还需要鉴权，那也很简单：\n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\t// 遍历校验规则，依次鉴权 \n\t\tMap<String, String> rules = getAuthRules();\n\t\tfor (String path : rules.keySet()) {\n\t\t\tSaRouter.match(path, () -> StpUtil.checkPermission(rules.get(path)));\n\t\t}\n\t})).addPathPatterns(\"/**\");\n}\n\n// 动态获取鉴权规则 \npublic Map<String, String> getAuthRules() {\n\t// key 代表要拦截的 path，value 代表需要校验的权限 \n\tMap<String, String> authMap = new LinkedHashMap<>();\n\tauthMap.put(\"/user/**\", \"user\");\n\tauthMap.put(\"/admin/**\", \"admin\");\n\tauthMap.put(\"/article/**\", \"article\");\n\t// 更多规则 ... \n\treturn authMap;\n}\n```\n\n\n--- \n\n错误的写法：\n\n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\tStpUtil.checkLogin();\n\t}))\n\t.addPathPatterns(\"/**\")\n\t.excludePathPatterns(excludePaths());\n}\n\n// 动态获取哪些 path 可以忽略鉴权 \npublic List<String> excludePaths() {\n\t// 此处仅为示例，实际项目你可以写任意代码来查询这些path\n\treturn Arrays.asList(\"/path1\", \"/path2\", \"/path3\");\n}\n```\n\n错误点：因为 lambda 表达式之外的代码只会在启动时执行一次，所以 `excludePaths()` 方法是无法做到动态化读取的，\n若要在项目运行时动态读写，必须把调用 `excludePaths()` 的时机放在 lambda 里。\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/exception-code.md",
    "content": "# 异常细分状态码\n\n--- \n\n### 获取异常细分状态码\n\nSa-Token 中的基础异常类是 `SaTokenException`，在此基础上，又针对一些特定场景划分出诸如 `NotLoginException`、`NotPermissionException` 等。\n\n但是框架中异常抛出点远远多于异常种类的划分，比如在 SSO 插件中，[ redirect 重定向地址无效 ] 和 [ ticket 参数值无效 ] 都会导致 SSO 授权的失败，\n但是它们抛出的异常都是 `SaSsoException`，如果你需要对这两种异常情形做出不同的处理，仅仅判断异常的 ClassType 显然不够。\n\n为了解决上述需求，Sa-Token 对每个异常抛出点都会指定一个特定的 code 值，就像这样：\n\n``` java\nif(SaFoxUtil.isUrl(url) == false) {\n\tthrow new SaSsoException(\"无效redirect：\" + url).setCode(SaSsoErrorCode.CODE_30001);\t\n}\n```\n\n就像是打上一个特定的标记，不同异常情形标记的 code 码值也会不同，这就为你精细化异常处理提供了前提。\n\n要在捕获异常时获取这个 code 码也非常简单：\n\n``` java\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\t@ExceptionHandler(SaTokenException.class)\n\tpublic SaResult handlerSaTokenException(SaTokenException e) {\n\t\t\n\t\t// 根据不同异常细分状态码返回不同的提示 \n\t\tif(e.getCode() == 30001) {\n\t\t\treturn SaResult.error(\"redirect 重定向 url 是一个无效地址\");\n\t\t}\n\t\tif(e.getCode() == 30002) {\n\t\t\treturn SaResult.error(\"redirect 重定向 url 不在 allowUrl 允许的范围内\");\n\t\t}\n\t\tif(e.getCode() == 30004) {\n\t\t\treturn SaResult.error(\"提供的 ticket 是无效的\");\n\t\t}\n\t\t// 更多 code 码判断 ... \n\t\t\n\t\t// 默认的提示 \n\t\treturn SaResult.error(\"服务器繁忙，请稍后重试...\");\n\t}\n}\n```\n\nSaToken 中的所有异常都是继承于 `SaTokenException` 的，也就是说，所有异常你都可以通过 `e.getCode()` 的方式获取对应的异常细分状态码。\n\n\n\n\n\n### 异常细分状态码-参照表\n\n#### sa-token-code 核心包\n\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t|\n| -1\t\t| 代表这个异常在抛出时未指定异常细分状态码\t|\n| 10001\t\t| 未能获取有效的上下文处理器\t\t\t\t|\n| 10002\t\t| 未能获取有效的上下文\t\t\t\t\t|\n| 10003\t\t| JSON 转换器未实现\t\t\t\t\t\t|\n| 10011\t\t| 未能从全局 StpLogic 集合中找到对应 type 的 StpLogic\t\t\t\t\t|\n| 10021\t\t| 指定的配置文件加载失败\t\t\t\t\t|\n| 10022\t\t| 配置文件属性无法正常读取\t\t\t\t|\n| 10031\t\t| 重置的侦听器集合不可以为空\t\t\t\t|\n| 10032\t\t| 注册的侦听器不可以为空\t\t\t\t\t|\n| 10301\t\t| 提供的 Same-Token 是无效的\t\t\t\t|\n| 10311\t\t| 表示未能通过 Http Basic 认证校验\t\t|\n| 10321\t\t| 提供的 HttpMethod 是无效的\t\t\t\t|\n| 11001\t\t| 未能读取到有效Token\t\t\t\t\t\t|\n| 11002\t\t| 登录时的账号id值为空\t\t\t\t\t|\n| 11003\t\t| 更改 Token 指向的 账号Id 时，账号Id值为空\t\t\t\t\t\t|\n| 11011\t\t| 未能读取到有效Token\t\t\t\t\t\t|\n| 11012\t\t| Token无效 \t\t\t\t\t\t\t\t|\n| 11013\t\t| Token已过期\t\t\t\t\t\t\t|\n| 11014\t\t| Token已被顶下线\t\t\t\t\t\t|\n| 11015\t\t| Token已被踢下线\t\t\t\t\t\t|\n| 11016\t\t| Token已被冻结\t\t\t\t\t\t|\n| 11031\t\t| 在未集成 sa-token-jwt 插件时调用 getExtra() 抛出异常\t\t\t\t\t|\n| 11041\t\t| 缺少指定的角色\t\t\t\t\t\t\t|\n| 11051\t\t| 缺少指定的权限\t\t\t\t\t\t\t|\n| 11061\t\t| 当前账号未通过服务封禁校验\t\t\t\t|\n| 11062\t\t| 提供要解禁的账号无效\t\t\t\t\t|\n| 11063\t\t| 提供要解禁的服务无效\t\t\t\t\t|\n| 11064\t\t| 提供要解禁的等级无效\t\t\t\t\t|\n| 11071\t\t| 二级认证校验未通过\t\t\t\t\t\t|\n| 12001\t\t| 请求中缺少指定的参数\t\t\t\t\t|\n| 12002\t\t| 构建 Cookie 时缺少 name 参数\t\t\t|\n| 12003\t\t| 构建 Cookie 时缺少 value 参数\t\t|\n| 12101\t\t| Base64 编码异常\t\t\t\t\t|\n| 12102\t\t| Base64 解码异常\t\t\t\t\t|\n| 12103\t\t| URL 编码异常\t\t\t\t\t\t|\n| 12104\t\t| URL 解码异常\t\t\t\t\t\t|\n| 12111\t\t| md5 加密异常\t\t\t\t\t\t|\n| 12112\t\t| sha1 加密异常\t\t\t\t\t\t|\n| 12113\t\t| sha256 加密异常\t\t\t\t\t|\n| 12114\t\t| AES 加密异常\t\t\t\t\t\t|\n| 12115\t\t| AES 解密异常\t\t\t\t\t\t|\n| 12116\t\t| RSA 公钥加密异常\t\t\t\t\t|\n| 12117\t\t| RSA 私钥加密异常\t\t\t\t\t|\n| 12118\t\t| RSA 公钥解密异常\t\t\t\t\t|\n| 12119\t\t| RSA 私钥解密异常\t\t\t\t\t|\n| 12201\t\t| 参与参数签名的秘钥不可为空\t\t\t|\n| 12202\t\t| 给定的签名无效\t\t\t\t\t\t|\n| 12203\t\t| timestamp 超出允许的范围\t\t\t|\n\n\n#### sa-token-servlet\n\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t|\n| 20001\t\t| 转发失败\t\t\t\t\t\t\t\t\t\t\t|\n| 20002\t\t| 重定向失败\t\t\t\t\t\t\t\t\t\t\t|\n\n\n#### sa-token-spring-boot-starter\n\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t|\n| 20101\t\t| 企图在非 Web 上下文获取 Request、Response 等对象\t\t|\n| 20103\t\t| 对象转 JSON 字符串失败\t\t\t\t\t\t\t\t|\n| 20104\t\t| JSON 字符串转 Map 失败\t\t\t\t\t\t\t\t|\n| 20105\t\t| 默认的 Filter 异常处理函数\t\t\t\t\t\t\t|\n\n\n#### sa-token-reactor-spring-boot-starter\n\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t|\n| 20203\t\t| 对象转 JSON 字符串失败\t\t\t\t\t\t\t\t|\n| 20204\t\t| JSON 字符串转 Map 失败\t\t\t\t\t\t\t\t|\n| 20205\t\t| 默认的 Filter 异常处理函数\t\t\t\t\t\t\t|\n\n\n#### sa-token-solon-plugin\n\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t|\n| 20301\t\t| 默认的拦截器异常处理函数\t\t\t\t\t\t\t|\n| 20302\t\t| 默认的 Filter 异常处理函数\t\t\t\t\t\t\t|\n\n\n#### sa-token-sso 单点登录相关：\n\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t|\n| 30001\t\t| `redirect` 重定向 url 是一个无效地址\t\t\t\t\t|\n| 30002\t\t| `redirect` 重定向 url 不在 allowUrl 允许的范围内\t\t|\n| 30003\t\t| 接口调用方提供的 `secretkey` 秘钥无效\t\t\t\t\t|\n| 30004\t\t| 提供的 `ticket` 是无效的\t\t\t\t\t\t\t\t|\n| 30005\t\t| 在模式三下，sso-client 调用 sso-server 端 校验ticket接口 时，得到的响应是校验失败\t|\n| 30006\t\t| 在模式三下，sso-client 调用 sso-server 端 单点注销接口 时，得到的响应是注销失败\t|\n| 30007\t\t| http 请求调用 提供的 `timestamp` 与当前时间的差距超出允许的范围\t|\n| 30008\t\t| http 请求调用 提供的 `sign` 无效\t\t\t\t\t\t|\n| 30009\t\t| 本地系统没有配置 `secretkey` 字段\t\t\t\t\t\t|\n| 30010\t\t| 本地系统没有配置 http 请求处理器\t\t\t\t\t\t\t|\n| 30011\t\t| 该 ticket 不属于当前 client\t\t\t\t\t\t\t\t|\n\n\n#### sa-token-oauth2 相关：\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t|\n| 30101\t\t| client_id 不可为空\t\t\t\t\t\t|\n| 30102\t\t| scope 不可为空\t\t\t\t\t\t\t|\n| 30103\t\t| redirect_uri 不可为空\t\t\t\t\t|\n| 30104\t\t| LoginId 不可为空\t\t\t\t\t\t|\n| 30105\t\t| 无效 client_id\t\t\t\t\t\t\t|\n| 30106\t\t| 无效 access_token\t\t\t\t\t\t|\n| 30107\t\t| 无效 client_token\t\t\t\t\t\t|\n| 30108\t\t| Access-Token 不具备指定的 Scope\t\t|\n| 30109\t\t| Client-Token 不具备指定的 Scope\t\t|\n| 30110\t\t| 无效 code 码\t\t\t\t\t\t\t|\n| 30111\t\t| 无效 Refresh-Token\t\t\t\t\t\t|\n| 30112\t\t| 请求的 Scope 暂未签约\t\t\t\t\t|\n| 30113\t\t| 无效 redirect_url\t\t\t\t\t\t|\n| 30114\t\t| 非法 redirect_url\t\t\t\t\t\t|\n| 30115\t\t| 无效 client_secret\t\t\t\t\t\t|\n| 30120\t\t| redirect_uri 不一致\t\t\t\t\t|\n| 30122\t\t| client_id\t不一致\t\t\t\t\t\t|\n| 30125\t\t| 无效 response_type\t\t\t\t\t\t|\n| 30126\t\t| 无效 grant_type\t\t\t\t\t\t|\n| 30127\t\t| 无效 state\t\t\t\t\t\t\t\t|\n| 30141\t\t| 系统暂未开放的授权模式\t\t\t\t\t|\n| 30142\t\t| 应用暂未开放的授权模式\t\t\t\t\t|\n| 30151\t\t| 无效的请求 Method\t\t\t\t\t\t|\n| 30191\t\t| 其它异常\t\t\t\t\t\t\t\t|\n\n\n#### sa-token-jwt 插件相关：\n\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t|\n| 30201\t\t| 对 jwt 字符串解析失败\t\t\t\t\t|\n| 30202\t\t| 此 jwt 的签名无效\t\t\t\t\t\t|\n| 30203\t\t| 此 jwt 的 `loginType` 字段不符合预期\t|\n| 30204\t\t| 此 jwt 已超时\t\t\t\t\t\t\t|\n| 30205\t\t| 没有配置jwt秘钥\t\t\t\t\t\t|\n| 30206\t\t| 登录时提供的账号id为空\t\t\t\t\t|\n\n\n#### sa-token-temp-jwt 插件相关：\n| code码值\t| 含义\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t\t\t\t\t\t\t\t|\n| 30301\t\t| jwt 模式没有提供秘钥\t\t\t\t\t|\n| 30302\t\t| jwt 模式不可以删除 Token\t\t\t\t|\n| 30303\t\t| Token已超时\t\t\t\t\t\t\t|\n\n\n> [!WARNING| label:注意] \n> 部分插件因异常抛出点较少，暂未做状态码细分处理\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/firewall.md",
    "content": "# 防火墙\n\nSa-Token 内置防火墙组件 `SaFirewallStrategy`，用于拦截一些可能造成攻击的危险请求。\n例如当前端提交的 path 为 `/test//login` 时，框架将会强制截断请求，响应以下内容：\n\n``` txt\n非法请求：/test//login\n```\n\n因为包含双斜杠的 path 请求通常被用于鉴权绕行攻击。类似的拦截规则还有很多， `SaFirewallStrategy` 采用 hooks 机制，允许开发者自由扩展拦截规则，\n框架默认具有以下 hook 拦截规则：\n\n- `SaFirewallCheckHookForWhitePath`：请求 path 白名单放行。\n- `SaFirewallCheckHookForBlackPath`：请求 path 黑名单校验。\n- `SaFirewallCheckHookForPathDangerCharacter`：请求 path 危险字符校。\n- `SaFirewallCheckHookForPathBannedCharacter`：请求 path 禁止字符校验。\n- `SaFirewallCheckHookForDirectoryTraversal`：请求 path 目录遍历符检测。\n- `SaFirewallCheckHookForHost`：Host 检测。\n- `SaFirewallCheckHookForHttpMethod`：请求 Method 检测。\n- `SaFirewallCheckHookForHeader`：请求头检测。\n- `SaFirewallCheckHookForParameter`：请求参数检测。\n\n\n### 1、默认 hook 配置：\n\n假设我们想要增加请求 path 黑名单，可以使用如下代码：\n\n``` java\n@Configuration\npublic class SaTokenConfigure {\n\t@PostConstruct\n\tpublic void saTokenPostConstruct() {\n\t\tSaFirewallCheckHookForBlackPath.instance.resetConfig(\"/abc\");\n\t}\n}\n```\n\n现在从浏览器访问 `/abc`，将会被防火墙组件直接拦截：\n\n``` txt\n非法请求：/abc\n```\n\n\n除了 `SaFirewallCheckHookForBlackPath` 以外，其它所有 hook 均可通过此方式重载配置，在此暂不冗余演示。\n\n\n### 2、注册新的 hook 规则：\n你可以使用如下代码注册新的 hook 规则：\n\n``` java\n@PostConstruct\npublic void saTokenPostConstruct() {\n\t// 注册新 hook 演示，拦截所有带有 pwd 参数的请求，拒绝响应 \n\tSaFirewallStrategy.instance.registerHook((req, res, extArg)->{\n\t\tif(req.getParam(\"pwd\") != null) {\n\t\t\tthrow new FirewallCheckException(\"请求中不可包含 pwd 参数\");\n\t\t}\n\t});\n}\n```\n\n除了注册新 hook 规则，你还可以移除默认 hook ，来删减你认为不必要存在的校验规则：\n\n``` java\n// 移除指定类型的 hook 验证\nSaFirewallStrategy.instance.removeHook(SaFirewallCheckHookForHost.class);\n```\n\n\n### 3、利用自动注入特性注册 hook\n如果你的项目属于 IOC 环境（例如 SpringBoot 项目），还可以这样注册 hook：\n``` java\n// 自定义防火墙校验 hook \n@Component\npublic class SaFirewallCheckHookForXxx implements SaFirewallCheckHook {\n    @Override\n    public void execute(SaRequest req, SaResponse res, Object extArg) {\n        System.out.println(\"----------- 自定义防火墙校验 hook \");\n    }\n}\n```\n\n\n### 4、指定异常处理：\n\n被防火墙拦截的请求不会做出格式化响应，因为通常这些请求为非正常业务请求，只需阻断即可，无需前端依照响应做出页面提示。\n\n如果你的业务切实需要对防火墙拦截做出格式化响应，可以通过以下代码完成：\n\n``` java\n@PostConstruct\npublic void saTokenPostConstruct() {\n\t// 指定防火墙校验不通过时的处理方案\n\tSaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {\n\t\tSystem.out.println(\"防火墙校验不通过：\" + e.getMessage());\n\t\ttry {\n\t\t\tHttpServletResponse response = (HttpServletResponse)res.getSource();\n\t\t\tresponse.setContentType(\"application/json;charset=UTF-8\");\n\t\t\tString resJson = SaResult.error(e.getMessage()).toString();\n\t\t\tresponse.getWriter().print(resJson);\n\t\t\tresponse.getWriter().flush();\n\t\t} catch (IOException ex) {\n\t\t\tthrow new RuntimeException(ex);\n\t\t}\n\t};\n}\n```\n\n浏览器将得到以下 json 格式响应：\n\n``` js\n{\n  \"code\": 500,\n  \"msg\": \"非法请求：/abc\",\n  \"data\": null\n}\n"
  },
  {
    "path": "sa-token-doc/fun/git-pr.md",
    "content": "# 如何更新在线文档\n1. 打开要修改的文档页面\n2. 滑动右侧页面滑块, 查看页面内容最下方, 评论区上方\n3. 找到这一行文字\n   \n<img src=\"/big-file/doc/fun/online_1.png\" alt=\"在线编辑提示\" />\n\n4. 点击Gitee或GitHub按钮中的任意一个, 国内用户推荐使用 [Gitee](https://gitee.com) (请先注册登录后再往下浏览)\n5. 此时会进入当前页面源码预览页面,找到下方按钮组\n\n<img src=\"/big-file/doc/fun/online_2.png\" alt=\"按钮组\" />\n\n6. 点击编辑按钮\n7. 此时进入待修改页面的源码页面, 按照markdown格式编辑为需要的结果(Ctrl+P可查看最终效果,再次按下可恢复源码界面)\n8. 滑动到最下方点击提交审核即可\n\n\n# 如何提交代码\n## 环境安装过程\n1. 在本地[下载Git软件](https://pc.qq.com/detail/13/detail_22693.html)并安装\n2. 配置用户名和邮件地址(Gitee或GitHub上关联的邮箱)\n\n```\ngit config --global user.name \"这里替换为你在项目中希望展示的昵称\"\ngit config --global user.email 这里替换为你的关联邮箱\n// 查看是否配置正确\ngit config --list  \n```\n\n3. 为了让Gitee服务器认可你的身份,需要配置一次SSH Key, 在本地生成密匙对, 公钥上传到Gitee服务器后台\n4. 具体方法见[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)\n5. 最小开发环境安装包括[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)\n6. 在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), 基础部分不再赘述\n\n## 项目下载过程\n1. 点击[Gitee](https://gitee.com/dromara/sa-token)或[Github](https://github.com/dromara/sa-token)进入Sa-Token项目主页, 以下以Gitee为例,Github类似(请先注册登录后再往下浏览)\n2. 找到页面右上角的按钮组, 点击Forked按钮\n   \n<img src=\"/big-file/doc/fun/code_1.png\" alt=\"按钮组\" />\n\n3. 选择个人仓库并点击确认\n4. 此时在你的个人仓库中会多了一个Sa-Token项目\n5. 在新的Sa-Token项目中, 点击 <img src=\"/big-file/doc/fun/code_2.png\" alt=\"克隆/下载\" /> 按钮, 点击弹出框里面的复制按钮\n6.  在本地某空文件夹下右键选择: git bash here\n\n<img src=\"/big-file/doc/fun/code_4.png\" alt=\"git bash\" />\n\n<img src=\"/big-file/doc/fun/code_3.png\" alt=\"git bash 打开后的图\" />\n\n14. 在里面输入如下命令, 按换行后自动下载整个项目\n\n```\ngit clone 这里替换为复制后的链接\n```\n\n## 项目载入过程\n1. 下载结束后, 开启 idea, 选择 File->Open... 选中项目下载后的Sa-Token文件夹(Trust Project 相信此项目, 否则不可编辑)\n2. 这时项目就是可编辑状态, 修改完代码并测试完成后即可提交\n\n## 项目暂存并提交远程\n### 方式一\n1. 在idea中打开项目进入Commit选项\n\n<img src=\"/big-file/doc/fun/code_5.png\" alt=\"本地暂存\" />\n\n2. 勾选需要本地暂存的文件\n3. 在同一页面的下方输入提示信息\n\n<img src=\"/big-file/doc/fun/code_6.png\" alt=\"提示信息\" />\n\n4. 点击Commit按钮暂存到本地, 点击Commit and Push按钮暂存之后提交到远程\n### 方式二\n1. 除了点击Commit and Push按钮外,还有一个地方可以提交git\n\n<img src=\"/big-file/doc/fun/code_7.png\" alt=\"git按钮\" />\n\n2. 位置在idea右上方的工具栏里面\n3. 指向左下箭头为拉取项目,可以随时更新\n4. 打对号为本地暂存\n5. 指向右上箭头提交远程\n## 私人项目推送到主项目\n1. 提交后进入Gitee个人仓库中克隆的Sa-Token项目\n2. 找到下图的Pull Request按钮\n\n<img src=\"/big-file/doc/fun/code_8.png\" alt=\"工具栏\" />\n\n3. 点击提交, 进入如下页面\n\n<img class=\"s-width\" src=\"/big-file/doc/fun/code_9.png\" alt=\"提交信息填写页面\" />\n\n4. 在这里,你可以选择要提交的分支,一般都是dev开发分支.可以填写合并信息,其他测试审查之类的可以不填写, 最后点击创建即可完成一次提交.\n\n## 远程项目更新\n1. 有时候主项目更新了,之前克隆的项目代码陈旧,如何处理?\n2. 在个人仓库的Sa-Token项目主页面中, 找到下图的圆圈\n\n<img src=\"/big-file/doc/fun/code_10.png\" alt=\"更新按钮\" />\n\n3. 点击右侧圆圈按钮后Gitee会自动同步主项目, 这样就不用像我之前一样,删除项目又重新fork了.\n\n## 为什么在国内推荐Gitee\n1. 近期Github下载网速较慢\n2. Gitee上中文界面方便操作"
  },
  {
    "path": "sa-token-doc/fun/issue-template.md",
    "content": "# issue 提问模板\n\n在线提问链接：[Gitee issue](https://gitee.com/dromara/sa-token/issues)、[GitHub issue](https://github.com/dromara/sa-token/issues)\n\n> [!TIP| label:请在新建 issue 时，尽量复制模板格式进行提交] \n> 1. 提交之前率先参考 <a href=\"#/more/common-questions\" target=\"_blank\">Sa-Token 常见问题解答</a> 以及善用 Gitee issues 搜索功能，查阅问题是否已有答案，已存在的 issue 就不要再重复提交了。\n> 2. 问题已得到处理的 issue 请大家及时手动关闭，如果超过24小时没有追问，我们将默认提交者已找到解决方案，关闭issue。\n> 3. 有时候 issue 提交之后，没有得到及时回复，大家可以加入QQ群@管理员寻求帮助。\n> 4. 请大家新建 issue 时删除不必要的模板信息、精简语句、**做好代码排版**，对于不方便描述的业务场景，可参阅 <a href=\"#/more/noun-intro\" target=\"_blank\">Sa-Token 名词解释</a> 方便组织语句，这样有助于减低大家的沟通成本。\n> 5. **代码截图要带上行号！报错信息要把异常堆栈截全！页面截图要把地址栏带上！Ajax请求要把请求地址、请求头、请求参数都截全！**\n\n\n--- \n\n\n### 预期不符：\n``` js\n### 使用版本:\n\n\n### 涉及的功能模块：\n\n\n### 测试步骤：\n+ 我经过以下步骤测试：\n\n+ 得出以下结果：\n\n+ 其中第 xx 行的代码输出表现 和文档上描述的不一致：\n\n+ 我的理解是：\n\n请问，是我的理解不对，还是文档出了问题？\n```\n\n\n### bug反馈：\n``` js\n### 使用版本:\n\n\n### 报错信息：\n\n\n### 希望结果：\n\n\n### 复现步骤：\n\n\n< 备注：如果复现步骤比较复杂，请将 demo 上传到 gitee 并留下地址 >\n```\n\n\n### 功能提问：\n``` js\n### 对以下问题有疑问:\n\n\n< 备注：请尽量详细描述问题所在 >\n```\n\n\n### 建议增加新功能：\n``` js\n### 建议增加的新功能:\n\n\n### 应用场景阐述：\n\n\n< 备注：请尽量详细描述功能应用场景 >\n```\n\n\n### 踩坑记录：\n``` js\n### 遇到的问题:\n\n\n### 解决方案：\n\n\n< 备注：请尽量描述详细一点，为后人提供清晰的排查思路，人人为我，我为人人 >\n```\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/jur-cache.md",
    "content": "# 参考：将权限数据放在缓存里\n前面我们讲解了如何通过`StpInterface`接口注入权限数据，框架默认是不提供缓存能力的，如果你想减小数据库的访问压力，则需要将权限数据放到缓存中\n\n--- \n\n参考如下：\n``` java\n/**\n * 自定义权限验证接口扩展 \n */\n@Component  \npublic class StpInterfaceImpl implements StpInterface {\n    \n\t// 返回一个账号所拥有的权限码集合\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\n\t\t// 1. 声明权限码集合\n\t\tList<String> list = new ArrayList<>();\n\n\t\t// 2. 遍历角色列表，查询拥有的权限码\n\t\tfor (String roleId : getRoleList(loginId, loginType)) {\n\t\t\tList<String> permissionList = (List<String>)SaManager.getSaTokenDao().getObject(\"satoken:role-find-permission:\" + roleId);\n\t\t\tif(permissionList == null) {\n\t\t\t\t// 从数据库查询这个角色 id 所拥有的权限列表\n\t\t\t\tpermissionList = ...\n\t\t\t\t// 查好后，set 到缓存中\n\t\t\t\tSaManager.getSaTokenDao().setObject(\"satoken:role-find-permission:\" + roleId, permissionList, 60 * 60 * 24 * 30);\n\t\t\t}\n\t\t\tlist.addAll(permissionList);\n\t\t}\n\n\t\t// 3. 返回权限码集合\n\t\treturn list;\n\t}\n\n\t// 返回一个账号所拥有的角色标识集合\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\tList<String> roleList = (List<String>)SaManager.getSaTokenDao().getObject(\"satoken:loginId-find-role:\" + loginId);\n\t\tif(roleList == null) {\n\t\t\t// 从数据库查询这个账号id拥有的角色列表，\n\t\t\troleList = ... \n\t\t\t// 查好后，set 到缓存中\n\t\t\tSaManager.getSaTokenDao().setObject(\"satoken:loginId-find-role:\" + loginId, roleList, 60 * 60 * 24 * 30);\n\t\t}\n\t\treturn roleList;\n\t}\n\t\n}\n```\n\n##### 疑问：为什么不直接缓存 `[账号id->权限列表]`的关系，而是 `[账号id -> 角色id -> 权限列表]`？\n\n<!-- ``` java\n// 在一个账号登录时写入其权限数据\nRedisUtil.setValue(\"账号id\", <权限列表>);\n\n// 然后在`StpInterface`接口中，如下方式获取\nList<String> list = RedisUtil.getValue(\"账号id\");\n``` -->\n\n答：`[账号id->权限列表]`的缓存方式虽然更加直接粗暴，却有一个严重的问题：\n\n- 通常我们系统的权限架构是RBAC模型：权限与用户没有直接的关系，而是：用户拥有指定的角色，角色再拥有指定的权限\n- 而这种'拥有关系'是动态的，是可以随时修改的，一旦我们修改了它们的对应关系，便要同步修改或清除对应的缓存数据 \n\n现在假设如下业务场景：我们系统中有十万个账号属于同一个角色，当我们变动这个角色的权限时，难道我们要同时清除这十万个账号的缓存信息吗？\n这显然是一个不合理的操作，同一时间缓存大量清除容易引起Redis的缓存雪崩\n\n而当我们采用 `[账号id -> 角色id -> 权限列表]` 的缓存模型时，则只需要清除或修改 `[角色id -> 权限列表]` 一条缓存即可 \n\n一言以蔽之：权限的缓存模型需要跟着权限模型走，角色缓存亦然 \n\n\n"
  },
  {
    "path": "sa-token-doc/fun/log.md",
    "content": "# 参考：全局 Log 输出\n\n--- \n\n### 打开全局日志输出\n\n以下配置可以打开全局日志输出：\n\n<!---------------------------- tabs:start ---------------------------->\n\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\t# 是否输出操作日志 \n\tis-log: true\n```\n\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 是否输出操作日志 \nsa-token.is-log=true\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n此配置项打开之后，框架将会在账号登录、注销、二级认证 等关键性步骤打印日志，以方便项目开发调试。\n\n框架默认将日志信息打印到控制台，如果需要将日志输出到其它地方，你可以重写 SaLog 对象，例如以下代码将会把日志转接到 Slf4j 下：\n\n``` java\n/**\n * 将 Sa-Token log 信息转接到 Slf4j \n */\n@Component\npublic class SaLogForSlf4j implements SaLog {\n\tLogger log = LoggerFactory.getLogger(SaLogForSlf4j.class);\n\t\n\t@Override\n\tpublic void trace(String str, Object... args) {\n\t\tlog.trace(str, args);\n\t}\n\t@Override\n\tpublic void debug(String str, Object... args) {\n\t\tlog.debug(str, args);\n\t}\n\t@Override\n\tpublic void info(String str, Object... args) {\n\t\tlog.info(str, args);\n\t}\n\t@Override\n\tpublic void warn(String str, Object... args) {\n\t\tlog.warn(str, args);\n\t}\n\t@Override\n\tpublic void error(String str, Object... args) {\n\t\tlog.error(str, args);\n\t}\n\t@Override\n\tpublic void fatal(String str, Object... args) {\n\t\tlog.error(str, args);\n\t}\n}\n```\n\n重新启动项目，观察日志打印变化。\n\n### 增加API访问日志\n\n手动增加 API 请求日志信息，这将非常有助于你调试代码，例如：\n\n``` java\n@Bean\npublic SaServletFilter getSaServletFilter() {\n\treturn new SaServletFilter()\n\t\t\t.addInclude(\"/**\")\n\t\t\t.addExclude(\"/favicon.ico\")\n\t\t\t.setAuth(obj -> {\n\t\t\t\t// 输出 API 请求日志，方便调试代码 \n\t\t\t\tSaManager.getLog().debug(\"----- 请求path={}  提交token={}\", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());\n\t\t\t\t// 其它校验代码... \n\t\t\t})\n\t\t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n\t\t\t.setError(e -> {\n\t\t\t\tSystem.out.println(\"---------- sa全局异常 \");\n\t\t\t\treturn SaResult.error(e.getMessage());\n\t\t\t})\n\t\t\t;\n}\n```"
  },
  {
    "path": "sa-token-doc/fun/not-login-scene.md",
    "content": "# NotLoginException 场景值\n\n本篇介绍如何根据`NotLoginException`异常的场景值，来定制化处理未登录的逻辑 <br/>\n应用场景举例：未登录、被顶下线、被踢下线等场景需要不同方式来处理 \n\n\n## 何为场景值\n在前面的章节中，我们了解到，在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常，而同为未登录异常却有五种抛出场景的区分 \n\n| 场景值   | 对应常量  |  含义说明                         |\n|---   \t  |---        |---                            |\n| -1      | NotLoginException.NOT_TOKEN  \t| 未能从请求中读取到有效 token         |\n| -2      | NotLoginException.INVALID_TOKEN\t| 已读取到 token，但是 token 无效  |\n| -3      | NotLoginException.TOKEN_TIMEOUT\t| 已读取到 token，但是 token 已经过期 ([详](/fun/token-timeout)) |\n| -4      | NotLoginException.BE_REPLACED\t| 已读取到 token，但是 token 已被顶下线  |\n| -5      | NotLoginException.KICK_OUT\t\t| 已读取到 token，但是 token 已被踢下线  |\n| -6      | NotLoginException.TOKEN_FREEZE\t| 已读取到 token，但是 token 已被冻结  |\n| -7      | NotLoginException.NO_PREFIX\t\t| 未按照指定前缀提交 token\t\t  |\n\n\n\n那么，如何获取场景值呢？废话少说直接上代码：\n\n\n``` java\n// 全局异常拦截（拦截项目中的NotLoginException异常）\n@ExceptionHandler(NotLoginException.class)\npublic SaResult handlerNotLoginException(NotLoginException nle)\n\t\tthrows Exception {\n\n\t// 打印堆栈，以供调试\n\tnle.printStackTrace(); \n\t\n\t// 判断场景值，定制化异常信息 \n\tString message = \"\";\n\tif(nle.getType().equals(NotLoginException.NOT_TOKEN)) {\n\t\tmessage = \"未能读取到有效 token\";\n\t}\n\telse if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {\n\t\tmessage = \"token 无效\";\n\t}\n\telse if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {\n\t\tmessage = \"token 已过期\";\n\t}\n\telse if(nle.getType().equals(NotLoginException.BE_REPLACED)) {\n\t\tmessage = \"token 已被顶下线\";\n\t}\n\telse if(nle.getType().equals(NotLoginException.KICK_OUT)) {\n\t\tmessage = \"token 已被踢下线\";\n\t}\n\telse if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {\n\t\tmessage = \"token 已被冻结\";\n\t}\n\telse if(nle.getType().equals(NotLoginException.NO_PREFIX)) {\n\t\tmessage = \"未按照指定前缀提交 token\";\n\t}\n\telse {\n\t\tmessage = \"当前会话未登录\";\n\t}\n\t\n\t// 返回给前端\n\treturn SaResult.error(message);\n}\n```\n\n<br/>\n注意：以上代码并非处理逻辑的最佳方式，只为以最简单的代码演示出场景值的获取与应用，大家可以根据自己的项目需求来定制化处理\n\n"
  },
  {
    "path": "sa-token-doc/fun/plugin-dev.md",
    "content": "# Sa-Token 插件开发指南 \n\n<!-- > 注：为 Sa-Token 提交插件请在 sa-token-three-plugin 仓库进行：[点击跳转](https://gitee.com/sa-tokens/sa-token-three-plugin) -->\n\n插件，从字面意思理解就是可拔插的组件，作用是在不改变 Sa-Token 现有架构的情况下，替换或扩展一部分底层代码逻辑。\n\n--- \n\n\n## 1、插件开发\n\t\t\t\n为 Sa-Token 开发插件非常简单，以下是几种可行的方式：\n\n- 1、自定义全局策略。\n- 2、更改全局组件实现。\n- 3、实现自定义SaTokenContext。\n- 4、其它自由扩展。\n\n下面依次介绍这几种方式。\n\n### 方式1：自定义全局策略\n\nSa-Token 将框架的一些关键逻辑抽象出一个统一的概念 —— 策略，并统一定义在 `SaStrategy` 中，源码参考：[SaStrategy](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java) 。\n\nSaStrategy 的每一个函数都可以单独重写，以 “自定义Token生成策略” 这一需求为例：\n\n``` java\n// 重写 Token 生成策略 \nSaStrategy.instance.createToken = (loginId, loginType) -> {\n\treturn SaFoxUtil.getRandomString(60);    // 随机60位长度字符串\n};\n```\n\n就像变量的重新赋值一样，你只需重新指定一个新的策略函数，即可自定义 Token 生成的逻辑。\n\n\n### 方式2：更改全局组件实现\n\n你可以找到不符合你需求的组件，重新定义一个子类，以 临时令牌认证 模块为例，你需要自定义 `SaTempTemplate` 的子类：\n\n``` java\n/**\n * 临时认证模块 自定义子类实现 \n */\n@Component\npublic class MySaTempTemplate extends SaTempTemplate {\n\n    @Override\n    public String createToken(Object value, long timeout, boolean isRecordIndex) {\n        System.out.println(\"------- 自定义一些逻辑 createToken \");\n        return super.createToken(value, timeout, isRecordIndex);\n    }\n\n    @Override\n    public Object parseToken(String token) {\n        System.out.println(\"------- 自定义一些逻辑 parseToken \");\n        return super.parseToken(token);\n    }\n\n}\n```\n\n### 方式3：实现自定义SaTokenContext\nSaTokenContext 是对接不同框架的上下文接口，篇幅限制，可参考：[自定义 SaTokenContext 指南](/fun/sa-token-context)\n\n\n### 方式4：其它自由扩展\n这种方式就无需注入什么全局组件替换内部实现了，你可以在 Sa-Token 的基础之上封装任何代码，进行功能扩展。\n\n\n\n## 2、插件注册\n\n在你完成插件开发之后，你还需要考虑一个问题，如何让插件代码注入到项目中。\n\n首先这里需要分两种情况：\n- 情况1：只打算自己的项目使用这个插件。\n- 情况2：准备提交 pr 到 Sa-Token 仓库，让更多人使用。\n\n\n### 情况1：只打算自己的项目使用这个插件\n\n这种情况比较简单，如果是 SpringBoot 项目，你可以在自定义插件类上添加注解 `@Component`：\n\n``` java\n@Component\npublic class MySaTempTemplate extends SaTempTemplate {\n\t// ... \n}\n```\n\n这样在项目启动时， sa-token-spring-boot-starter 集成包将会扫描到这个自定义组件，注入到框架中。\n\n如果是重写全局策略的代码，也可以通过 `@PostConstruct` 注解做到项目启动时自动执行：\n\n``` java\n@PostConstruct\npublic void rewriteSaStrategy() {\n\t// 重写 token 生成策略\n\tSaStrategy.instance.createToken = (loginId, loginType) -> {\n\t\treturn SaFoxUtil.getRandomString(60);\n\t};\n}\n```\n\n如果是非 SpringBoot 项目，项目环境无法做到自动注入，保底的方案是在 main 方法中，手动注册组件：\n\n``` java\npublic static void main(String[] args) {\n\t// 示例：手动替换 Sa-Token 内部组件\n\t// Sa-Token 大部分全局组件都定义在 SaManager 之上，参考：https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java \n\tSaManager.setSaTempTemplate(new MySaTempTemplate());\n\t\n\t// 示例：手动重写 Sa-Token 全局策略\n\tSaStrategy.instance.createToken = (loginId, loginType) -> {\n\t\treturn SaFoxUtil.getRandomString(60);\n\t};\n}\n```\n\n\n### 情况2：准备提交 pr 到 Sa-Token 仓库，让更多人使用。\n\n这种情况稍微复杂一些，因为你基本上很难：通过在插件内部写一些代码，帮助“插件使用者”注册插件到项目中。\n\n一种解决方案是：难办，那就别办了。\n\n对，就是你只负责开发相对应的自定义组件，而将自定义组件的注册过程完全交给使用者，这并不是妥协的选择，反而会给插件使用者更大的自由度，\nsa-token-jwt、sa-token-thymeleaf 等官方插件都是这样做的。\n\n如果你觉得还是完成插件的自动注入比较好，也是有办法的，那就是利用 SPI 机制来注册组件。\n\n（关于 java SPI 机制，网上教程众多，此处暂不详细介绍，不熟悉的同学可以直接向 deepseek 等 AI 工具提问，给你讲的明明白白的）\n\n你需要考虑一点：这个插件是专门给 SpringBoot 项目使用的，还是面向 Solon、JFinal 等任意项目使用：\n\n#### 如果是：SpringBoot 专用插件\n\n如果这个插件只打算给 SpringBoot 项目使用，可以利用 SpringBoot 的 SPI 机制注册插件\n\nSpringBoot2 格式：创建 `resources\\META-INF\\spring.factories` 文件：\n\n``` txt\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=插件完全限定名\n```\n\nSpringBoot3 格式：创建 `resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件：\n\n``` txt\n插件完全限定名\n```\n\n这样在别人引入此插件时，便会根据 SPI 文件指定的地址去加载插件类，做到插件引入即注册的效果。\n\n\n#### 如果是：通用型插件\n\n通用型插件则不能使用 SpringBoot 的 SPI 机制去注册组件，因为其它项目是无法识别 SpringBoot SPI 文件的，\n好在 Sa-Token 提供了自己的 SPI 机制，所有环境均可使用：\n\n1、新建 `SaTokenPluginForXxx` 类，此类需要 `implements SaTokenPlugin` 接口，并且推荐定义在 `cn.dev33.satoken.plugin` 下：\n\n``` java\n/**\n * SaToken 插件安装：插件作用描述\n */\npublic class SaTokenPluginForXxx implements SaTokenPlugin {\n    @Override\n    public void install() {\n        // 书写需要在项目启动时执行的代码，例如：\n        // SaManager.setXxx(new SaXxxForXxx());\n    }\n}\n```\n\n2、新建 `resources\\META-INF\\satoken\\cn.dev33.satoken.plugin.SaTokenPlugin` 文件，填写上插件类的完全限定名地址\n``` txt\ncn.dev33.satoken.plugin.SaTokenPluginForXxx\n```\n\n这样便可以在项目启动时，被 Sa-Token 插件管理器加载到此插件，执行自定义 `SaTokenPluginForXxx` 实现类的 `install` 方法，完成插件安装。\n\n\n## 3、练练手\n\n学废了吗？给你出个题练练手：\n\n开发一个 `sa-token-hutool-json` 插件，要求引入该插件后，自动替换掉 Sa-Token 的 json 序列化方案为 hutool-json 模块。\n\n如果没有思路，可以参考一下 `sa-token-fastjson` 的插件源码实现哦。\n\n\n<!-- ##### 3、开始测试：\n\n``` java \n// 根据 value 创建一个 token \nString token = SaTempUtil.createToken(\"10014\", 120);\nSystem.out.println(\"生成的Token为：\" + token);\n\nObject value = SaTempUtil.parseToken(token);\nSystem.out.println(\"将Token解析后的值为：\" + value);\n```\n\n观察控制台输出，检验自定义实现类是否注入成功：\n\n![dev-plugin-print](https://oss.dev33.cn/sa-token/doc/dev-plugin-print.png) -->\n\n\n<!-- ### 5、练练手\n熟悉了插件开发流程，下面的 [ 待开发插件列表 ] 或许可以给你提供一个练手的方向。\n\n##### SaTokenContext 实现：\n\n| 插件\t\t\t\t\t\t| 功能\t\t\t\t\t\t| 状态\t\t\t|\n| :--------\t\t\t\t\t| :--------\t\t\t\t\t| :--------\t\t|\n| sa-token-solon-starter\t| Sa-Token 与 Solon 的整合\t| <font color=\"green\" >已完成</font>\t\t|\n| sa-token-jfinal-starter\t| Sa-Token 与 JFinal 的整合\t| <font color=\"green\" >已完成</font>\t\t|\n| sa-token-hasor-starter\t| Sa-Token 与 Hasor 的整合\t| 待开发\t\t\t|\n\n##### 标签方言：\n\n| 插件\t\t\t\t\t\t\t| 功能\t\t\t\t\t\t\t| 状态\t\t\t|\n| :--------\t\t\t\t\t\t| :--------\t\t\t\t\t\t| :--------\t\t|\n| sa-token-thymeleaf\t| Sa-Token 与 thymeleaf 的整合\t| <font color=\"green\" >已完成</font>\t\t\t|\n| sa-token-freemarker\t| Sa-Token 与 freemarker 的整合\t| 待开发\t\t\t|\n| sa-token-jsp\t\t\t| Sa-Token 与 jsp 的整合\t\t\t| 待开发\t\t\t|\n| sa-token-velocity\t\t| Sa-Token 与 velocity 的整合\t| 待开发\t\t\t|\n| sa-token-beetl\t\t| Sa-Token 与 beetl 的整合\t\t| 待开发\t\t\t|\n\n##### 持久层扩展：\n\n| 插件\t\t\t\t\t\t\t| 功能\t\t\t\t\t\t\t| 状态\t\t\t|\n| :--------\t\t\t\t\t\t| :--------\t\t\t\t\t\t| :--------\t\t|\n| sa-token-redis\t\t\t| Sa-Token 与 Redis 的整合\t\t| <font color=\"green\" >已完成</font>\t\t\t|\n| sa-token-memcached\t\t| Sa-Token 与 memcached 的整合\t| 待开发\t\t\t|\n\n##### 其它：\n任何你认为有价值的功能代码，都可以扩展为插件。 -->\n\n\n<!-- ### 6、发布代码\n插件开发完毕之后，你可以将其 pr 到 [sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin)，或：\n\n上传到 gitee/github 作为独立项目维护，并发布到 Maven 中央仓库，参考这篇：[https://juejin.cn/post/6844904104834105358](https://juejin.cn/post/6844904104834105358) -->\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/refer-info.md",
    "content": "# 参考资料\n记录 Sa-Token 框架开发中参考过的一些资料。（由 2025-3-1 开始整理）\n\n\n- Sa-Token对url过滤不全存在的风险点 - https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA\n- SpringBoot 热拔插 AOP 组件：\n\t- https://www.jb51.net/program/297714rev.htm\n\t- https://www.bilibili.com/video/BV1WZ421W7Qx\n\t- https://blog.csdn.net/Tomwildboar/article/details/139199801\n- 单元测试 - https://www.cnblogs.com/flypig666/p/11505277.html\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/sa-token-context--backup.md",
    "content": "# 自定义 SaTokenContext 指南 \n\n目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包，\n如果我们使用的 Web 框架不在上述列表之中，则需要自定义 SaTokenContext 接口的实现完成整合工作。\n\n---\n\n### 1、SaTokenContext是什么，为什么要实现 SaTokenContext 接口？\n\n在鉴权中，必不可少的步骤就是从 `HttpServletRequest` 中读取 Token，然而并不是所有框架都具有 HttpServletRequest 对象，例如在 WebFlux 中，只有 `ServerHttpRequest`，\n在一些其它Web框架中，可能连 `Request` 的概念都没有。\n\n那么，Sa-Token 如何只用一套代码就对接到所有 Web 框架呢？\n\n解决这个问题的关键就在于 `SaTokenContext` 接口，此接口的作用是屏蔽掉不同 Web 框架之间的差异，提供统一的调用API：\n\n<img class=\"s-w\" src=\"/big-file/doc/fun/sa-token-context.svg\" alt=\"sa-token-context\" />\n\n\nSaTokenContext只是一个接口，没有工作能力，这也就意味着 SaTokenContext 接口的实现是必须的。\n那么疑问来了，我们之前在 SpringBoot 中引用 Sa-Token 时为什么可以直接使用呢？\n\n其实原理很简单，`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)，\n并且根据 Spring 的自动注入特性，在项目启动时注入到 Sa-Token 中，做到“开箱即用”。\n\n那么如果我们使用不是 Spring 框架，是不是就必须得手动实现 `SaTokenContext` 接口？答案是肯定的，脱离Spring 环境后，我们就不能再使用`sa-token-spring-boot-starter`集成包了，\n此时我们只能引入 `sa-token-core` 核心包，然后手动实现 `SaTokenContext` 接口。\n\n不过不用怕，这个工作很简单，只要跟着下面的文档一步步来，你就可以将 Sa-Token 对接到任意Web框架中。\n\n\n### 2、实现 Model 接口\n我们先来观察一下 `SaTokenContext` 接口的签名：\n``` java\n/**\n * Sa-Token 上下文处理器\n */\npublic interface SaTokenContext {\n\n\t/**\n\t * 获取当前请求的 [Request] 对象\n\t */\n\tpublic SaRequest getRequest();\n\n\t/**\n\t * 获取当前请求的 [Response] 对象\n\t */\n\tpublic SaResponse getResponse();\n\n\t/**\n\t * 获取当前请求的 [存储器] 对象 \n\t */\n\tpublic SaStorage getStorage();\n\n\t/**\n\t * 校验指定路由匹配符是否可以匹配成功指定路径 \n\t */\n\tpublic boolean matchPath(String pattern, String path);\n\n}\n```\n\n你可能对 `SaRequest` 比较疑惑，这个对象是干什么用的？正如每个 Web 框架都有 Request 概念的抽象，Sa-Token 也封装了 `Request`、`Response`、`Storage`三者的抽象：\n\n- `Request`：请求对象，携带着一次请求的所有参数数据。参考：[SaRequest.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java)。\n- `Response`：响应对象，携带着对客户端一次响应的所有数据。参考：[SaResponse.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java)。\n- `Storage`：请求上下文对象，提供 [一次请求范围内] 的上下文数据读写。参考：[SaStorage.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java)。\n\n\n因此，在实现 `SaTokenContext` 之前，你必须先实现这三个 Model 接口。\n\n先别着急动手，如果你的 Web 框架是基于 Servlet 规范开发的，那么 Sa-Token 已经为你封装好了三个 Model 接口的实现，你要做的就是引入 `sa-token-servlet`包即可：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证（ServletAPI 集成包） -->\n<dependency>\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-servlet</artifactId>\n    <version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证（ServletAPI 集成包）\nimplementation 'cn.dev33:sa-token-servlet:${sa.top.version}'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n\n如果你的 Web 框架不是基于 Servlet 规范，那么你就需要手动实现这三个 Model 接口，我们可以参考 `sa-token-servlet` 是怎样实现的：\n[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)、\n[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)、\n[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)。\n\n\n### 3、实现 SaTokenContext 接口\n\n接下来我们奔入主题，提供 `SaTokenContext` 接口的实现，同样我们可以参考 Spring 集成包是怎样实现的：\n\n``` java\n/**\n * Sa-Token 上下文处理器 [ SpringMVC版本实现 ] \n */\npublic class SaTokenContextForSpring implements SaTokenContext {\n\n\t/**\n\t * 获取当前请求的Request对象\n\t */\n\t@Override\n\tpublic SaRequest getRequest() {\n\t\treturn new SaRequestForServlet(SpringMVCUtil.getRequest());\n\t}\n\n\t/**\n\t * 获取当前请求的Response对象\n\t */\n\t@Override\n\tpublic SaResponse getResponse() {\n\t\treturn new SaResponseForServlet(SpringMVCUtil.getResponse());\n\t}\n\n\t/**\n\t * 获取当前请求的 [存储器] 对象 \n\t */\n\t@Override\n\tpublic SaStorage getStorage() {\n\t\treturn new SaStorageForServlet(SpringMVCUtil.getRequest());\n\t}\n\t\n\t/**\n\t * 校验指定路由匹配符是否可以匹配成功指定路径 \n\t */\n\t@Override\n\tpublic boolean matchPath(String pattern, String path) {\n\t\treturn SaPathMatcherHolder.getPathMatcher().match(pattern, path);\n\t}\n\n}\n```\n\n详细参考：\n[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)\n\n\n### 4、将自定义实现注入到 Sa-Token 框架中\n\n有了 `SaTokenContext` 接口的实现，我们还需要将这个实现类注入到 Sa-Token 之中，伪代码参考如下：\n``` java\n/**\n * 程序启动类\n */\npublic class Application {\n\n\tpublic static void main(String[] args) {\n\t\t// 框架启动\n\t\tXxxApplication.run(xxx);\n\t\t\n\t\t// 将自定义的 SaTokenContext 实现类注入到框架中 \n\t\tSaTokenContext saTokenContext = new SaTokenContextForXxx();\n\t\tSaManager.setSaTokenContext(saTokenContext);\n\t}\n\t\n}\n```\n\n如果你使用的框架带有自动注入特性，那就更简单了，参考 Spring 集成包的 Bean 注入流程：\n[注册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)、\n[注入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)\n\n\n### 5、启动项目\n\n启动项目，尝试打印一下 `SaManager.getSaTokenContext()` 对象，如果输出的是你的自定义实现类，那就证明你已经自定义 `SaTokenContext` 成功了，\n快来体验一下 Sa-Token 的各种功能吧。\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/sa-token-context.md",
    "content": "# 自定义 SaTokenContext 指南 \n\n目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包，\n如果我们使用的 Web 框架不在上述列表之中，则需要自定义 SaTokenContext 相关接口完成整合工作。\n\n我们需要关注的主要就是四个接口：\n\n- SaTokenContext：上下文管理器。\n- SaRequest：请求对象，携带着一次请求的所有参数数据。\n- SaResponse：响应对象，携带着对客户端一次响应的所有数据。\n- SaStorage：请求上下文对象，提供 [一次请求范围内] 的上下文数据读写。\n\n---\n\n\n### 上下文包装类 \n\n在鉴权中，必不可少的步骤就是从 `HttpServletRequest` 中读取 Token，然而当我们调用 `StpUtil.isLogin()` 获取当前会话是否登录时，\n我们并没有传递 `HttpServletRequest` 参数，框架是怎么读取出来 Token 的呢？\n\n以 SpringBoot 项目为例，Sa-Token 框架会自动注册一个全局过滤器，在每次接收到请求时，将 `HttpServletRequest` 对象保存在 `ThreadLocal` 之中。\n\n在后续的方法中，如果你调用了 `StpUtil.isLogin()` 等方法，框架便会从 `ThreadLocal` 中获取 `HttpServletRequest` 对象，从而进一步读取 token 等信息。\n\n让我们来看一下具体的代码细节，全局上下文初始化过滤器：\n\n``` java\n/**\n * SaTokenContext 上下文初始化过滤器 (基于 Servlet)\n */\n@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)\npublic class SaTokenContextFilterForServlet implements Filter {\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\t\ttry {\n\t\t\tSaTokenContextServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response);\n\t\t\tchain.doFilter(request, response);\n\t\t} finally {\n\t\t\tSaTokenContextServletUtil.clearContext();\n\t\t}\n\t}\n}\n```\n\n进一步追踪 `SaTokenContextServletUtil.setContext` 方法：\n\n``` java\npublic static void setContext(HttpServletRequest request, HttpServletResponse response) {\n\tSaRequest req = new SaRequestForServlet(request);\n\tSaResponse res = new SaResponseForServlet(response);\n\tSaStorage stg = new SaStorageForServlet(request);\n\tSaManager.getSaTokenContext().setContext(req, res, stg);\n}\n```\n\n此处有一个细节，为什么保存的不是原生 `HttpServletRequest` 与 `HttpServletResponse`，而是 `SaRequest`、`SaResponse`、`SaStorage` 三个包装对象？\n\n因为并不是所有的 web 框架都具有 `HttpServletRequest` 对象，例如在 WebFlux 中，只有 `ServerHttpRequest`，\n在一些其它Web框架中，可能连 `Request` 的概念都没有。\n\nSa-Token 为了一套代码对接所有的 Web 框架，就在原生请求对象的基础上又封装了一层 `SaTokenContext` 相关接口，用于屏蔽掉不同 Web 框架之间的差异，提供统一的调用API：\n\n<img src=\"/big-file/doc/fun/sa-token-context-2.svg\" alt=\"sa-token-context\" />\n\n因此，要对接不同的 Web 框架，就要针对不同的 Web 框架封装不同版本的 `SaRequest`、`SaResponse`、`SaStorage` 包装类对象。\n\n如果你的 Web 框架是基于 Servlet 规范开发的，那么你可以直接引入 `sa-token-servlet`，这个包封装了针对 Servlet 规范的上下文包装类对象：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证（ServletAPI 集成包） -->\n<dependency>\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-servlet</artifactId>\n    <version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证（ServletAPI 集成包）\nimplementation 'cn.dev33:sa-token-servlet:${sa.top.version}'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n\n如果你的 web 框架不是基于 Servlet 规范开发的，也问题不大，手动实现一下即可，参考一下 Servlet 包是怎么做的：\n[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)、\n[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)、\n[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)。\n\n封装好包装类对象之后，接下来要做的就是在这个 Web 框架中注册一个全局过滤器，将包装类对象保存到“全局上下文管理器”之中，以备调用：\n\n``` java\nSaRequest req = new SaRequestForXxx(request);\nSaResponse res = new SaResponseForXxx(response);\nSaStorage stg = new SaStorageForXxx(request);\nSaManager.getSaTokenContext().setContext(req, res, stg);\n```\n\n这样我们即可在具体的 Controller 请求中，成功调用 `StpUtil.isLogin()` 的 API。\n\n总结：整体的步骤并不复杂，就是先定义 `SaRequest`、`SaResponse`、`SaStorage` 的包装类，然后在全局过滤器保存在上下文管理器中。\n可以参考具体实现 `sa-token-spring-boot-starter`（SpringBoot2 项目 starter 包）：\n[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)\n\n\n\n### 旧版本方案\n在旧版本中（< v1.42.0）我们推荐的方案是自定义整个 `SaTokenCentext` 接口，目前此方案在新版本已不推荐，此处仅做留档备份：[自定义 SaTokenContext 指南](/fun/sa-token-context--backup.md)\n\n"
  },
  {
    "path": "sa-token-doc/fun/sa-token-test.md",
    "content": "# Sa-Token框架掌握度--在线考试\n\n--- \n\n此份考卷将测评您对Sa-Token框架的掌握程度（满分100），链接：[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx)\n\n"
  },
  {
    "path": "sa-token-doc/fun/session-model.md",
    "content": "# Sa-Token 中的 Session会话 模型详解\n\n--- \n\n### 1、Account-Session \n\n提起Session，你脑海中最先浮现的可能就是 JSP 中的 HttpSession，它的工作原理可以大致总结为：\n\n客户端每次与服务器第一次握手时，会被强制分配一个 `[唯一id]` 作为身份标识，注入到 Cookie 之中，\n之后每次发起请求时，客户端都要将它提交到后台，服务器根据 `[唯一id]` 找到每个请求专属的Session对象，维持会话\n\n这种机制简单粗暴，却有N多明显的缺点：\n\n1. 同一账号分别在PC、APP登录，会被识别为两个不相干的会话 \n2. 一个设备难以同时登录两个账号\n3. 每次一个新的客户端访问服务器时，都会产生一个新的Session对象，即使这个客户端只访问了一次页面 \n4. 在不支持Cookie的客户端下，这种机制会失效 \n\n\nSa-Token Session可以理解为 HttpSession 的升级版：\n\n1. Sa-Token只在调用`StpUtil.login(id)`登录会话时才会产生Session，不会为每个陌生会话都产生Session，节省性能 \n2. 在登录时产生的Session，是分配给账号id的，而不是分配给指定客户端的，也就是说在PC、APP上登录的同一账号所得到的Session也是同一个，所以两端可以非常轻松的同步数据  \n3. Sa-Token支持Cookie、Header、body三个途径提交Token，而不是仅限于Cookie \n4. 由于不强依赖Cookie，所以只要将Token存储到不同的地方，便可以做到一个客户端同时登录多个账号 \n\n这种为账号id分配的Session，我们给它起一个合适的名字：`Account-Session`，你可以通过如下方式操作它：\n``` java\n// 获取当前会话的 Account-Session \nSaSession session = StpUtil.getSession();\n\n// 从 Account-Session 中读取、写入数据 \nsession.get(\"name\");\nsession.set(\"name\", \"张三\");\n```\n\n使用`Account-Session`在不同端同步数据是非常方便的，因为只要 PC 和 APP 登录的账号id一致，它们对应的都是同一个Session，\n举个应用场景：在PC端点赞的帖子列表，在APP端的点赞记录里也要同步显示出来\n\n\n### 2、Token-Session  \n\n随着业务推进，我们还可能会遇到一些需要数据隔离的场景：\n\n> [!NOTE| label:业务场景] \n> 指定客户端超过两小时无操作就自动下线，如果两小时内有操作，就再续期两小时，直到新的两小时无操作 \n\n那么这种请求访问记录应该存储在哪里呢？放在 Account-Session 里吗？\n\n可别忘了，PC端和APP端可是共享的同一个 Account-Session ，如果把数据放在这里，\n那就意味着，即使用户在PC端一直无操作，只要手机上用户还在不间断的操作，那PC端也不会过期！\n\n解决这个问题的关键在于，虽然两个设备登录的是同一账号，但是两个它们得到的token是不一样的，\nSa-Token针对会话登录，不仅为账号id分配了`Account-Session`，同时还为每个token分配了不同的`Token-Session`\n\n不同的设备端，哪怕登录了同一账号，只要它们得到的token不一致，它们对应的 `Token-Session` 就不一致，这就为我们不同端的独立数据读写提供了支持：\n\n``` java\n// 获取当前会话的 Token-Session \nSaSession session = StpUtil.getTokenSession();\n\n// 从 Token-Session 中读取、写入数据 \nsession.get(\"name\");\nsession.set(\"name\", \"张三\");\n```\n\n### 3、Custom-Session\n\n除了以上两种Session，Sa-Token还提供了第三种Session，那就是：`Custom-Session`，你可以将其理解为：自定义Session\n\nCustom-Session不依赖特定的 账号id 或者 token，而是依赖于你提供的SessionId：\n\n``` java\n// 获取指定key的 Custom-Session \nSaSession session = SaSessionCustomUtil.getSessionById(\"goods-10001\");\n\n// 从 Custom-Session 中读取、写入数据 \nsession.get(\"name\");\nsession.set(\"name\", \"张三\");\n```\n\n只要两个自定义Session的Id一致，它们就是同一个Session \n\nCustom-Session的会话有效期默认使用`SaManager.getConfig().getTimeout()`, 如果需要修改会话有效期, 可以在创建之后, 使用对象方法修改\n\n``` java\nsession.updateTimeout(1000); // 参数说明和全局有效期保持一致\n```\n\n\n### 4、Session模型结构图 \n\n三种Session创建时机：\n\n- `Account-Session`: 指的是框架为每个 账号id 分配的 Session \n- `Token-Session`: 指的是框架为每个 token 分配的 Session  \n- `Custom-Session`: 指的是以一个 特定的值 作为SessionId，来分配的 Session \n\n\n**假设三个客户端登录同一账号，且配置了不共享token，那么此时的Session模型是：**\n\n<img class=\"s-w\" src=\"/big-file/doc/fun/session-model3.png\" alt=\"session-model\" />\n\n简而言之：\n- `Account-Session`  以账号 id 为主，只要 token 指向的账号 id 一致，那么对应的Session对象就一致\n- `Token-Session` 以token为主，只要token不同，那么对应的Session对象就不同\n- `Custom-Session` 以特定的key为主，不同key对应不同的Session对象，同样的key指向同一个Session对象 \n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/sso-vs-oauth2.md",
    "content": "# 技术选型：[ 单点登录 ] VS [ OAuth2.0 ]\n\n--- \n\nQQ群里经常有小伙伴提问：项目需要搭建统一认证中心，是用 SSO 方便还是 OAuth2.0 方便呢？针对这个问题，我们列出两者的主要区别以供大家参考：\n\n\n| 功能点\t\t\t\t| SSO单点登录\t\t\t| OAuth2.0\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t| :--------\t\t\t\t| :--------\t\t\t\t\t\t\t\t\t\t\t|\n| 统一认证\t\t\t| 支持度高\t\t\t\t| 支持度高\t\t\t\t\t\t\t\t\t\t\t|\n| 统一注销\t\t\t| 支持度高\t\t\t\t| 支持度低\t\t\t\t\t\t\t\t\t\t\t|\n| 多个系统会话一致性\t| 强一致\t\t\t\t\t| 弱一致\t\t\t\t\t\t\t\t\t\t|\n| 第三方应用授权管理\t| 不支持\t\t\t\t\t| 支持度高\t\t\t\t\t\t\t\t\t|\n| 自有系统授权管理\t| 支持度高\t\t\t\t| 支持度低\t\t\t\t\t\t\t\t\t\t|\n| Client级的权限校验\t| 不支持\t\t\t\t\t| 支持度高\t\t\t\t\t\t\t\t\t\t|\n| 集成简易度\t\t\t| 比较简单\t\t\t\t| 难度中等\t\t\t\t\t\t\t\t\t|\n| 适合项目\t\t\t| 企业内部项目整合\t\t| 企业搭建统一认证授权平台，对外开放服务\t\t\t|\n\n\n注：以上仅为在 Sa-Token 中两种技术的差异度比较，不同框架的实现可能略有差异，但整体思想是一致的。\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/team.md",
    "content": "# 团队成员 \n\n\n### 开发 \n\n负责：代码开发、社区维护、issue 处理、pr 审核等。\n\n<table class=\"team-table\">\n    <tr>\n        <th>头像</th>\n        <th>昵称</th>\n        <th>个人主页</th>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-xiaofengzheng.jpg\" /></td>\n        <td>小风筝（作者）</td>\n        <td><a href=\"https://gitee.com/click33\" target=\"_blank\">https://gitee.com/click33</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-AppleOfGray.png\" /></td>\n        <td>AppleOfGray</td>\n\t\t<td><a href=\"https://gitee.com/appleOfGray\" target=\"_blank\">https://gitee.com/appleOfGray</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-ly-chn.png\" /></td>\n        <td>ly-chn</td>\n\t\t<td><a href=\"https://gitee.com/ly-chn\" target=\"_blank\">https://gitee.com/ly-chn</a></td>\n    </tr>\n</table>\n\n\n\n### 提案讨论组成员 \n\n负责：提案新增、讨论、投票。\n\n<table class=\"team-table\">\n    <tr>\n        <th>头像</th>\n        <th>昵称</th>\n        <th>个人主页</th>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-xiaofengzheng.jpg\" /></td>\n        <td>刘潇</td>\n        <td><a href=\"https://gitee.com/click33\" target=\"_blank\">https://gitee.com/click33</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-AppleOfGray.png\" /></td>\n        <td>AppleOfGray</td>\n\t\t<td><a href=\"https://gitee.com/appleOfGray\" target=\"_blank\">https://gitee.com/appleOfGray</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-ly-chn.png\" /></td>\n        <td>ly-chn</td>\n\t\t<td><a href=\"https://gitee.com/ly-chn\" target=\"_blank\">https://gitee.com/ly-chn</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-moli.jpg\" /></td>\n        <td>茉莉</td>\n\t\t<td><a href=\"https://gitee.com/kidoldman\" target=\"_blank\">https://gitee.com/kidoldman</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-yaoshui.jpg\" /></td>\n        <td>药水</td>\n\t\t<td><a href=\"https://gitee.com/java_pioneer\" target=\"_blank\">https://gitee.com/java_pioneer</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-daimouren.png\" /></td>\n        <td>呆某人</td>\n\t\t<td><a href=\"https://gitee.com/zhubj0510\" target=\"_blank\">https://gitee.com/zhubj0510</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-chunkunqiufa.jpg\" /></td>\n        <td>春困夏倦秋乏</td>\n\t\t<td><a href=\"https://gitee.com/uncarbon97\" target=\"_blank\">https://gitee.com/uncarbon97</a></td>\n    </tr>\n    <tr>\n        <td><img src=\"/big-file/doc/team/avatar-danmo.jpg\" /></td>\n        <td>淡墨</td>\n\t\t<td><a href=\"https://gitee.com/jinan-jimeng-network_0\" target=\"_blank\">https://gitee.com/jinan-jimeng-network_0</a></td>\n    </tr>\n</table>\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/tech-stack.md",
    "content": "# Sa-Token 源码用到的所有技术栈\n\n包括但不限于以下：\n\n- Maven多模块项目\n- Servlet API、临时Cookie与永久Cookie、Request参数获取\n- SpringBoot2.0、Redis、Jackson、Hutool、jwt\n- SpringBoot自定义starter、Spring包扫描 + 依赖注入、AOP注解切面、yml配置映射、拦截器\n- Java8 接口与default实现、静态方法、枚举、定时器、异常类、泛型、反射、IO流、自定义注解、Lambda表达式、函数式编程\n- package-info注释、Serializable序列化接口、synchronized锁\n- java加密算法：MD5、SHA1、SHA256、AES、RSA\n- OAuth2.0、同域单点登录、集群与分布式、路由Ant匹配\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/three-scope.md",
    "content": "# 三大作用域 \n\n--- \n\nSa-Token 数据存储有三大作用域，分别是：\n- `SaStorage` - 请求作用域：存储的数据只在一次请求内有效。\n- `SaSession` - 会话作用域：存储的数据在一次会话范围内有效。\n- `SaApplication` - 全局作用域：存储的数据在全局范围内有效。\n\n\n### SaStorage - 请求作用域\n在 SaStorage 中存储的数据只在一次请求范围内有效，请求结束后数据自动清除。使用 SaStorage 时无需处于登录状态。\n\n``` java\nSaStorage storage = SaHolder.getStorage();\nstorage.get(\"key\");   // 取值\nstorage.set(\"key\", \"value\");   // 写值 \nstorage.delete(\"key\");   // 删值 \n```\n\n\n### SaSession - 会话作用域\n在 SaSession 存储的数据在一次会话范围内有效，会话结束后数据自动清除。必须登录后才能使用 SaSession 对象。\n\n``` java\nSaSession session = StpUtil.getSession();\nsession.get(\"key\");   // 取值\nsession.set(\"key\", \"value\");   // 写值 \nsession.delete(\"key\");   // 删值 \n```\n\n\n### SaApplication - 全局作用域\n在 SaApplication 存储的数据在全局范围内有效，应用关闭后数据自动清除（如果集成了 Redis 那则是 Redis 关闭后数据自动清除）。使用 SaApplication 时无需处于登录状态。\n\n``` java\nSaApplication application = SaHolder.getApplication();\napplication.get(\"key\");   // 取值\napplication.set(\"key\", \"value\");   // 写值 \napplication.delete(\"key\");   // 删值 \n```\n\n"
  },
  {
    "path": "sa-token-doc/fun/timeline.md",
    "content": "# Sa-Token 开源大事记\n\n--- \n\n- **2020-02-04：** 在 GitHub 提交第一个版本，正式开源。\n- **2020-09-14：** GitHub star 数量破 100。\n- **2020-10-26：** Gitee star 数量破 100。\n- **2021-03-01：** 被 [HelloGitHub] 第 59 期收录推荐。\n- **2021-03-26：** GitHub star 数量破 1k。\n- **2021-03-30：** 受 TLog 作者邀请，Sa-Token 加入 dromara 社区。\n- **2021-03-30：** 被 Gitee 列为推荐项目。\n- **2021-03-31：** Gitee star 数量破1K。\n<!-- - **2021-04-09：** GitHub star 数量破2K。 -->\n<!-- - **2021-05-09：** Gitee star 数量破2K。 -->\n<!-- - **2021-05-17：** GitHub star 数量破3K。 -->\n- **2021-06-17：** Sa-Token star 数量 (3529) 超过 Shiro (3506)。\n<!-- - **2021-07-15：** GitHub star 数量破4K。 -->\n- **2021-07-26：** GitHub star 数量破5K。\n<!-- - **2021-07-29：** Gitee star 数量破3K。 -->\n<!-- - **2021-09-07：** GitHub star 数量破6K。 -->\n- **2021-09-24：** Sa-Token star 数量 (6280) 超过 SpringSecurity (6247)。\n<!-- - **2021-10-19：** Gitee star 数量破4K。 -->\n- **2021-11-08：** 荣获开源中国 “码云 GVP 认证”。\n<!-- - **2021-11-17：** GitHub star 数量破7K。 -->\n- **2021-11-28：** Gitee star 数量破5K。\n- **2021-12-27：** 荣获 OSC-2021 最佳软件 Top 30。\n- **2022-05-20：** 成为 [可信开源社区共同体] 预备成员。\n- **2022-08-01：** 加入 [中国开源社区 landscape]。\n- **2022-08-18：** GitHub 第 10000 个 star 里程碑！\n- **2023-01-09：** 荣获 OSC 2022 年度最热开源项目社区。\n- **2023-11-21：** 被评为“开放原子基金会2023快速成长开源项目”。\n- **2024-04-25：** 42.9k star 登顶 Gitee 开源项目推荐榜 Top 1。\n- **2024-08-19：** 成为 GitCode G-Star 开源摘星计划毕业项目。\n- **2024-11-22：** 所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛（开源）》二等奖。\n\n\n\n"
  },
  {
    "path": "sa-token-doc/fun/token-info.md",
    "content": "# SaTokenInfo 参数详解\n\ntoken信息Model: 用来描述一个token的常用参数\n\n``` js\n{\n\t\"code\": 200,\n\t\"msg\": \"ok\",\n\t\"data\": {\n\t\t\"tokenName\": \"satoken\",           // token名称\n\t\t\"tokenValue\": \"e67b99f1-3d7a-4a8d-bb2f-e888a0805633\",      // token值\n\t\t\"isLogin\": true,                  // 此token是否已经登录\n\t\t\"loginId\": \"10001\",               // 此token对应的LoginId，未登录时为null\n\t\t\"loginType\": \"login\",              // 账号类型标识\n\t\t\"tokenTimeout\": 2591977,          // token剩余有效期 (单位: 秒)\n\t\t\"sessionTimeout\": 2591977,        // Account-Session剩余有效时间 (单位: 秒)\n\t\t\"tokenSessionTimeout\": -2,        // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)\n\t\t\"tokenActiveTimeout\": -1,         // token 距离被冻结还剩的时间 (单位: 秒)\n\t\t\"loginDevice\": \"DEF\"   // 登录设备类型 \n\t},\n}\n```"
  },
  {
    "path": "sa-token-doc/fun/token-timeout.md",
    "content": "# Token有效期详解\n\n<!-- 本篇介绍Token有效期的详细用法 -->\n\nSa-Token 提供两种 Token 自动过期策略，分别是 `timeout` 与 `active-timeout`，配置方法如下：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\t# token 有效期（单位：秒），默认30天，-1代表永不过期 \n\ttimeout: 2592000\n\t# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\tactive-timeout: -1\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# token 有效期（单位：秒），默认30天，-1代表永不过期 \nsa-token.timeout=2592000\n# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\nsa-token.active-timeout=-1\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n两者的区别，可以通过下面的例子体现：\n\n> [!TIP| label:场景示例] \n> 1. 假设你到银行要存钱，首先就要办理一张卡 （要访问系统接口先登录）。\n> 2. 银行为你颁发一张储蓄卡（系统为你颁发一个Token），以后每次存取钱都要带上这张卡（后续每次访问系统都要提交 Token）。\n> 3. 银行为这张卡设定两个过期时间：\n> \t- 第一个是 `timeout`，代表这张卡的长久有效期，就是指这张卡最长能用多久，假设 `timeout=3年`，那么3年后此卡将被银行删除，想要继续来银行办理业务必须重新办卡（Token 过期后想要访问系统必须重新登录）。\n> \t- 第二个就是 `active-timeout`，代表这张卡的最低活跃频率限制，就是指这张卡必须每隔多久来银行一次，假设 `active-timeout=1月` ，你如果超过1月不来办一次业务，银行就将你的卡冻结，列为长期不动户（Token 长期不访问系统，被冻结，但不会被删除）。\n> 4. 两个过期策略可以单独配置，也可以同时配置，只要有其中一个有效期超出了范围，这张卡就会变得不可用（两个有效期只要有一个过期了，Token就无法成功访问系统了）。\n\n下面是对两个过期策略的详细解释：\n\n### timeout\n1. `timeout`代表 Token 的长久有效期，单位/秒，例如将其配置为 2592000 (30天)，代表在30天后，Token必定过期，无法继续使用。\n2. `timeout`~~无法续签，想要继续使用必须重新登录~~。v1.29.0+ 版本新增续期方法：`StpUtil.renewTimeout(100)`。\n3. `timeout`的值配置为-1后，代表永久有效，不会过期。\n\n\n### active-timeout\n1. `active-timeout`代表最低活跃频率，单位/秒，例如将其配置为 1800 (30分钟)，代表用户如果30分钟无操作，则此Token会立即过期（被冻结，但不会删除掉）。\n2. 如果在30分钟内用户有操作，则会再次续签30分钟，用户如果一直操作则会一直续签，直到连续30分钟无操作，Token才会过期。\n3. `active-timeout`的值配置为-1后，代表永久有效，不会过期，此时也无需频繁续签。\n\n\n### 关于active-timeout的续签\n如果`active-timeout`配置了大于零的值，Sa-Token 会在登录时开始计时，在每次直接或间接调用`getLoginId()`、`getTokenSession()`时进行一次冻结检查与续签操作。\n此时会有两种情况：\n1. 一种是会话无操作时间太长，Token已经被冻结，此时框架会抛出`NotLoginException`异常(场景值=-3)，\n2. 另一种则是会话在`active-timeout`有效期内通过检查，此时Token可以成功续签 \n\n\n### 我可以手动续签 active-timeout 吗？\n**可以！**\n如果框架的自动续签算法无法满足您的业务需求，你可以进行手动续签，Sa-Token 提供两个API供你操作：\n1. `StpUtil.checkActiveTimeout()`: 检查当前Token 是否已经被冻结，如果是则抛出异常\n2. `StpUtil.updateLastActiveToNow()`: 续签当前Token：(将 [最后操作时间] 更新为当前时间戳) \n\n注意：在手动续签时，即使 Token 已经被冻结也可续签成功（解冻），如果此场景下需要提示续签失败，可采用先检查再续签的形式保证Token有效性 \n\n例如以下代码：\n``` java\n// 先检查是否已被冻结\nStpUtil.checkActiveTimeout();\n// 检查通过后继续续签\nStpUtil.updateLastActiveToNow();\n```\n\n同时，你还可以关闭框架的自动续签（在配置文件中配置 `autoRenew=false` ），此时续签操作完全由开发者控制，框架不再自动进行任何续签操作\n\n如果你需要给其它 Token 续签：\n\n``` java\n// 为指定 Token 续签 \nStpUtil.stpLogic.updateLastActiveToNow(tokenValue);\n```\n\n\n### timeout 与 active-timeout 可以同时使用吗？\n**可以同时使用！** \n两者的认证逻辑彼此独立，互不干扰，可以同时使用。\n\n\n### StpUtil 类中哪些方法支持自动续签 active-timeout? \n> 直接或间接调用过 `getLoginId()`、`getTokenSession()` 的方法\n\n| 包括但不限于这些： |\n|---|\n| StpUtil.checkLogin() |\n| StpUtil.getLoginId() |\n| StpUtil.getLoginIdAsInt() |\n| StpUtil.getLoginIdAsString() |\n| StpUtil.getLoginIdAsLong() |\n|---|\n| StpUtil.getSession() |\n| StpUtil.getTokenSession() |\n|---|\n| StpUtil.getRoleList() |\n| StpUtil.hasRole() |\n| StpUtil.hasRoleAnd() |\n| StpUtil.hasRoleOr() |\n| StpUtil.checkRole() |\n| StpUtil.checkRoleAnd() |\n| StpUtil.checkRoleOr() |\n|---|\n| StpUtil.getPermissionList() |\n| StpUtil.hasPermission() |\n| StpUtil.hasPermissionAnd() |\n| StpUtil.hasPermissionOr() |\n| StpUtil.checkPermission() |\n| StpUtil.checkPermissionAnd() |\n| StpUtil.checkPermissionOr() |\n|---|\n| StpUtil.openSafe() |\n| StpUtil.isSafe() |\n| StpUtil.checkSafe() |\n| StpUtil.getSafeTime() |\n| StpUtil.closeSafe() |\n\n> 以下注解都间接调用过 getLoginId() 方法\n\n| 支持自动续签的注解 |\n|---|\n| @SaCheckLogin      |\n| @SaCheckRole       |\n| @SaCheckPermission |\n| @SaCheckSafe       |\n"
  },
  {
    "path": "sa-token-doc/include/include-qa.md",
    "content": "<!-- embed:start:hostsInvalid -->\n\n> [!WARNING| label:更改了 hosts 但无法访问？] \n> - 可能 1：你没保存。\n> - 可能 2：你后端项目没启动。\n> - 可能 3：你访问时端口写错了。\n> - 可能 4：你开了 VPN，关掉试试。\n\n<!-- embed:end:hostsInvalid -->"
  },
  {
    "path": "sa-token-doc/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<meta charset=\"UTF-8\">\n\t\t<title>Sa-Token</title>\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n\t\t<meta name=\"description\"\n\t\t\tcontent=\"Sa-Token是一个java权限认证框架，功能全面，上手简单，登录认证、权限认证、Session会话、踢人下线、账号封禁、集成Redis、前后端分离、分布式会话、微服务网关鉴权、单点登录、OAuth2.0、临时Token验证、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成、WebFlux集成...，有了sa-token，你所有的权限认证问题，都不再是问题\">\n\t\t<meta name=\"keywords\" content=\"sa-token,sa-token框架,sa-token文档,java权限认证\">\n\t\t<meta name=\"viewport\"\n\t\t\tcontent=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n\t\t<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"logo.png\">\n\t\t<link rel=\"stylesheet\" href=\"static/index.css\">\n\t\t<link rel=\"stylesheet\" href=\"static/swiper/swiper-bundle.min.css\">\n\t\t<link rel=\"stylesheet\" href=\"static/swiper/index-swiper.css\">\n\t</head>\n\t<body>\n\t\t<!-- 总盒子 -->\n\t\t<div class=\"z-div\" style=\"\">\n\n\t\t\t<!-- ------------ 头部 ------------- -->\n\t\t\t<header class=\"doc-header\">\n\t\t\t\t<div class=\"nav-left\">\n\t\t\t\t\t<a href=\"./\">\n\t\t\t\t\t\t<div class=\"logo-box\">\n\t\t\t\t\t\t\t<img src=\"./logo.png\" title=\"logo\" />\n\t\t\t\t\t\t\t<span class=\"logo-text\">Sa-Token</span>\n\t\t\t\t\t\t\t<!-- <h1 class=\"logo-text\">Sa-Token</h1> -->\n\t\t\t\t\t\t\t<!-- <sub>vx.x.x</sub> -->\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t\t<nav class=\"nav-right\">\n\t\t\t\t\t<!-- <div class=\"zk-box p-none\">\n\t\t\t\t\t\t<a class=\"wzi\" href=\"javascript:;\">\n\t\t\t\t\t\t\t<img class=\"theme-btn\" src=\"static/icon/theme.svg\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<div class=\"zk-context theme-box\">\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<div style=\"height: 5px;\"></div>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #FFFFFF;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #f5f5f5;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #F1FAFA;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #f5f5d5;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #d5f5f5;\"></span>\n\n\t\t\t\t\t\t\t\t<span style=\"background-color: #f5e5f5;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #E8E8FF;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #f0f9eb;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #ebe5dd;\"></span>\n\t\t\t\t\t\t\t\t<span style=\"background-color: #e8f4ff;\"></span>\n\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div> -->\n\t\t\t\t\t<a class=\"wzi\" href=\"index.html\">首页</a>\n\t\t\t\t\t<a class=\"wzi\" href=\"doc.html\">文档</a>\n\t\t\t\t\t<div class=\"zk-box\">\n\t\t\t\t\t\t<a class=\"wzi\" href=\"javascript:;\">\n\t\t\t\t\t\t\t<span>视频 </span>\n\t\t\t\t\t\t\t<span class=\"zk-icon\"></span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<div class=\"zk-context\">\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1fsUVBWEyH/\" target=\"_blank\">朱老师的小课堂（7集）</a>\n\t\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1NF1FBpEe6/\" target=\"_blank\">王清江唷 SSO篇（29集）</a>\n\t\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1uZUpYVEst/\" target=\"_blank\">fox说技术（7集）</a>\n\t\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1eFtRezERp?p=87\" target=\"_blank\">架构驿站（11集）</a>\n\t\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1Zt421u7gk/\" target=\"_blank\">王清江唷（99集）</a>\n\t\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV1kG411o7Ms/\" target=\"_blank\">筑梦信仰-joy（20集）</a>\n\t\t\t\t\t\t\t\t<a href=\"https://www.bilibili.com/video/BV11u4y197JL/\" target=\"_blank\">达达-Java（26集）</a>\n\t\t\t\t\t\t\t\t<a href=\"https://space.bilibili.com/473679148/video\" target=\"_blank\">晒太阳的盐（22集）</a>\n\t\t\t\t\t\t\t\t<div class=\"zk-fengexian\"></div>\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/more/content-cooperation\">[ + 课程提交 ]</a>\n\t\t\t\t\t\t\t\t<!-- <a href=\"javascript: layer.alert('如您有 Sa-Token 相关课程录制，请联系官网文档右侧 < sa-token 小助手 > 进行提交');\">\n\t\t\t\t\t\t\t\t\t[ + 课程提交 ]\n\t\t\t\t\t\t\t\t</a> -->\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<a class=\"p-none wzi\" href=\"doc.html#/more/link\">案例</a>\n\t\t\t\t\t<a class=\"p-none wzi\" href=\"doc.html#/more/join-group\">加入讨论群</a>\n\t\t\t\t\t<a class=\"p-none wzi\" href=\"doc.html#/more/demand-commit\">需求提交</a>\n\t\t\t\t\t<a class=\"p-none wzi\" href=\"doc.html#/more/blog\">博客</a>\n\t\t\t\t\t<a class=\"p-none wzi\" href=\"doc.html#/more/sa-token-donate\">赞助</a>\n\t\t\t\t\t<a class=\"p-none wzi\" href=\"doc.html#/pro/st_index_top\">🔥 SSO/OAuth2 商业版</a>\n\t\t\t\t\t<div class=\"zk-box\">\n\t\t\t\t\t\t<a class=\"wzi\" href=\"javascript:;\">\n\t\t\t\t\t\t\t<span>相关资源 </span>\n\t\t\t\t\t\t\t<span class=\"zk-icon\"></span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<div class=\"zk-context\">\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<!-- <a href=\"#/more/sa-token-donate\">❤️ &nbsp;赞助2</a> -->\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/more/update-log\">更新日志</a>\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/more/common-questions\">常见报错</a>\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/more/tj-gzh\">推荐公众号</a>\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/more/blog\">相关博客</a>\n\t\t\t\t\t\t\t\t<div class=\"zk-fengexian\"></div>\n\t\t\t\t\t\t\t\t<!-- <a href=\"http://sa-app.dev33.cn/wall.html?name=sa-token\" target=\"_blank\">需求墙</a> -->\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/fun/sa-token-test\">在线考试</a>\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/fun/issue-template\">在线提问</a>\n\t\t\t\t\t\t\t\t<!-- <a href=\"https://wj.qq.com/s2/10852322/0d8b/\" target=\"_blank\">需求提交</a> -->\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/more/wenjuan\">问卷调查</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- github小章鱼图标 -->\n\t\t\t\t\t<a href=\"https://github.com/dromara/sa-token\" target=\"_blank\" class=\"github-corner\"\n\t\t\t\t\t\taria-label=\"View source on Github\" style=\"position: fixed; right: -16px; padding-left: 0px;\">\n\t\t\t\t\t\t<svg viewBox=\"0 0 250 250\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t<path d=\"M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z\"></path>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\td=\"M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2\"\n\t\t\t\t\t\t\t\tfill=\"currentColor\" style=\"transform-origin: 130px 106px;\" class=\"octo-arm\"></path>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\td=\"M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z\"\n\t\t\t\t\t\t\t\tfill=\"currentColor\" class=\"octo-body\"></path>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</a>\n\t\t\t\t</nav>\n\t\t\t</header>\n\n\t\t\t<!-- ------------ 海报部分 ------------- -->\n\t\t\t<div class=\"main-box\">\n\t\t\t\t<div class=\"content-box\">\n\t\t\t\t\t<!-- <div class=\"fenge\"></div> -->\n\t\t\t\t\t<h1>Sa-Token<small>v1.45.0</small></h1>\n\t\t\t\t\t<div class=\"sub-title\">\n\t\t\t\t\t\t<span class=\"sub-title-nr\">开源、免费、一站式 java 权限认证框架，让鉴权变得简单、优雅！</span>\n\t\t\t\t\t\t<div class=\"gb-cursor\">&nbsp;</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"btn-box\">\n\t\t\t\t\t\t<!-- <a class=\"abtn\" href=\"https://gitee.com/dromara/sa-token\" target=\"_blank\">Gitee</a>\n\t\t\t\t\t\t<a class=\"abtn\" href=\"https://github.com/dromara/sa-token\" target=\"_blank\">GitHub</a> -->\n\t\t\t\t\t\t<a class=\"abtn\" href=\"doc.html#/more/demand-commit\" target=\"_self\">需求提交</a>\n\t\t\t\t\t\t<a class=\"abtn\" href=\"https://gitee.com/sa-tokens/awesome-sa-token\" target=\"_blank\">开源案例</a>\n\t\t\t\t\t\t<a class=\"abtn\" href=\"doc.html#/more/join-group\" target=\"_self\">加入讨论群</a>\n\t\t\t\t\t\t<a class=\"abtn doc-btn\" href=\"doc.html\" target=\"_self\">在线文档 →</a>\n\t\t\t\t\t\t<!-- <a href=\"https://gitee.com/dromara/sa-token\" target=\"_blank\">集成案例</a> -->\n\t\t\t\t\t</div>\n\t\t\t\t\t<h4 align=\"center\" class=\"badge-box\">\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/sa-token/stargazers\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp\"></a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/sa-token/members\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp\"></a>\n\t\t\t\t\t\t<a href=\"https://atomgit.com/dromara/sa-token/stargazers\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://atomgit.com/dromara/Sa-Token/star/badge.svg\"></a>\n\t\t\t\t\t\t<a href=\"https://github.com/dromara/sa-token/stargazers\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://img.shields.io/github/stars/dromara/sa-token?style=flat-square&logo=GitHub\"></a>\n\t\t\t\t\t\t<a href=\"https://github.com/dromara/sa-token/network/members\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://img.shields.io/github/forks/dromara/sa-token?style=flat-square&logo=GitHub\"></a>\n\t\t\t\t\t\t<a href=\"https://github.com/dromara/sa-token/watchers\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://img.shields.io/github/watchers/dromara/sa-token?style=flat-square&logo=GitHub\"></a>\n\t\t\t\t\t\t<!-- <a href=\"https://github.com/dromara/sa-token/issues\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub\"></a> -->\n\t\t\t\t\t\t<a href=\"https://github.com/dromara/sa-token/blob/master/LICENSE\"><img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square\"></a>\n\t\t\t\t\t</h4>\n\t\t\t\t\t<div class=\"qt-pt-box\">\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/sa-token\" target=\"_blank\">\n\t\t\t\t\t\t\t<img src=\"/big-file/index/platform/gitee.png\" alt=\"\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://atomgit.com/dromara/sa-token\" target=\"_blank\">\n\t\t\t\t\t\t\t<img src=\"/big-file/index/platform/atomgit.svg\" alt=\"\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://github.com/dromara/sa-token\" target=\"_blank\">\n\t\t\t\t\t\t\t<img src=\"/big-file/index/platform/github.png\" alt=\"\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<span class=\"dmt-link\">\n\t\t\t\t\t\t\t<img class=\"dmt-img\" src=\"/big-file/index/platform/zong-4.png\" alt=\"\">\n\t\t\t\t\t\t\t<span class=\"dmt-tips\">B站、抖音、视频号 ...</span>\n\t\t\t\t\t\t\t<div class=\"dmt-detail\">\n\t\t\t\t\t\t\t\t<h4>关注我们 → 分享“权限认证架构设计”干货视频</h4>\n\t\t\t\t\t\t\t\t<div class=\"dmt-item-box\">\n\t\t\t\t\t\t\t\t\t<div class=\"dmt-item dmt-item-bilibili\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"https://space.bilibili.com/3546758575557094\" target=\"_blank\">\n\t\t\t\t\t\t\t\t\t\t\t<img class=\"dmt-qr-img\" src=\"/big-file/index/platform/bilibili-qr-fang.png\" alt=\"\">\n\t\t\t\t\t\t\t\t\t\t\t<img class=\"dmt-logo-img\" src=\"/big-file/index/platform/bilibili-light.png\" alt=\"\">\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"dmt-item dmt-item-douyin\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"https://www.douyin.com/user/MS4wLjABAAAArVqj2lGRurfj-9eO0T12q6_vrbIK-Om9bi3eo4OwB2g\" target=\"_blank\">\n\t\t\t\t\t\t\t\t\t\t\t<img class=\"dmt-qr-img\" src=\"/big-file/index/platform/douyin-qr-fang.png\" alt=\"\">\n\t\t\t\t\t\t\t\t\t\t\t<img class=\"dmt-logo-img\" src=\"/big-file/index/platform/douyin-light.png\" alt=\"\">\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"dmt-item dmt-item-wxsph\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"javascript: layer.msg('微信视频号暂未提供PC网站，请在手机微信扫码订阅');\">\n\t\t\t\t\t\t\t\t\t\t\t<img class=\"dmt-qr-img\" src=\"/big-file/index/platform/wxsph-qr-fang.png\" alt=\"\">\n\t\t\t\t\t\t\t\t\t\t\t<img class=\"dmt-logo-img\" src=\"/big-file/index/platform/wxsph-light.png\" alt=\"\">\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t\n\t\t\t\n\t\t\t\n\n\t\t\t<!-- ------------ 支持特性 ------------- -->\n\t\t\t<div>\n\t\t\t\t<div class=\"feature-z s-width\">\n\t\t\t\t\t<h2 class=\"s-title s-title-tx\">Sa-Token 支持特性</h2>\n\t\t\t\t\t<div class=\"feature-box\">\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>⚡️ 登录认证</h2>\n\t\t\t\t\t\t\t<p>多端登录、单端登录、同端互斥登录、七天免登录…… 多种登录策略只需改个配置即可完成</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>🔑️️ 权限认证</h2>\n\t\t\t\t\t\t\t<p>权限认证、角色认证、会话二级认证、注解鉴权、路由鉴权……多种姿势灵活鉴权</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>⛏️ 踢人下线</h2>\n\t\t\t\t\t\t\t<p>强制注销、踢人下线、账号封禁、身份切换、自动续签 …… 提供完善的会话管理方案</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>🔎 Redis集成</h2>\n\t\t\t\t\t\t\t<p>提供 Redis 集成方案、项目重启数据不丢失、多系统数据互通，可自定义数据持久化策略</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>🚀️️ 前后端分离</h2>\n\t\t\t\t\t\t\t<p>内置多种 Token 读取策略，适配APP、小程序、SPA单页应用等前后端分离场景</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>️🍃 单点登录</h2>\n\t\t\t\t\t\t\t<p>同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离……提供各种架构下的SSO接入方案</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>🍂 OAuth2.0</h2>\n\t\t\t\t\t\t\t<p>轻松搭建 OAuth2.0 认证中心，支持四种授权模式，支持 openid 授权机制，支持二次扩展开发</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>💦️ 微服务支持</h2>\n\t\t\t\t\t\t\t<p>分布式 Session 会话、网关统一鉴权、RPC调用鉴权……提供开箱即用的微服务认证方案</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"feature\">\n\t\t\t\t\t\t\t<h2>🗳️ 开箱即用</h2>\n\t\t\t\t\t\t\t<p>提供SpringMVC、WebFlux、Solon、jwt 等常见框架集成包，真正的开箱即用……</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\n\t\t\t<!-- ------------ 支持特性 ------------- -->\n\t\t\t<div>\n\t\t\t\t<div class=\"feature-z s-width\">\n\t\t\t\t\t<div style=\"margin-top: -60px;\"></div>\n\t\t\t\t\t<div class=\"s-fenge\"></div>\n\t\t\t\t\t<div style=\"height: 20px;\"></div>\n\t\t\t\t\t<h2 class=\"s-title s-title-tx\">\n\t\t\t\t\t\t七年磨一剑 🗡️ \n\t\t\t\t\t\t<span style=\"background: linear-gradient(to right, #44f, #bd34fe); background-clip: text; color: transparent;\"> 一站式解决方案</span>\n\t\t\t\t\t</h2>\n\t\t\t\t\t<div style=\"margin-top: -20px; margin-bottom: 40px;\">\n\t\t\t\t\t\t<!-- <img class=\"sa-token-jss-img\" src=\"/big-file/index/intro/sa-token-jss--tran.png\"> -->\n\t\t\t\t\t\t<!-- 使用 object 引入跨域 svg 图片时，图片初始会非常小，鼠标移上去一下才会恢复正常，未找到解决方案 -->\n\t\t\t\t\t\t<object class=\"sa-token-jss-img\" data=\"/big-file/index/intro/sa-token-jss--tran--onclick.svg\"></object>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"re-text\">\n\t\t\t\t\t\t<!-- <span>有了Sa-Token，你所有的权限认证问题，都不再是问题！</span> -->\n\t\t\t\t\t\t<span>Sa-Token 可以帮你轻松解决大多数权限认证问题！</span>\n\t\t\t\t\t\t<a href=\"/big-file/index/intro/sa-token-js4.png\" target=\"_blank\">点击查看功能结构图</a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\n\t\t\t<!-- ------------ 曾获荣誉 ------------- -->\n\t\t\t<div>\n\t\t\t\t<div class=\"feature-z ry-kuai\">\n\t\t\t\t\t<div class=\"s-fenge\"></div>\n\t\t\t\t\t<h2 class=\"s-title\">曾获荣誉</h2>\n\t\t\t\t\t<div class=\"ry-box\">\n\t\t\t\t\t\t<div class=\"swiper mySwiper\">\n\t\t\t\t\t\t\t<div class=\"swiper-wrapper\">\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/gvp.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>GVP - Gitee 最有价值开源项目</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/g-star.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>GitCode G-Star 优质开源项目</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/osc-2021.jpg\"/> <br>\n\t\t\t\t\t\t\t\t\t<p>OSCHINA 2021 人气指数 TOP 30 开源项目</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide swiper-slide-tx1\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/osc-2022.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>OSCHINA 2022 年度最火热中国开源项目社区</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide swiper-slide-tx1\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/kaifangyuanzi2.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>开放原子基金会2023快速成长开源项目</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/gitee-star-5000.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>Gitee 5000 star 专属奖杯</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/gitee-2025.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>Gitee 2025年度开源项目 Web应用开发 Top 2</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/dromara.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>Dromara 组织顶尖项目（之一）</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/kexin.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>可信开源社区共同体预备成员</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/dromara-2024-tzds.jpg\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛（开源）》二等奖</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\" style=\"width: 750px;\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/gitee-top-1.png\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>Gitee 项目推荐榜 top 1</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"swiper-slide\" style=\"width: 750px;\">\n\t\t\t\t\t\t\t\t\t<img src=\"/big-file/index/awards-zip/github-star-18k.png\" /> <br>\n\t\t\t\t\t\t\t\t\t<p>GitHub stars 超 18k+</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"swiper-button-next\"></div>\n\t\t\t\t\t\t\t<div class=\"swiper-button-prev\"></div>\n\t\t\t\t\t\t\t<div class=\"swiper-pagination\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- ------------ GitHub Stars 对比 ------------- -->\n\t\t\t<div style=\"margin-top: -50px;\">\n\t\t\t\t<div class=\"feature-z\">\n\t\t\t\t\t<h2 class=\"s-title\">Java 鉴权框架 Stars 对比</h2>\n\t\t\t\t\t<div style=\"border: 0px #000 solid;\">\n\t\t\t\t\t\t<iframe \n\t\t\t\t\t\t  src=\"./static/page-com/github-stars-vs/github-stars-vs.html\" \n\t\t\t\t\t\t  width=\"1000\" \n\t\t\t\t\t\t  height=\"700\" \n\t\t\t\t\t\t  frameborder=\"0\"\n\t\t\t\t\t\t  scrolling=\"no\"\n\t\t\t\t\t\t  style=\"border: none;\"\n\t\t\t\t\t\t></iframe>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\n\t\t\t<!-- ------------ 赞助者名单 ------------- -->\n\t\t\t<div style=\"margin-top: -50px;\">\n\t\t\t\t<div class=\"feature-z\">\n\t\t\t\t\t<h2 class=\"s-title\">赞助者名单（感谢！感谢！感谢！）</h2>\n\t\t\t\t\t<div class=\"\">\n\t\t\t\t\t\t<div class=\"zanzhu-box s-width\">\n\t\t\t\t\t\t\t<div class=\"zanzhu-sort-box\">\n\t\t\t\t\t\t\t\t<span class=\"zanzhu-sort-btn zz-sort-native\" sort-value=\"1\">日期排序</span>\n\t\t\t\t\t\t\t\t<span> | </span>\n\t\t\t\t\t\t\t\t<span class=\"zanzhu-sort-btn\" sort-value=\"2\">赞助额排序</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<table class=\"zanzhu-table\" cellspacing=\"0\" border=\"1\" bordercolor=\"e9e9e9\">\n\t\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<th>赞助人</th>\n\t\t\t\t\t\t\t\t\t\t<th>赞助金额</th>\n\t\t\t\t\t\t\t\t\t\t<th>留言</th>\n\t\t\t\t\t\t\t\t\t\t<th style=\"width: 100px;\">时间</th>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t<!-- <tr>\n\t\t\t\t\t\t\t\t\t\t<td>时间很快</td>\n\t\t\t\t\t\t\t\t\t\t<td>赞助金额</td>\n\t\t\t\t\t\t\t\t\t\t<td>感谢您的开源项目！</td>\n\t\t\t\t\t\t\t\t\t\t<td>2023-10-27</td>\n\t\t\t\t\t\t\t\t\t</tr> -->\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t<!-- 一些按钮 -->\n\t\t\t\t\t\t\t<div class=\"zz-btn-box\">\n\t\t\t\t\t\t\t\t<button onclick=\"prevPageRDT()\"> < </button>\n\t\t\t\t\t\t\t\t<span class=\"zz-pageInfo\">第 1/1 页</span>\n\t\t\t\t\t\t\t\t<button onclick=\"nextPageRDT()\"> > </button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div style=\"height: 30px;\"></div>\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t（如果您也有赞助 Sa-Token 的想法，可以参考：\n\t\t\t\t\t\t\t\t<a href=\"doc.html#/more/sa-token-donate\" style=\"color: #999;\">赞助名单</a>\n\t\t\t\t\t\t\t\t）\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\n\n\t\t\t<!-- ------------ 开源案例 ------------- -->\n\t\t\t<div>\n\t\t\t\t<div class=\"feature-z s-width\">\n\t\t\t\t\t<div class=\"s-fenge\"></div>\n\t\t\t\t\t<h2 class=\"s-title\" style=\"margin-top: 80px;\">优秀开源集成案例</h2>\n\t\t\t\t\t<div class=\"feature-box s-case-box\">\n\t\t\t\t\t\t<!-- Snowy  33.1K -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/xiaonuobase/snowy\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/case/case--snowy.jpg\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">Snowy</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> 小诺开源技术 </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">国内首个国密前后分离快速开发平台，基于Vue3、Antdv、SaToken</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- mall4j  17k -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/gz-yami/mall4j\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\"\n\t\t\t\t\t\t\t\t\tdata-original=\"/big-file/index/case/case--mall4j.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">mall4j</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> Mall4j商城系统 </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">基于Spring Boot 3 JDK17的一个商城手脚架。</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- RuoYi-Vue-Plus  15.6k -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/RuoYi-Vue-Plus\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\"\n\t\t\t\t\t\t\t\t\tdata-original=\"/big-file/index/case/case--ruoyi-vue-plus.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">RuoYi-Vue-Plus</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> 疯狂的狮子Li </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">重写 RuoYi-Vue 所有功能，集成 Sa-Token、Mybatis-Plus、Hutool 定期同步</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- Smart-Admin 10.3K -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/lab1024/smart-admin\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/case/case--smart-admin.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">Smart-Admin</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> 1024创新实验室 </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">坚持以「高质量代码」为核心，「简洁、高效、安全」的中后台解决方案！</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- SpringBoot_v2  6.1k -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/bdj/SpringBoot_v2\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\"\n\t\t\t\t\t\t\t\t\tdata-original=\"/big-file/index/case/case--springboot_v2.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">SpringBoot_v2</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\">开源oschina</span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">努力打造 springboot 框架的极致细腻的脚手架，原生纯净。</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- Lamp-Cloud 5.7K -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/lamp-cloud\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/case/case--lamp-cloud.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">灯灯</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> 最后 </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">专注于多租户解决方案的微服务中后台快速开发平台。</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- RuoYi-Cloud-Plus  6.8K -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/RuoYi-Cloud-Plus\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\"\n\t\t\t\t\t\t\t\t\tdata-original=\"/big-file/index/case/case--ruoyi-cloud-plus.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">RuoYi-Cloud-Plus</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> 疯狂的狮子Li </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">重写 RuoYi-Cloud 所有功能 整合 SpringCloudAlibaba、Dubbo3.0、Sa-Token</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- Orange-Admin 4.6K -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/orangeform/orange-admin\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/case/case--orange-admin.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">橙单</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> orange-form </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">橙单中台化低代码生成器。多应用、多租户、多渠道、工作流、在线表单等。</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- 拾壹博客  2.9K -->\n\t\t\t\t\t\t<div class=\"s-case\">\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/quequnlong/shiyi-blog\" target=\"_blank\" class=\"s-case-link\">\n\t\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/case/case--shiyi-blog.png\">\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<h3 class=\"s-case-title\">拾壹博客</h3>\n\t\t\t\t\t\t\t<span class=\"s-author\"> bule </span>\n\t\t\t\t\t\t\t<p class=\"s-case-intro\">一款 Vue + SpringBoot 前后端分离的博客系统</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<!-- https://github.com/DataLinkDC/dinky 3.7k -->\n\t\t\t\t\t\t<!-- https://gitee.com/mldong/mldong  10k -->\n\t\t\t\t\t\t<!-- https://gitee.com/junyue/flyflow 6.4k -->\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"re-text\">\n\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t如果您的开源项目也使用了 Sa-Token，您可以\n\t\t\t\t\t\t\t<a href=\"https://gitee.com/sa-token/awesome-sa-token\" target=\"_blank\"\n\t\t\t\t\t\t\t\tstyle=\"text-decoration: none;\">在此</a>\n\t\t\t\t\t\t\t提交\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t\n\t\t\t\t\t<div style=\" margin: 40px 14px 0; padding: 20px 0 10px; background-color: #f4f5f7;\">\n\t\t\t\t\t\t<h3 style=\"padding: 5px 0 20px; color: #333;\">Sa-Token 官方公众号，及时接收框架更新通知、技术文章</h3>\n\t\t\t\t\t\t<img class=\"lazy gzh-qr\" data-original=\"/big-file/contact/lykj-gzh.jpg\" style=\"width: 150px; cursor: pointer;\">\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\n\n\t\t\t<!-- ------------ 使用公司 ------------- -->\n\t\t\t<div>\n\t\t\t\t<div class=\"com-box-f s-width\">\n\t\t\t\t\t<div class=\"s-fenge\"></div>\n\t\t\t\t\t<br>\n\t\t\t\t\t<h2 class=\"s-title\">正在使用 Sa-Token 的企业 / 机构</h2>\n\t\t\t\t\t<div class=\"com-box\">\n\t\t\t\t\t\t<a href=\"http://yun94.cn/\" target=\"_blank\" title=\"济南凉云网络科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/liangyunwangluo.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://budwk.com/\" target=\"_blank\" title=\"BudWk 开发框架 V7.x\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/budwk.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.quandashi.com/\" target=\"_blank\" title=\"北京梦知网科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/quandashi.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.gree.com.cn/\" target=\"_blank\" title=\"珠海格力电器股份有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/geli.jpg\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"###\" title=\"货好多科技\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/huohaoduo.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.tuodan.tech/\" target=\"_blank\" title=\"深圳加速脱单科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/tuodan.jpg\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://uniadmin.jiangruyi.com/\" target=\"_blank\" title=\"南京星意信息科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/uniadmin.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.dchealth.com/\" target=\"_blank\" title=\"神州医疗\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/shenzhouyiliao.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"javascript:;\" title=\"暖通管家\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/nuantong.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.turingoal.com\" target=\"_blank\" title=\"图灵谷（北京）科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/tulinggu.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"javascript:;\" title=\"辽宁薪达网络科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/taipingyangcanyin.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.pactera.com/?renqun_youhua=2483561&bd_vid=9062916023494825120\"\n\t\t\t\t\t\t\ttarget=\"_blank\" title=\"中电文思海辉\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/zhongdianwensi-logo.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.shylsoft.com/\" target=\"_blank\" title=\"上海营联信息技术有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/yinglian.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.sxpartner.com/\" target=\"_blank\" title=\"陕西小伙伴网络科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/cptc.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.hmnst.com/index.html\" target=\"_blank\" title=\"微纳感知（合肥）技术有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/weinaganzhi.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://mimirii.com/\" target=\"_blank\" title=\"西安米默网络科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/mimokeji.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.geostar.com.cn/\" target=\"_blank\" title=\"吉奥时空\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/jieaoshikong.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.stbella.cn/\" target=\"_blank\" title=\"贝康国际\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/beikangguoji.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.chually.cn/\" target=\"_blank\" title=\"湖北楚商联盟金融信息服务有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/chushangjinfu.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.zhongyuankeji.cn/\" target=\"_blank\" title=\"山东众远信息科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/zhongyuankeji.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://xmnk.cn/\" target=\"_blank\" title=\"希梦耐康网络科技\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/ximengnaikang.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://hxp.liuxin.online/\" target=\"_blank\" title=\"沪小漂\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/hero.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.brath.cn\" target=\"_blank\" title=\"荔知AI助手\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/lizhi-ai.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.ninthpalace.com/\" target=\"_blank\" title=\"苏州九宫数字科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/jiugongshuzi.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.mall4j.com/\" target=\"_blank\" title=\"广州市蓝海创新科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/guangzhoulanhai.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://yimei.liuxin.online/\" target=\"_blank\" title=\"逸玫工作室\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/yimei-black.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.njhrchina.top/\" target=\"_blank\" title=\"南京桓瑞软件科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/nanjing-hengrui.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://sohelp.net/\" target=\"_blank\" title=\"宁波互邦软件有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/ningbohubang.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.fakamiao.com/\" target=\"_blank\" title=\"秦皇岛桃猫信息科技有限责任公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/taomaoxinxi.png\" style=\"max-height: 80px;\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://jiagouyizhan.com/\" target=\"_blank\" title=\"可持续架构（菏泽）信息科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/jiagouyizhan.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.symtc.com/\" target=\"_blank\" title=\"沈阳地铁\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/shenyangditie.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.meixxx.com/\" target=\"_blank\" title=\"美象信息\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/meixiangxinxi.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.3into1.cn/\" target=\"_blank\" title=\"杭州三之一智联科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/3into1.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.kingchuangcloud.com/\" target=\"_blank\" title=\"北京金创云医疗健康科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/jinchuangzhongcheng.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.guihaisoft.cn\" target=\"_blank\" title=\"泰州归海软件有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/guihairuanjian.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.dargin.com.cn/\" target=\"_blank\" title=\"北京达净科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/dajingkeji.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.sitime.vip/\" target=\"_blank\" title=\"思年华信息科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/sinianhuakeji.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.tiyuanai.cn\" target=\"_blank\" title=\"广州市题渊网络科技有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/tiyuanwangluo.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.tutuzu.cn\" target=\"_blank\" title=\"宁波埃图电子商务有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/aitukeji.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.xunmengvip.com\" target=\"_blank\" title=\"安徽梦馨信息技术有限公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/xunmenghehuoren.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://ruoyi.plus/\" target=\"_blank\" title=\"湛江市麻章区湖光镇若依科技工作室\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/ruoyi-gongzuoshi.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://aiflowy.tech\" target=\"_blank\" title=\"AIFlowy - 开源的 AI 智能体开发平台\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/aiflowy.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t<a href=\"https://www.klszkj.com\" target=\"_blank\" title=\"昆仑数智科技有限责任公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/kunlunshuzhi.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.haozpay.com/\" target=\"_blank\" title=\"皓臻云（重庆）科技有限责任公司\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/com/haozpay.png\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t</div>\n\t\t\t\t\t<div style=\"height: 10px; clear: both;\"></div>\n\t\t\t\t\t<p>\n\t\t\t\t\t\t（如果您的企业也使用了 Sa-Token，您可以\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/sa-token/issues/I3EV1M\" target=\"_blank\"\n\t\t\t\t\t\t\tstyle=\"text-decoration: none;\">在此</a>\n\t\t\t\t\t\t提交）\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div style=\"height: 60px;\"></div>\n\t\t\t</div>\n\n\n\t\t\t<!-- ------------ Dromara 成员项目 ------------- -->\n\t\t\t<div>\n\t\t\t\t<div class=\"com-box-f s-width\">\n\t\t\t\t\t<div class=\"s-fenge\"></div>\n\t\t\t\t\t<br>\n\t\t\t\t\t<h2 class=\"s-title\">\n\t\t\t\t\t\tDromara 成员项目\n\t\t\t\t\t</h2>\n\t\t\t\t\t<div class=\"com-box com-box-you table-show-pj\">\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/TLog\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/tlog.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个轻量级的分布式日志标记追踪神器，10分钟即可接入，自动对日志打标签完成微服务的链路追踪\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/liteFlow\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/liteflow.png\"\n\t\t\t\t\t\t\t\tmsg=\"轻量，快速，稳定，可编排的组件式流程引擎\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://hutool.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/hutool.jpg\"\n\t\t\t\t\t\t\t\tmsg=\"小而全的Java工具类库，使Java拥有函数式语言般的优雅，让Java语言也可以“甜甜的”。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://sa-token.cc/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/sa-token.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个轻量级 java 权限认证框架，让你的鉴权变得简单、优雅！\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/hmily\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/hmily.png\"\n\t\t\t\t\t\t\t\tmsg=\"高性能一站式分布式事务解决方案。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/Raincat\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/raincat.png\"\n\t\t\t\t\t\t\t\tmsg=\"强一致性分布式事务解决方案。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/myth\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/myth.png\"\n\t\t\t\t\t\t\t\tmsg=\"可靠消息分布式事务解决方案。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://cubic.jiagoujishu.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/cubic.png\"\n\t\t\t\t\t\t\t\tmsg=\"一站式问题定位平台，以agent的方式无侵入接入应用，完整集成arthas功能模块，致力于应用级监控，帮助开发人员快速定位问题\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://maxkey.top/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/maxkey.png\"\n\t\t\t\t\t\t\t\tmsg=\"业界领先的身份管理和认证产品\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://forest.dtflyx.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/forest-logo.png\"\n\t\t\t\t\t\t\t\tmsg=\"Forest能够帮助您使用更简单的方式编写Java的HTTP客户端\" nf>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://jpom.top/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/jpom.png\"\n\t\t\t\t\t\t\t\tmsg=\"一款简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://su.usthe.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/sureness.png\"\n\t\t\t\t\t\t\t\tmsg=\"面向 REST API 的高性能认证鉴权框架\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://easy-es.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/easy-es2.png\"\n\t\t\t\t\t\t\t\tmsg=\"傻瓜级ElasticSearch搜索引擎ORM框架\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/northstar\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/northstar_logo.png\"\n\t\t\t\t\t\t\t\tmsg=\"Northstar盈富量化交易平台\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://dromara.gitee.io/fast-request/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/fast-request.gif\"\n\t\t\t\t\t\t\t\tmsg=\"Idea 版 Postman，为简化调试API而生\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.jeesuite.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/mendmix.png\"\n\t\t\t\t\t\t\t\tmsg=\"开源分布式云原生架构一站式解决方案\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/koalas-rpc\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/koalas-rpc2.png\"\n\t\t\t\t\t\t\t\tmsg=\"企业生产级百亿日PV高可用可拓展的RPC框架。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://async.sizegang.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/gobrs-async.png\"\n\t\t\t\t\t\t\t\tmsg=\"配置极简功能强大的异步任务动态编排框架\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://dynamictp.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dynamic-tp.png\"\n\t\t\t\t\t\t\t\tmsg=\"基于配置中心的轻量级动态可监控线程池\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.x-easypdf.cn\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/x-easypdf.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个用搭积木的方式构建pdf的框架（基于pdfbox）\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://dromara.gitee.io/image-combiner\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/image-combiner.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个专门用于图片合成的工具，没有很复杂的功能，简单实用，却不失强大\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.herodotus.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dante-cloud2.png\"\n\t\t\t\t\t\t\t\tmsg=\"Dante-Cloud 是一款企业级微服务架构和服务能力开发平台。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.mtruning.club\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/go-view.png\"\n\t\t\t\t\t\t\t\tmsg=\"低代码数据可视化开发平台\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://tangyh.top/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/lamp-cloud.png\"\n\t\t\t\t\t\t\t\tmsg=\"微服务中后台快速开发平台，支持租户(SaaS)模式、非租户模式\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.redisfront.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/redis-front.png\"\n\t\t\t\t\t\t\t\tmsg=\"RedisFront 是一款开源免费的跨平台 Redis 桌面客户端工具, 支持单机模式, 集群模式, 哨兵模式以及 SSH 隧道连接, 可轻松管理Redis缓存数据.\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.yuque.com/u34495/mivcfg\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/electron-egg.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个入门简单、跨平台、企业级桌面软件开发框架\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/open-capacity-platform\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\"\n\t\t\t\t\t\t\t\tdata-original=\"/big-file/index/dromara/open-capacity-platform.jpg\"\n\t\t\t\t\t\t\t\tmsg=\"简称ocp是基于Spring Cloud的企业级微服务框架(用户权限管理，配置中心管理，应用管理，....)\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://easy-trans.fhs-opensource.top/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/easy_trans.png\"\n\t\t\t\t\t\t\t\tmsg=\"Easy-Trans 一个注解搞定数据翻译,减少30%SQL代码量\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/neutrino-proxy\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/neutrino-proxy.svg\"\n\t\t\t\t\t\t\t\tmsg=\"一款基于 Netty 的、开源的内网穿透神器。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/zyplayer-doc\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/zyplayer-doc.png\"\n\t\t\t\t\t\t\t\tmsg=\"zyplayer-doc是一款适合团队和个人使用的WIKI文档管理工具，同时还包含数据库文档、Api接口文档。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/payment-spring-boot\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/payment-spring-boot.png\"\n\t\t\t\t\t\t\t\tmsg=\"最全最好用的微信支付V3 Spring Boot 组件。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.j2eefast.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/j2eefast.png\"\n\t\t\t\t\t\t\t\tmsg=\"J2eeFAST 是一个致力于中小企业 Java EE 企业级快速开发平台,我们永久开源!\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/data-compare\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dataCompare.png\"\n\t\t\t\t\t\t\t\tmsg=\"数据库比对工具：hive 表数据比对，mysql、Doris 数据比对，实现自动化配置进行数据比对，避免频繁写sql 进行处理，低代码(Low-Code) 平台\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/open-giteye-api\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/open-giteye-api.svg\"\n\t\t\t\t\t\t\t\tmsg=\"giteye.net 是专为开源作者设计的数据图表服务工具类站点，提供了包括 Star 趋势图、贡献者列表、Gitee指数等数据图表服务。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/RuoYi-Vue-Plus\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/RuoYi-Vue-Plus.png\"\n\t\t\t\t\t\t\t\tmsg=\"后台管理系统 重写 RuoYi-Vue 所有功能 集成 Sa-Token + Mybatis-Plus + Jackson + Xxl-Job + SpringDoc + Hutool + OSS 定期同步\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/RuoYi-Cloud-Plus\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/RuoYi-Cloud-Plus.png\"\n\t\t\t\t\t\t\t\tmsg=\"微服务管理系统 重写RuoYi-Cloud所有功能 整合 SpringCloudAlibaba Dubbo3.0 Sa-Token Mybatis-Plus MQ OSS ES Xxl-Job Docker 全方位升级 定期同步\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/stream-query\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/stream-query.png\"\n\t\t\t\t\t\t\t\tmsg=\"允许完全摆脱 Mapper 的 mybatis-plus 体验！封装 stream 和 lambda 操作进行数据返回处理。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://wind.kim/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/sms4j.png\"\n\t\t\t\t\t\t\t\tmsg=\"短信聚合工具，让发送短信变的更简单。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://cloudeon.top/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/cloudeon.png\"\n\t\t\t\t\t\t\t\tmsg=\"简化kubernetes上大数据集群的运维管理\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://github.com/dromara/hodor\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/hodor.png\"\n\t\t\t\t\t\t\t\tmsg=\"Hodor是一个专注于任务编排和高可用性的分布式任务调度系统。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://nsrule.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/test-hub.png\"\n\t\t\t\t\t\t\t\tmsg=\"流程编排，插件驱动，测试无限可能\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/disjob\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/disjob-2.png\"\n\t\t\t\t\t\t\t\tmsg=\"Disjob是一个分布式的任务调度框架\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/binlog4j\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/Binlog4j.png\"\n\t\t\t\t\t\t\t\tmsg=\"轻量级 Mysql Binlog 客户端, 提供宕机续读, 高可用集群等特性\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/yft-design\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/yft-design.png\"\n\t\t\t\t\t\t\t\tmsg=\"基于 Canvas 的开源版 创客贴 支持导出json，svg, image文件。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/x-file-storage\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/x-file-storage.svg\"\n\t\t\t\t\t\t\t\tmsg=\"在 SpringBoot 中通过简单的方式将文件存储到 本地、阿里云 OSS、腾讯云 COS、七牛云 Kodo等\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://wemq.nicholasld.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/wemq.png\"\n\t\t\t\t\t\t\t\tmsg=\"开源、高性能、安全、功能强大的物联网调试和管理解决方案。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/mayfly-go\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/mayfly-go.png\"\n\t\t\t\t\t\t\t\tmsg=\"web 版 linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库（mysql postgres）、redis(单机 哨兵 集群)、mongo 统一管理操作平台\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://akali.yomahub.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/akali.png\"\n\t\t\t\t\t\t\t\tmsg=\"Akali(阿卡丽)，轻量级本地化热点检测/降级框架，10秒钟即可接入使用！大流量下的神器\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/dbswitch\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dbswitch.png\"\n\t\t\t\t\t\t\t\tmsg=\"异构数据库迁移同步(搬家)工具。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/easyAi\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/easyAI.png\"\n\t\t\t\t\t\t\t\tmsg=\"Java 傻瓜式 AI 框架。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/tianai-captcha\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/tianai-captcha.png\"\n\t\t\t\t\t\t\t\tmsg=\"可能是java界最好的开源行为验证码 captcha、captcha、captcha、captcha、tianai-captcha [滑块验证码、点选验证码、行为验证码、旋转验证码， 滑动验证码]。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/mybatis-plus-ext\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/mybatis-plus-ext.png\"\n\t\t\t\t\t\t\t\tmsg=\"mybatis-plus 框架的增强拓展包。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/dax-pay\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dax-pay.png\"\n\t\t\t\t\t\t\t\tmsg=\"免费开源的支付网关。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/sayOrder\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/sayorder.png\"\n\t\t\t\t\t\t\t\tmsg=\"基于easyAi引擎的JAVA高性能，低成本，轻量级智能客服。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/mybatis-jpa-extra\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/mybatis-jpa-extra.png\"\n\t\t\t\t\t\t\t\tmsg=\"扩展MyBatis JPA支持，简化CUID操作，增强SELECT分页查询\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://newcar.js.org/zh/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/newcar.png\"\n\t\t\t\t\t\t\t\tmsg=\"现代化的动画引擎\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://warm-flow.cn\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/warm-flow.png\"\n\t\t\t\t\t\t\t\tmsg=\"国产自研工作流，其特点简洁(只有6张表)但又不简单，五脏俱全，组件独立，可扩展，可满足中小项目的组件。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/dy-java\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dy-java.png\"\n\t\t\t\t\t\t\t\tmsg=\"DyJava是一款功能强大的抖音Java开发工具包\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/MilvusPlus\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/MilvusPlus-logo.png\"\n\t\t\t\t\t\t\t\tmsg=\"MilvusPlus（简称 MP）是一个 Milvus 的操作工具，旨在简化与 Milvus 向量数据库的交互，为开发者提供类似 MyBatis-Plus 注解和方法调用风格的直观 API,提高效率而生。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.easy-query.com/easy-query-doc/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/easy-query.png\"\n\t\t\t\t\t\t\t\tmsg=\"java下唯一一款同时支持强类型对象关系查询和强类型SQL语法查询的ORM,拥有对象模型筛选、隐式子查询、隐式join、显式子查询、显式join,支持Java/Kotlin\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/orion-visor\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/horizontal.png\"\n\t\t\t\t\t\t\t\tmsg=\"一款高颜值、现代化的智能运维&轻量堡垒机平台。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.ujcms.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/ujcms.png\"\n\t\t\t\t\t\t\t\tmsg=\"Java开源网站内容管理系统(java cms)。使用SpringBoot、MyBatis、Vue3、ElementPlus、Vite、TypeScript等技术开发。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/skyeye\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/skyeye-logo.png\"\n\t\t\t\t\t\t\t\tmsg=\"智能制造一体化，采用Springboot + winUI的低代码平台开发模式。包含30多个应用模块、50多种电子流程\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://domain-admin.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/domain-admin.png\"\n\t\t\t\t\t\t\t\tmsg=\"SSL证书监测平台，申请证书，自动续签，到期提醒。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/carbon\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/carbon.svg\"\n\t\t\t\t\t\t\t\tmsg=\"轻量级、语义化、对开发者友好的 golang 时间处理库\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/mica-mqtt\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/mica-mqtt.png\"\n\t\t\t\t\t\t\t\tmsg=\"java mqtt 基于 java aio 实现，开源、简单、易用、低延迟、高性能百万级 java mqtt client 组件和 java mqtt broker 服务。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/wgai\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/wgai.png\"\n\t\t\t\t\t\t\t\tmsg=\"开箱即用的JAVA AI平台融合了AI图像识别opencv、yolo、esayAI内核识别;AI智能客服、AI语言模型、可定制化自主离线化部署并自主化行业化使用 避免占用内存、GPU消耗训练与识别分开使用;支持yolov3、yolov5、yolov8模型 支持exel、txt等文本语言模型；\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/omega-ai\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/omega-ai.png\"\n\t\t\t\t\t\t\t\tmsg=\"基于java打造的深度学习框架，帮助你快速搭建神经网络，实现模型推理与训练，引擎支持自动求导，多GPU训练，GPU支持CUDA，CUDNN。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://github.com/dromara/rsmedia\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/rsmedia.png\"\n\t\t\t\t\t\t\t\tmsg=\"audio/video toolkit based FFmpeg 6.x, 7.x supported for multimedia with Hardware Acceleration.\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/sqlrest\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/SQLREST.png\"\n\t\t\t\t\t\t\t\tmsg=\"SQLREST是一款完全开源的SQL转 RESTful API的SQL2API低代码工具\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/easy-tl\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/easy-tl.png\"\n\t\t\t\t\t\t\t\tmsg=\"EasyTL 是一个轻量级的字符串模板引擎，基于 Java 8 开发，无第三方依赖，提供类似 JavaScript 的表达式语法支持。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/dongle\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dongle.svg\"\n\t\t\t\t\t\t\t\tmsg=\"轻量级、语义化、对开发者友好的 golang 编码解码、加密解密和签名验签库\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/auto-table\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/auto-table.png\"\n\t\t\t\t\t\t\t\tmsg=\"Java最强数据库构建框架，超越JPA，根据 Java 实体，自动创建数据库、表、索引。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/free-fs\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/free-fs.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个基于 Spring Boot 3.x 的企业级文件管理网盘系统后端，专注于提供高性能、高可靠的文件存储和管理服务。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/surpass\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/surpass.png\"\n\t\t\t\t\t\t\t\tmsg=\"Surpass是API开放平台，支持OpenAPI定义，面向REST API资源无状态认证和调用，实现企业统一权限管理\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/dromara/my-obj\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/my-obj.png\"\n\t\t\t\t\t\t\t\tmsg=\"现代化的私有云存储解决方案\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t<!-- <a href=\"https://dromara.org/zh/projects/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/dromara/dromara.png\"\n\t\t\t\t\t\t\t\tmsg=\"让每一位开源爱好者，体会到开源的快乐。\">\n\t\t\t\t\t\t</a> -->\n\t\t\t\t\t</div>\n\t\t\t\t\t<div style=\"height: 10px; clear: both;\"></div>\n\t\t\t\t\t<p>\n\t\t\t\t\t\t为往圣继绝学，一个人或许能走的更快，但一群人会走的更远。\n\t\t\t\t\t</p>\n\t\t\t\t\t<!-- <div style=\" margin: 40px 14px 0; padding: 20px 0 10px; background-color: #f4f5f7;\">\n\t\t\t\t\t\t<h3 style=\"padding: 0px 0 10px; \">Dromara 知识星球</h3>\n\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/contact/dromara-xingqiu--sa-token.jpg\"\n\t\t\t\t\t\t\tstyle=\"width: 300px;\">\n\t\t\t\t\t</div> -->\n\t\t\t\t</div>\n\t\t\t\t<div style=\"height: 30px;\"></div>\n\t\t\t</div>\n\n\t\t\t<!-- ------------ 友情链接 ------------- -->\n\t\t\t<div>\n\t\t\t\t<div class=\"com-box-f s-width\">\n\t\t\t\t\t<div class=\"s-fenge\"></div>\n\t\t\t\t\t<br>\n\t\t\t\t\t<h2 class=\"s-title\">友情链接</h2>\n\t\t\t\t\t<div class=\"com-box com-box-you\">\n\t\t\t\t\t\t<a href=\"https://ok.zhxu.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/okhttps.png\"\n\t\t\t\t\t\t\t\tmsg=\"如艺术一般优雅，像 1、2、3 一样简单，前后端通用，轻量却强大的 HTTP 客户端（同时支持 WebSocket 以及 Stomp 协议）\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://bs.zhxu.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/bean-searcher.png\"\n\t\t\t\t\t\t\t\tmsg=\"轻量级关系数据库条件检索引擎，使一行代码实现复杂列表检索成为可能！\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://xiaonuo.vip/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/xiaonuo2.png\"\n\t\t\t\t\t\t\t\tmsg=\"通用型后台权限管理框架，紧随潮流、开箱即用, 同时拥有Vue、Layui、SpringCloud三个版本\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.pearadmin.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/pear-admin.png\"\n\t\t\t\t\t\t\t\tmsg=\"致 力 于 让 Web 开 发 变 得 简 单 优 雅\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://www.layui-vue.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/layui-vue.png\"\n\t\t\t\t\t\t\t\tmsg=\"layui - vue（谐音：类 UI) 是 一 套 Vue 3.0 的 桌 面 端 组 件 库.\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://shenyu.apache.org/zh/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/shenyu.svg\"\n\t\t\t\t\t\t\t\tmsg=\"一个异步的，高性能的，跨语言的，响应式的 API 网关。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://dwz.cn/L9hCwepg\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/yungouos.png\"\n\t\t\t\t\t\t\t\tmsg=\"官方直连支付系统解决方案，支持个人、个体户、企业全渠道签约。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://hippo4j.cn/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/hippo4j.jpg\"\n\t\t\t\t\t\t\t\tmsg=\"强大的动态线程池框架，附带监控报警功能，支持 Tomcat、Jetty、Undertow、RocketMQ、Dubbo、RabbitMQ、Hystrix 消费线程池\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://gitee.com/gz-yami/mall4j\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/mall4j.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个基于Spring Boot 3 JDK17的商城系统。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"http://solon.noear.org/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/solon.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个更现代感的应用开发框架：更快、更小、更自由。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://baomidou.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/mybatis-plus.png\"\n\t\t\t\t\t\t\t\tstyle=\"max-width: 110%;\"\n\t\t\t\t\t\t\t\tmsg=\"MyBatis-Plus（简称 MP）是一个 MyBatis 的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://www.mvncenter.com\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/mvn-center.jpg\"\n\t\t\t\t\t\t\t\tstyle=\"max-width: 110%;\"\n\t\t\t\t\t\t\t\tmsg=\"Maven中文站\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://hertzbeat.com/\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/hertzbeat-brand.svg\"\n\t\t\t\t\t\t\t\tmsg=\"易用友好的云监控系统\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://chat2db-ai.com\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/chat2db.png\"\n\t\t\t\t\t\t\t\tmsg=\"一个AI驱动的数据库管理和BI工具，支持Mysql、pg、Oracle、Redis等22种数据库的管理。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"https://onlinenotepad101.org/zh\" target=\"_blank\">\n\t\t\t\t\t\t\t<img class=\"lazy\" data-original=\"/big-file/index/link/notepad.png\"\n\t\t\t\t\t\t\t\tmsg=\"在线记事本 – 免费在线文本编辑器：专为无干扰写作、高效记笔记。完全免费，无需注册，打开即用，支持富文本内容编辑。支持保存、导出，以及分享给其他人。简洁流畅，专注创作。\">\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div style=\"height: 10px; clear: both;\"></div>\n\t\t\t\t</div>\n\t\t\t\t<div style=\"height: 60px;\"></div>\n\t\t\t</div>\n\n\n\t\t\t<!-- ------------ 底部 连接 ------------- -->\n\t\t\t<div id=\"footer\">\n\t\t\t\t<div id=\"s-footer\" class=\"mao-link\"></div>\n\t\t\t\t<div class=\"footer-r-b s-width\">\n\t\t\t\t\t<div class=\"ss-box\">\n\t\t\t\t\t\t<h3>特别鸣谢</h3>\n\t\t\t\t\t\t<ul class=\"list-unstyle\">\n\t\t\t\t\t\t\t<!-- <li><a href=\"https://dromara.org/zh/projects/\" target=\"_blank\">Dromara社区</a></li> -->\n\t\t\t\t\t\t\t<li><a href=\"https://gitee.com/dromara/liteFlow\" target=\"_blank\">LiteFlow 规则引擎</a></li>\n\t\t\t\t\t\t\t<li><a href=\"https://gitee.com/dromara/forest\" target=\"_blank\">Forest 工具</a></li>\n\t\t\t\t\t\t\t<li><a href=\"https://gitee.com/Apache-ShenYu/incubator-shenyu\" target=\"_blank\">ShenYu 网关</a></li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"ss-box\">\n\t\t\t\t\t\t<h3>技术干货</h3>\n\t\t\t\t\t\t<ul class=\"list-unstyle\">\n\t\t\t\t\t\t\t<li><a href=\"https://juejin.cn/user/3702810894945422\" target=\"_blank\">稀土掘金</a></li>\n\t\t\t\t\t\t\t<li><a href=\"https://blog.csdn.net/shengzhang_\" target=\"_blank\">CSDN</a></li>\n\t\t\t\t\t\t\t<li><a href=\"https://my.oschina.net/u/3503445\" target=\"_blank\">OSCHINA</a></li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"ss-box\">\n\t\t\t\t\t\t<h3>关注我们</h3>\n\t\t\t\t\t\t<ul class=\"list-unstyle\">\n\t\t\t\t\t\t\t<li><a href=\"https://space.bilibili.com/3546758575557094\" target=\"_blank\">哔哩哔哩</a></li>\n\t\t\t\t\t\t\t<li><a href=\"https://www.douyin.com/user/MS4wLjABAAAArVqj2lGRurfj-9eO0T12q6_vrbIK-Om9bi3eo4OwB2g\" target=\"_blank\">抖音</a></li>\n\t\t\t\t\t\t\t<li><a href=\"https://www.xiaohongshu.com/user/profile/67063b01000000001e00ef23\" target=\"_blank\">小红书</a></li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t\t<!-- <div class=\"ss-box\">\n\t\t\t\t\t\t<h3>联系我们</h3>\n\t\t\t\t\t\t<ul class=\"list-unstyle\">\n\t\t\t\t\t\t\t<li>QQ群 ：<a href=\"doc.html#/more/join-group\">点击加入</a></li>\n\t\t\t\t\t\t\t<li>邮箱：<a href=\"javascript: alert('暂无');\">暂无</a></li>\n\t\t\t\t\t\t\t<li>联系：<a href=\"javascript: alert('暂无');\">暂无</a></li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div> -->\n\t\t\t\t\t<div class=\"ss-box\">\n\t\t\t\t\t\t<h3 class=\"last\" style=\"text-align: left; float: none; padding-left: 0px;\">Sa-Token 公众号</h3>\n\t\t\t\t\t\t<div class=\"media-img padding-small-top\" style=\"text-align: left;\">\n\t\t\t\t\t\t\t<img class=\"dro-qr\" src=\"/big-file/contact/lykj-gzh.jpg\" width=\"100\"\n\t\t\t\t\t\t\t\theight=\"100\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- -------------- 底部 版权 -------------- -->\n\t\t\t<div>\n\t\t\t\t<meta charset=\"UTF-8\">\n\t\t\t\t<style type=\"text/css\">\n\n\t\t\t\t</style>\n\t\t\t\t<div class=\"foot-box\" id=\"foot\">\n\t\t\t\t\t<div class=\"s-width\" style=\"text-align: center;\">\n\t\t\t\t\t\tCopyright ©2025 Sa-Token java 权限认证 | sa-token.cc | <a href=\"https://beian.miit.gov.cn/\"\n\t\t\t\t\t\t\ttarget=\"_blank\">鲁ICP备18046274号-4</a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t</div>\n\n\t\t<!-- UI逐渐显现 -->\n\t\t<style type=\"text/css\">\n\t\t\tbody {\n\t\t\t\topacity: 0.01;\n\t\t\t\ttransition: opacity 0.5s;\n\t\t\t}\n\t\t</style>\n\t\t<script type=\"text/javascript\">\n\t\t\tsetTimeout(function() {\n\t\t\t\tdocument.body.style.opacity = 1;\n\t\t\t}, 1);\n\t\t</script>\n\n\t\t<!-- 搜索引擎自动提交 -->\n\t\t<script>\n\t\t\t(function() {\n\t\t\t\tvar bp = document.createElement('script');\n\t\t\t\tvar curProtocol = window.location.protocol.split(':')[0];\n\t\t\t\tif (curProtocol === 'https') {\n\t\t\t\t\tbp.src = 'https://zz.bdstatic.com/linksubmit/push.js';\n\t\t\t\t} else {\n\t\t\t\t\tbp.src = 'http://push.zhanzhang.baidu.com/push.js';\n\t\t\t\t}\n\t\t\t\tvar s = document.getElementsByTagName(\"script\")[0];\n\t\t\t\ts.parentNode.insertBefore(bp, s);\n\t\t\t})();\n\t\t</script>\n\t\t<!-- 百度统计 -->\n\t\t<script>\n\t\t\tvar _hmt = _hmt || [];\n\t\t\t(function() {\n\t\t\t\tvar hm = document.createElement(\"script\");\n\t\t\t\thm.src = \"https://hm.baidu.com/hm.js?35ad501304eae758ac6139a22a9830f5\";\n\t\t\t\tvar s = document.getElementsByTagName(\"script\")[0];\n\t\t\t\ts.parentNode.insertBefore(hm, s);\n\t\t\t})();\n\t\t</script>\n\n\t\t<!-- 悬浮效果 -->\n\t\t<script src=\"static/jquery.min.js\"></script>\n\t\t<script src=\"static/layer-v3.1.1/layer.js\"></script>\n\t\t\n\t\t<!-- 赞助者名单 -->\n\t\t<script src=\"static/donate/donate-list.js\"></script>\n\t\t<script src=\"static/donate/donate-fun.js\"></script>\n\t\t<script type=\"text/javascript\">renderDonateTable();</script>\n\t\t<script type=\"text/javascript\">\n\t\t\t\n\t\t\t// 鼠标悬浮在友情链接时，提示信息 \n\t\t\t$(\".com-box-you a img\").hover(function() {\n\t\t\t\tvar msg = $(this).attr(\"msg\");\n\t\t\t\tif (msg) {\n\t\t\t\t\twindow.msgLayer = layer.tips(msg, $(this), {\n\t\t\t\t\t\ttips: 1,\n\t\t\t\t\t\ttime: 0\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}, function() {\n\t\t\t\tvar index = window.msgLayer;\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\tlayer.close(index);\n\t\t\t\t}, 1000);\n\t\t\t});\n\t\t\t// 点击二维码放大\n\t\t\t$('.wx-qr,.dro-qr,.gzh-qr').click(function() {\n\t\t\t\tvar w = '300px';\n\t\t\t\tvar h = 'auto';\n\t\t\t\tvar content = '<div style=\"height: 100%; overflow: hidden !important;\">' +\n\t\t\t\t\t'<img src=\"' + this.src + ' \" style=\"width: 100%; height: 100%;\" />' +\n\t\t\t\t\t'</div>';\n\t\t\t\tlayer.open({\n\t\t\t\t\ttype: 1,\n\t\t\t\t\ttitle: false,\n\t\t\t\t\tshadeClose: true,\n\t\t\t\t\tcloseBtn: 0,\n\t\t\t\t\tarea: [w, h], //宽高\n\t\t\t\t\tcontent: content\n\t\t\t\t});\n\t\t\t})\n\t\t</script>\n\n\t\t<!-- 初始化轮播图 -->\n\t\t<script src=\"static/swiper/swiper-bundle.min.js\"></script>\n\t\t<script src=\"static/swiper/index-swiper.js\"></script>\n\n\t\t<!-- 修改背景颜色 -->\n\t\t<script>\n\t\t\t// // 绑定修改背景色的按钮事件\n\t\t\t// $('.theme-box span').click(function() {\n\t\t\t// \tlet bgColor = this.style.backgroundColor;\n\t\t\t// \tsetBg(bgColor);\n\t\t\t// \tlocalStorage.setItem('bg-color-value', bgColor)\n\t\t\t// })\n\t\t\t// // 读取上次记录\n\t\t\t// let bgColor = localStorage.getItem('bg-color-value');\n\t\t\t// if (bgColor) {\n\t\t\t// \tsetBg(bgColor);\n\t\t\t// }\n\n\t\t\t// // 设置背景颜色 \n\t\t\t// function setBg(bgColor) {\n\t\t\t// \tconsole.log('---- 背景颜色设定为：', bgColor);\n\n\t\t\t// \t// -------- 设置 body 背景\n\t\t\t// \tdocument.body.style.backgroundColor = bgColor;\n\n\t\t\t// \t// -------- 设置 header 头背景\n\t\t\t// \t// 如果是 16 进制，转 rgba\n\t\t\t// \tif (bgColor.indexOf('#') == 0) {\n\t\t\t// \t\tbgColor = hexToRgba(bgColor, 0.97);\n\t\t\t// \t}\n\t\t\t// \t// 如果是 rgb，转 rgba\n\t\t\t// \telse if (bgColor.match(/\\,/g).length == 2) {\n\t\t\t// \t\tbgColor = bgColor.replace(')', ' ,0.97)');\n\t\t\t// \t}\n\n\t\t\t// \tdocument.querySelector('.doc-header').style.backgroundColor = bgColor;\n\t\t\t// }\n\n\t\t\t// // 16进制 转 rgba\n\t\t\t// function hexToRgba(str, a) {\n\t\t\t// \ta = a || 1;\n\n\t\t\t// \tvar reg = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/\n\t\t\t// \tif (!reg.test(str)) {\n\t\t\t// \t\treturn;\n\t\t\t// \t}\n\t\t\t// \tlet newStr = (str.toLowerCase()).replace(/\\#/g, '')\n\t\t\t// \tlet len = newStr.length;\n\t\t\t// \tif (len == 3) {\n\t\t\t// \t\tlet t = ''\n\t\t\t// \t\tfor (var i = 0; i < len; i++) {\n\t\t\t// \t\t\tt += newStr.slice(i, i + 1).concat(newStr.slice(i, i + 1))\n\t\t\t// \t\t}\n\t\t\t// \t\tnewStr = t\n\t\t\t// \t}\n\t\t\t// \tlet arr = []; //将字符串分隔，两个两个的分隔\n\t\t\t// \tfor (var i = 0; i < 6; i = i + 2) {\n\t\t\t// \t\tlet s = newStr.slice(i, i + 2)\n\t\t\t// \t\tarr.push(parseInt(\"0x\" + s))\n\t\t\t// \t}\n\t\t\t// \treturn 'rgb(' + arr.join(\",\") + ', ' + a + ')';\n\t\t\t// }\n\t\t</script>\n\n\t\t<!-- 图片懒加载 -->\n\t\t<script src=\"static/jquery.lazyload-1.9.3.js\"></script>\n\t\t<script>\n\t\t\t$(function() {\n\t\t\t\t$(\"img.lazy\").lazyload({\n\t\t\t\t\teffect: \"fadeIn\", // 动画，show=显示，fadeIn=淡入，slideDown=下拉\n\t\t\t\t\teffectspeed: 1200, // 动画持续时间\n\t\t\t\t\tskip_invisible: true, // 不加载隐藏的图像\n\t\t\t\t\t// threshold: -180,  // 提前加载：距离屏幕多少px时就显示出来\n\t\t\t\t\t// event: 'click',  // 事件触发时才加载，scroll=滑动，click=点击，mouseover=鼠标划过，sporty=运动的\n\t\t\t\t\t// 未加载时的占位图，此为3x3透明小图片\n\t\t\t\t\tplaceholder: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAhdEVYdENyZWF0aW9uIFRpbWUAMjAyMTowMToyMiAyMjoxNDoxM63SwyUAAAANSURBVBhXYyAGMDAAAAAnAAF2ypRxAAAAAElFTkSuQmCC\",\n\t\t\t\t\tload: function() {\n\t\t\t\t\t\tconsole.log('lazy img: ' + this.src);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t})\n\t\t</script>\n\t\t\n\t\t<!-- 预览版提示 -->\n\t\t<script type=\"text/javascript\">\n\t\t\tif (location.host === 'rc.sa-token.cc') {\n\t\t\t\tconst newTips =\n\t\t\t\t\t'<b>当前文档为RC预览版文档，仅做学习测试使用，正式项目请使用正式版：<a href=\"https://sa-token.cc/\" target=\"_blank\">https://sa-token.cc/</a></b>';\n\t\t\t\tlayer.alert(newTips);\n\t\t\t}\n\t\t</script>\n\t\t\n\t\t<script>\n\t\t\t// 逐字打印效果\n\t\t\tvar tcStr = '开源、免费、一站式 java 权限认证框架，让鉴权变得简单、优雅！';\n\t\t\tvar con = $('.sub-title .sub-title-nr');\n\t\t\tvar index = 1;\n\t\t\tvar length = tcStr.length;\n\t\t\tvar tId = null;\n\t\t\t \n\t\t\tfunction start(){\n\t\t\t\tcon.text(tcStr.charAt(0));\n\n\t\t\t\ttId = setInterval(function(){\n\t\t\t\t\tcon.append(tcStr.charAt(index));\n\t\t\t\t\tif(index++ === length){\n\t\t\t\t\t\tclearInterval(tId);\n\t\t\t\t\t\tindex = 1;\n\t\t\t\t\t\tsetTimeout(function(){\n\t\t\t\t\t\t\tstart()\n\t\t\t\t\t\t}, 3000)\n\t\t\t\t\t}\n\t\t\t\t}, 90);\n\t\t\t}\n\t\t\tstart();\n\n\t\t</script>\n\t\t\n\t\t<!-- 自定义滚动条颜色 -->\n\t\t<!-- <style>\n\t\t    /* 自定义body滚动条样式 */\n\t\t    body::-webkit-scrollbar { width: 10px; }\n\t\t\t/* 滚动条颜色 */\n\t\t    body::-webkit-scrollbar-thumb { background-color: #5BAE63; border-radius: 3px; }\n\t\t\t/* 滚动条上面的和下面的颜色 */\n\t\t    body::-webkit-scrollbar-track {\n\t\t        background: linear-gradient(to bottom, \n\t\t            #42B983 0%, \n\t\t            #42B983 var(--scroll-progress, 0%), \n\t\t            #FCFCFC var(--scroll-progress, 0%), \n\t\t            #FCFCFC 100%);\n\t\t    }\n\t\t</style>\n\t\t<script>\n\t\t    // 动态更新滚动条颜色\n\t\t    window.addEventListener('scroll', function() {\n\t\t        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;\n\t\t        const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;\n\t\t        const scrollProgress = (scrollTop / scrollHeight) * 100;\n\t\t        \n\t\t        document.body.style.setProperty('--scroll-progress', scrollProgress + '%');\n\t\t    });\n\t\t    // 初始化滚动条状态\n\t\t    window.dispatchEvent(new Event('scroll'));\n\t\t</script> -->\n\t\t\n\t\t\n\t</body>\n</html>"
  },
  {
    "path": "sa-token-doc/micro/dcs-session.md",
    "content": "# 微服务 - 分布式Session会话\n\n--- \n\n### 需求场景 \n\n微服务架构下的第一个难题便是数据同步，单机版的`Session`在分布式环境下一般不能正常工作，为此我们需要对框架做一些特定的处理。\n\n首先我们要明白，分布式环境下为什么`Session`会失效？因为用户在一个节点对会话做出的更改无法实时同步到其它的节点，\n这就导致一个很严重的问题：如果用户在节点一上已经登录成功，那么当下一次的请求落在节点二上时，对节点二来讲，此用户仍然是未登录状态。\n\n### 解决方案 \n\n要怎么解决这个问题呢？目前的主流方案有四种：\n1. **Session同步**：只要一个节点的数据发生了改变，就强制同步到其它所有节点 \n2. **Session粘滞**：通过一定的算法，保证一个用户的所有请求都稳定的落在一个节点之上，对这个用户来讲，就好像还是在访问一个单机版的服务\n3. **建立会话中心**：将Session存储在专业的缓存中间件上，使每个节点都变成了无状态服务，例如：`Redis`\n4. **颁发无状态token**：放弃Session机制，将用户数据直接写入到令牌本身上，使会话数据做到令牌自解释，例如：`jwt`\n\n\n### 方案选择\n\n该如何选择一个合适的方案？\n- 方案一：性能消耗太大，不太考虑\n- 方案二：需要从网关处动手，与框架无关\n- 方案三：Sa-Token 整合`Redis`非常简单，详见章节：[集成 Redis](/up/integ-redis)\n- 方案四：详见官方仓库中 Sa-Token 整合`jwt`的示例\n\n由于`jwt`模式不在服务端存储数据，对于比较复杂的业务可能会功能受限，因此更加推荐使用方案三\n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/micro/g3--dcs-session.gif\">加载动态演示图</button>\n\n\n集成依赖示例：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.commons</groupId>\n    <artifactId>commons-pool2</artifactId>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n详细参考：[集成 Redis](/up/integ-redis)\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/micro/gateway-auth.md",
    "content": "# 微服务 - 网关统一鉴权\n\n微服务架构下的鉴权一般分为两种：\n1. 每个服务各自鉴权\n2. 网关统一鉴权 \n\n方案一和传统单体鉴权差别不大，不再过多赘述，本篇介绍方案二的整合步骤：\n\n--- \n\n\n\n### 1、引入依赖 \n\n首先，根据 [依赖引入说明](/micro/import-intro) 引入正确的依赖，以`[SpringCloud Gateway]`为例：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证（Reactor响应式集成）, 在线文档：https://sa-token.cc -->\n<dependency>\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n    <version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.commons</groupId>\n    <artifactId>commons-pool2</artifactId>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'\n\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n<!---------------------------- tabs:end ------------------------------>\n\n\n注：Redis包是必须的，因为我们需要和各个服务通过Redis来同步数据 \n\n### 2、实现鉴权接口\n``` java\n/**\n * 自定义权限验证接口扩展 \n */\n@Component   \npublic class StpInterfaceImpl implements StpInterface {\n\n    @Override\n    public List<String> getPermissionList(Object loginId, String loginType) {\n        // 返回此 loginId 拥有的权限列表 \n        return ...;\n    }\n\n    @Override\n    public List<String> getRoleList(Object loginId, String loginType) {\n        // 返回此 loginId 拥有的角色列表\n        return ...;\n    }\n\n}\n\n```\n关于数据的获取，建议以下方案三选一：\n1. 在网关处集成ORM框架，直接从数据库查询数据\n2. 先从Redis中获取数据，获取不到时走ORM框架查询数据库 \n3. 先从Redis中获取缓存数据，获取不到时走RPC调用子服务 (专门的权限数据提供服务) 获取\n\n\n### 3、注册全局过滤器 \n然后在网关处注册全局过滤器进行鉴权操作 \n\n``` java\n/**\n * [Sa-Token 权限认证] 配置类 \n * @author click33\n */\n@Configuration\npublic class SaTokenConfigure {\n\t// 注册 Sa-Token全局过滤器 \n    @Bean\n    public SaReactorFilter getSaReactorFilter() {\n        return new SaReactorFilter()\n\t\t\t// 拦截地址 \n\t\t\t.addInclude(\"/**\")    /* 拦截全部path */\n\t\t\t// 开放地址 \n\t\t\t.addExclude(\"/favicon.ico\")\n\t\t\t// 鉴权方法：每次访问进入 \n\t\t\t.setAuth(obj -> {\n\t\t\t\t// 登录校验 -- 拦截所有路由，并排除/user/doLogin 用于开放登录 \n\t\t\t\tSaRouter.match(\"/**\", \"/user/doLogin\", r -> StpUtil.checkLogin());\n\t\t\t\t\n\t\t\t\t// 权限认证 -- 不同模块, 校验不同权限 \n\t\t\t\tSaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n\t\t\t\tSaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n\t\t\t\tSaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n\t\t\t\tSaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n\t\t\t\t\n\t\t\t\t// 更多匹配 ...  */\n\t\t\t})\n\t\t\t// 异常处理方法：每次setAuth函数出现异常时进入 \n\t\t\t.setError(e -> {\n\t\t\t\treturn SaResult.error(e.getMessage());\n\t\t\t})\n\t\t\t;\n    }\n}\n```\n\n详细操作参考：[路由拦截鉴权](/use/route-check)\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/micro/import-intro.md",
    "content": "\n# 微服务中使用Sa-Token 依赖引入说明 \n\n--- \n\n虽然在 [开始] 章节已经说明了依赖引入规则，但是交流群里不少小伙伴提出bug解决到最后发现都是因为依赖引入错误导致的，此处再次重点强调一下：\n\n> [!TIP| style:callout] \n> **在微服务架构中使用Sa-Token时，网关和内部服务要分开引入Sa-Token依赖（不要直接在顶级父pom中引入Sa-Token）**\n\n总体来讲，我们需要关注的依赖就是两个：`sa-token-spring-boot-starter` 和 `sa-token-reactor-spring-boot-starter`：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证，在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n<!---------------------------- tabs:end ---------------------------->\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n<!---------------------------- tabs:end ------------------------------>\n\n\n至于怎么分辨我们需要引入哪个呢？这个要看你使用的基础框架：\n\n对于内部基础服务来讲，我们一般都是使用SpringBoot默认的web模块：SpringMVC，\n因为这个SpringMVC是基于Servlet模型的，在这里我们需要引入的是`sa-token-spring-boot-starter`\n\n对于网关服务，大体来讲分为两种：\n- 一种是基于Servlet模型的，如：Zuul，我们需要引入的是：`sa-token-spring-boot-starter`，详细戳：[在SpringBoot环境集成](/start/example)；理论上`Zuul`并不支持`Spring Boot3`\n- 一种是基于Reactor模型的，如：SpringCloud Gateway、ShenYu 等等，我们需要引入的是：`sa-token-reactor-spring-boot-starter`，**并且注册全局过滤器！**，详细戳：[在WebFlux环境集成](/start/webflux-example)\n\n注：切不可直接在一个项目里同时引入这两个依赖，否则会造成项目无法启动\n\n另外，我们需要引入Redis集成包，因为我们的网关和子服务主要通过Redis来同步数据 \n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.commons</groupId>\n    <artifactId>commons-pool2</artifactId>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n详细参考：[集成 Redis](/up/integ-redis)\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/micro/same-token.md",
    "content": "# 微服务 - 内部服务外网隔离 \n\n--- \n\n\n### 一、需求场景  \n\n我们的子服务一般不能通过外网直接访问，必须通过网关转发才是一个合法的请求，这种子服务与外网的隔离一般分为两种：\n\n1. 物理隔离：子服务部署在指定的内网环境中，只有网关对外网开放 \n2. 逻辑隔离：子服务与网关同时暴露在外网，但是子服务会有一个权限拦截层保证只接受网关发送来的请求，绕过网关直接访问子服务会被提示：无效请求 \n\n这种鉴权需求牵扯到两个环节： **`网关转发鉴权`** 、 **`服务间内部调用鉴权`**\n\nSa-Token提供两种解决方案：\n1. 使用 OAuth2.0 模式的凭证式，将 Client-Token 用作各个服务的身份凭证进行权限校验\n2. 使用 Same-Token 模块提供的身份校验能力，完成服务间的权限认证\n\n本篇主要讲解方案二 `Same-Token` 模块的整合步骤，其鉴权流程与 OAuth2.0 类似，不过使用方式上更加简洁（希望使用方案一的同学可参考Sa-OAuth2模块，此处不再赘述）\n\n<img class=\"w-100\" src=\"/big-file/doc/micro/micro-network-isolation.svg\" alt=\"Same-Token_同源系统认证.svg\" />\n\n\n### 二、网关转发鉴权 \n\n##### 1、引入依赖\n\n在网关处引入的依赖为（此处以 SpringCloud Gateway 为例）：\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证（Reactor响应式集成）, 在线文档：https://sa-token.cc -->\n<dependency>\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n    <version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.commons</groupId>\n    <artifactId>commons-pool2</artifactId>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'\n\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n<!---------------------------- tabs:end ------------------------------>\n\n在下游子服务引入的依赖为：\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc -->\n<dependency>\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-spring-boot-starter</artifactId>\n    <version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n    <groupId>org.apache.commons</groupId>\n    <artifactId>commons-pool2</artifactId>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n<!---------------------------- tabs:end ------------------------------>\n\n##### 2、网关处添加Same-Token\n\n为网关添加全局过滤器：\n``` java\n/**\n * 全局过滤器，为请求添加 Same-Token \n */\n@Component\npublic class ForwardAuthFilter implements GlobalFilter {\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {\n\t\tServerHttpRequest newRequest = exchange\n\t\t\t\t.getRequest()\n\t\t\t\t.mutate()\n\t\t\t\t// 为请求追加 Same-Token 参数 \n\t\t\t\t.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())\n\t\t\t\t.build();\n        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();\n        return chain.filter(newExchange);\n\t}\n}\n```\n此过滤器会为 Request 请求头追加 `Same-Token` 参数，这个参数会被转发到子服务 \n\n\n##### 3、在子服务里校验参数 \n\n在子服务添加过滤器校验参数 \n``` java\n/**\n * Sa-Token 权限认证 配置类 \n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t// 注册 Sa-Token 全局过滤器 \n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t.addInclude(\"/**\")\n        \t\t.addExclude(\"/favicon.ico\")\n        \t\t.setAuth(obj -> {\n        \t\t\t// 校验 Same-Token 身份凭证 \t—— 以下两句代码可简化为：SaSameUtil.checkCurrentRequestToken(); \n        \t\t\tString token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);\n        \t\t\tSaSameUtil.checkToken(token);\n        \t\t})\n        \t\t.setError(e -> {\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t;\n    }\n}\n```\n\n启动网关与子服务，访问测试：\n\n> [!WARNING| label:越过网关访问] \n> 如果通过网关转发，可以正常访问。如果直接访问子服务会提示：`无效Same-Token：xxx`\n\n\n### 三、服务间内部调用鉴权 \n\n有时候我们需要在一个服务调用另一个服务的接口，这也是需要添加`Same-Token`作为身份凭证的\n\n在服务里添加 Same-Token 流程与网关类似，我们以RPC框架 `Feign` 为例：\n\n##### 1、首先在调用方添加 FeignInterceptor\n``` java\n/**\n * feign拦截器, 在feign请求发出之前，加入一些操作 \n */\n@Component\npublic class FeignInterceptor implements RequestInterceptor {\n\t// 为 Feign 的 RPC 调用 添加请求头Same-Token \n\t@Override\n\tpublic void apply(RequestTemplate requestTemplate) {\n\t\trequestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());\n\t\t\n\t\t// 如果希望被调用方有会话状态，此处就还需要将 satoken 添加到请求头中\n\t\t// requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue());\n\t}\n}\n```\n\n##### 2、在调用接口里使用此 Interceptor \n``` java\n/**\n * 服务调用 \n */\n@FeignClient(\n\t\tname = \"sp-home\", \t\t\t\t// 服务名称 \n\t\tconfiguration = FeignInterceptor.class,\t\t// 请求拦截器 （⚠️ 关键代码）\n\t\tfallbackFactory = SpCfgInterfaceFallback.class\t// 服务降级处理 \n\t\t)\t\npublic interface SpCfgInterface {\n\n\t// 获取server端指定配置信息 \n\t@RequestMapping(\"/SpConfig/getConfig\")\n\tpublic String getConfig(@RequestParam(\"key\")String key);\n\t\n}\n```\n\n被调用方的代码无需更改（按照网关转发鉴权处的代码注册全局过滤器），保持启动测试即可 \n\n\n### 四、Same-Token 模块详解 \n\nSame-Token —— 专门解决同源系统互相调用时的身份认证校验，它的作用不仅局限于微服务调用场景\n\n基本使用流程为：服务调用方获取 Same-Token，提交到请求中，被调用方取出 Same-Token 进行校验：如果一致则校验通过，否则拒绝服务。\n\n<img class=\"w-100\" src=\"/big-file/doc/micro/micro-same-token.svg\" alt=\"Same-Token_同源系统认证.svg\" />\n\n\n\n\n首先我们预览一下此模块的相关API：\n``` java\n// 获取当前Same-Token\nSaSameUtil.getToken();\n\n// 判断一个Same-Token是否有效\nSaSameUtil.isValid(token);\n\n// 校验一个Same-Token是否有效 (如果无效则抛出异常)\nSaSameUtil.checkToken(token);\n\n// 校验当前Request提供的Same-Token是否有效 (如果无效则抛出异常)\nSaSameUtil.checkCurrentRequestToken();\n\n// 刷新一次Same-Token (注意集群环境中不要多个服务重复调用) \nSaSameUtil.refreshToken();\n\n// 在 Request 上储存 Same-Token 时建议使用的key\nSaSameUtil.SAME_TOKEN;\n```\n\n##### 1、疑问：这个Token保存在什么地方？有没有泄露的风险？Token为永久有效还是临时有效？\nSame-Token 默认随 Sa-Token 数据一起保存在Redis中，理论上不会存在泄露的风险，每个Token默认有效期只有一天\n\n##### 2、如何主动刷新Same-Token，例如：五分钟、两小时刷新一次？\nSame-Token 刷新间隔越短，其安全性越高，每个Token的默认有效期为一天，在一天后再次获取会自动产生一个新的Token\n\n> [!WARNING| label:注意点] \n> 需要注意的一点是：Same-Token默认的自刷新机制，并不能做到高并发可用，多个服务一起触发Token刷新可能会造成毫秒级的短暂服务失效，其只能适用于 项目开发阶段 或 低并发业务场景 \n\n因此在微服务架构下，我们需要有专门的机制主动刷新Same-Token，保证其高可用\n\n例如，我们可以专门起一个服务，使用定时任务来刷新Same-Token \n``` java\n/**\n * Same-Token，定时刷新\n */\n@Configuration\npublic class SaSameTokenRefreshTask {\n\t// 从 0 分钟开始 每隔 5 分钟执行一次 Same-Token  \n\t@Scheduled(cron = \"0 0/5 * * * ? \")\n\tpublic void refreshToken(){\n\t\tSaSameUtil.refreshToken();\n\t}\n}\n```\n\n以上的cron表达式刷新间隔可以配置为`五分钟`、`十分钟` 或 `两小时`，只要低于Same-Token的有效期（默认为一天）即可。\n\n##### 3、如果网关携带token转发的请求在落到子服务的节点上时，恰好刷新了token，导致鉴权未通过怎么办？\nSame-Token 模块在每次刷新 Token 时，旧 Token 会被作为次级 Token 存储起来，\n只要网关携带的 Token 符合新旧 Token 其一即可通过认证，直至下一次刷新，新 Token 再次作为次级 Token 将此替换掉。\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/more/blog.md",
    "content": "# 框架博客\n\n> 此页面收集 Sa-Token 相关技术文章（不限平台，按照发表日期倒序），\n> 如果你也想投稿，请考虑加入：[Sa-Token 内容合作群 ](/more/content-cooperation)\n\n--- \n\n- [[ 公众号 ] Kaleido-AI教程（九）基于Sa-Token实现多账户认证体系 ](https://mp.weixin.qq.com/s/ihW1sM8DvQJS-1ZKhFr9KQ) （2026-3-8）\n\n- [[ 公众号 ] Sa-Token 的 token-prefix 和 token-style，到底谁管谁？ ](https://mp.weixin.qq.com/s/1_PaPxvEui-16Is6Urw3CA) （2026-3-7）\n\n- [[ 公众号 ] JAVA：Spring Boot3 集成 Sa-Token 轻量级权限认证 ](https://mp.weixin.qq.com/s/cjk9ad9tj397Bd0hAyCsyA) （2026-3-6）\n\n- [[ 公众号 ] Sa-Token(二)之从入门到实战——一篇文章助你真正了解掌握Sa-Token ](https://mp.weixin.qq.com/s/9-CLoSJZBrfTF2tul-M8Ww) （2026-3-6）\n\n- [[ 公众号 ] 不用 Cookie，鉴权照样稳 ](https://mp.weixin.qq.com/s/k8DC-GiYYPbofGDD_4jlPQ) （2026-3-6）\n\n- [[ CSDN ] Sa-Token登录策略全解析：从单地登录到同端互斥，这些配置项你都知道吗？ ](https://blog.csdn.net/weixin_29284885/article/details/158675177) （2026-3-5）\n\n- [[ 公众号 ] 18.6k vs 9.5k Star！若依认证该选谁？ ](https://mp.weixin.qq.com/s/BVFWWPiYloa1nuZ4MZ8XZg) （2026-3-4）\n\n- [[ 公众号 ] SaToken 支持使用 JSON body验签 ](https://mp.weixin.qq.com/s/0Rr9PuDBUJwaEolhtJxBwA) （2026-3-3）\n\n- [[ 公众号 ] 明明接了 Redis，重启后会话还是丢了？ ](https://mp.weixin.qq.com/s/-O1qwR0I30wngurGo-qOuw) （2026-3-3）\n\n- [[ CSDN ] JAVA：Spring Boot3 集成 Sa-Token 轻量级权限认证](https://shdxhl.blog.csdn.net/article/details/157695326) （2026-2-27）\n\n- [[ 公众号 ] 苦 Spring Security 久矣？这款霸榜 Gitee 的权限框架，把优雅做到了极致](https://mp.weixin.qq.com/s/CzBPkeV6jWZ7mpA_6JH09g) （2026-2-27）\n\n- [[ 公众号 ] 架构师推荐开源项目：轻量级Java权限认证框架！](https://mp.weixin.qq.com/s/OZtTqYIZNU2l_yyKFvbd9A) （2026-2-24）\n\n- [[ 公众号 ] Sa-Token(一)之简介及入门：告别鉴权内耗，让每一位Java开发者都能轻松上手](https://mp.weixin.qq.com/s/HLG1PHnbfOTpC3e-tGutew) （2026-2-24）\n\n- [[ CSDN ] Sa-Token SSO 前后端分离实战：SpringBoot + Vue2 单点登录全流程解析](https://blog.csdn.net/weixin_29291863/article/details/158324193) （2026-2-24）\n\n- [[ CSDN ] RefreshToken反查踩坑记：Sa-Token 1.42.0临时令牌管理新姿势](https://blog.csdn.net/weixin_28327051/article/details/158301601) （2026-2-23）\n\n- [[ 公众号 ] SpringBoot3 + Sa-Token 单点登录｜30分钟上手，代码可复制，新手零踩坑](https://mp.weixin.qq.com/s/2jN9HotfYttLFMzHE54XiA) （2026-2-21）\n\n- [[ 公众号 ] Sa-Token Session会话：三种模型彻底搞懂，不再傻傻分不清](https://mp.weixin.qq.com/s/gpqogF0QyahuqJIqhs6DUg) （2026-2-21）\n\n- [[ 公众号 ] SpringBoot3 + Sa-Token 双Token登录认证实战（避坑版）](https://mp.weixin.qq.com/s/LDSCSZYuUIkQA91MYXhnGQ) （2026-2-20）\n\n- [[ CSDN ] SaToken实战：5分钟搞定微信小程序登录功能（附完整代码）](https://blog.csdn.net/weixin_29218509/article/details/158226233) （2026-2-20）\n\n- [[ 公众号 ] 告别SpringSecurity！Sa-Token+Gateway+Nacos极简鉴权实战](https://mp.weixin.qq.com/s/lFcH7XyLRtaNH6q4-TzHbQ) （2026-2-19）\n\n- [[ 公众号 ] SpringSecurity、Shiro和Sa-Token，哪个更好？](https://mp.weixin.qq.com/s/BEvk1ohFntL7iorDEvqIpg) （2026-2-18）\n\n- [[ CSDN ] Sa-Token 1.42.0实战：5分钟搞定API Key权限隔离与TOTP双因子认证](https://blog.csdn.net/weixin_29271053/article/details/158175327) （2026-2-18）\n\n- [[ CSDN ] SaToken权限注解全解析：@SaCheckPermission和@SaCheckRole的20种实战用法](https://blog.csdn.net/weixin_28454475/article/details/158163447) （2026-2-18）\n\n- [[ 公众号 ] 5 分钟上手 Sa-Token：Spring Boot 权限认证从未如此简单](https://mp.weixin.qq.com/s/2iRMPQdfBEgqSgeld3crXw) （2026-2-17）\n\n- [[ 公众号 ] SpringSecurity、Shiro和Sa-Token，哪个更好？](https://mp.weixin.qq.com/s/QdR1tyIXN8GvWbhN48XTjA) （2026-2-13）\n\n- [[ 公众号 ] sa-token前后端分离集成redis与jwt基础案例](https://mp.weixin.qq.com/s/c1UYdxuRjWudGnpQa_A72w) （2026-2-11）\n\n- [[ 公众号 ] Sa-Token(一)之简介及入门：告别鉴权内耗，让每一位Java开发者都能轻松上手](https://mp.weixin.qq.com/s/JLQSMAgqK1U0vtrdtRsKlA) （2026-2-11）\n\n- [[ 公众号 ] Sa-Token 的极简设计哲学](https://mp.weixin.qq.com/s/Yr48InNXxaVkNfVRbllO7w) （2026-2-10）\n\n- [[ 公众号 ] 还在用 @PreAuthorize？聊聊我切换到 Sa-Token 路由拦截后的真实体感](https://mp.weixin.qq.com/s/VMtSZDC1AFquCKRakTxytw) （2026-2-10）\n\n- [[ 公众号 ] Sa-Token 实战进阶：从“能用”到“好用”的企业级鉴权方案](https://mp.weixin.qq.com/s/hOY37lIxw01aPvjmdTf6VQ) （2026-2-9）\n\n- [[ 公众号 ] Sa-Token：把“登录/鉴权/踢人/SSO/OAuth2”做成一套顺手的 Java 权限方案（附 Spring Boot 快速上手）](https://mp.weixin.qq.com/s/zT8iRNuFfOEqDZfhVFy15w) （2026-2-9）\n\n- [[ 公众号 ] 开源、免费、一站式 java 权限认证框架，让鉴权变得简单、优雅！](https://mp.weixin.qq.com/s/FLDwIXHQoa6V2nKPs1r4cw) （2026-2-9）\n\n- [[ 公众号 ] Sa-Token 注解鉴权](https://mp.weixin.qq.com/s/72oLlgj-x8oetUpIJhR02A) （2026-2-9）\n\n- [[ 公众号 ] 用户投诉账号异常登录，CTO 让我 5 分钟内解决](https://mp.weixin.qq.com/s/pfMSZLxmDKIVYoq6UGUj1Q) （2026-2-4）\n\n- [[ 公众号 ] Sa-Token实战：SpringBoot与微服务权限认证极简方案](https://mp.weixin.qq.com/s/yNma6FhHvPLNHUqYFkdPCg) （2026-1-25）\n\n- [[ 公众号 ] Sa-Token过期机制](https://mp.weixin.qq.com/s/gFQ8YJT1yg5pTm8Z3uDZHw) （2026-1-22）\n\n- [[ 公众号 ] 别再写死权限了！SpringBoot + Sa-Token 实现 RBAC 的最佳姿势](https://mp.weixin.qq.com/s/ZwzAInOoqiQ2h0ogWaOoQg) （2026-1-14）\n\n- [[ 公众号 ] 集成sa-token跨域正确姿势](https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g) （2026-1-9）\n\n- [[ 公众号 ] 后端开发必看：最简单的 Java 登录认证框架 Sa-Token 上手指南](https://mp.weixin.qq.com/s/Kk9HEVAG43-FPikiMZKWJw) （2026-1-8）\n\n- [[ 公众号 ] Sa-Token：一站式权限认证解决方案的实战指南](https://mp.weixin.qq.com/s/FVkn-3CqWT8dNM6a5kD2oA) （2025-12-30）\n\n- [[ 公众号 ] SpringSecurity、Shiro和Sa-Token，哪个更好？](https://mp.weixin.qq.com/s/gtQ7_n9cPJd2-i_Qm-jk1A) （2025-12-28）\n\n- [[ 公众号 ] SpringBoot + JWT + Sa-Token：认证鉴权双框架对比，安全登录与权限控制最佳实践](https://mp.weixin.qq.com/s/SDPKdmxtwb4MbOHfg-bF8Q) （2025-12-27）\n\n- [[ 公众号 ] 一行代码搞定认证](https://mp.weixin.qq.com/s/UaAw1WdVtumA44SxjFW3yg) （2025-12-24）\n\n- [[ 掘金 ] Netty + Sa-Token 实现 WebSocket 握手认证](https://juejin.cn/post/7585490245006950406) （2025-12-21）\n\n- [[ 公众号 ] 《第27节》SpringBoot+SaToken实现鉴权功能](https://mp.weixin.qq.com/s/mY_jrQL1dG2yis51rX2i9w) （2025-12-16）\n\n- [[ 公众号 ] 告别 Spring Security！Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/s36bdkhi5ACGN2j7hLiD7w) （2025-12-15）\n\n- [[ 公众号 ] 《第26节》SpringBoot3+SaToken实现用户注册登录功能](https://mp.weixin.qq.com/s/u7nVa0PJcFWx-9xKq1RNhA) （2025-12-13）\n\n- [[ 公众号 ] 《第25节》SpringBoot3之集成sa-token权限认证框架](https://mp.weixin.qq.com/s/sxgzLqiKCf4_fqWAxN8Ozg) （2025-12-12）\n\n- [[ 掘金 ] sa-token前后端分离集成redis与jwt基础案例](https://juejin.cn/post/7576843726011645978) （2025-11-26）\n\n- [[ 公众号 ] Sa-Token 1.44.0：Java权限认证的“轻量级王者”，让鉴权优雅如诗](https://mp.weixin.qq.com/s/UprusTkp9LZOH9TJDTKJRw) （2025-11-20）\n\n- [[ 公众号 ] sa-token-rust 项目：高性能的 Rust 认证授权框架](https://mp.weixin.qq.com/s/jlQAX1K1M64DtUgrHrDX1A) （2025-11-17）\n\n- [[ 公众号 ] 不会吧，居然还有人没有用过？全网爆火的权限校验框架 Sa-Token 超详细教程它来了](https://mp.weixin.qq.com/s/kNYq0MmlYB_0tRWI-HvU1g) （2025-11-6）\n\n- [[ 公众号 ] 若依框架集成 Sa-Token 实现权限认证与会话管理](https://mp.weixin.qq.com/s/JAgL0hxcPeP0E4OW4oy8Yg) （2025-10-21）\n\n- [[ 公众号 ] 太强了！Sa-Token 的 Go 版本！](https://mp.weixin.qq.com/s/idfrMeAMY2CeGAZGY9csmw) （2025-10-20）\n\n- [[ 公众号 ] 太强了！Sa-Token 的 rust 版本！](https://mp.weixin.qq.com/s/CveVq368Dz5Xw-a2nT3YDw) （2025-10-12）\n\n- [[ 公众号 ] 功能最全的Java权限认证框架](https://mp.weixin.qq.com/s/fO5Mm1UIN8oDOwvbq-sQTw) （2025-10-10）\n\n- [[ 公众号 ] 从 0 到 1！Sa-Token 与 SpringBoot 整合教程，让鉴权优雅到飞起](https://mp.weixin.qq.com/s/PudodYBsIQODdfeweo39fQ) （2025-10-16）\n\n- [[ 公众号 ] 一篇搞定！SpringBoot 搭建超安全 Sa-Token 登录鉴权系统](https://mp.weixin.qq.com/s/5fbrNS6jMpuViPPO8z_kMw) （2025-10-3）\n\n- [[ 公众号 ] 《Spring Cloud Gateway 从入门到实战》第4篇：安全与认证 —— 基于 Sa-Token 的网关统一鉴权方案](https://mp.weixin.qq.com/s/qeWQufIDNtGPyJ0b7yF4vQ) （2025-9-29）\n\n- [[ 公众号 ] SpringBoot整合Sa-Token实现认证与鉴权](https://mp.weixin.qq.com/s/l4OjqdeNpXjMyFCSr3PuWw) （2025-9-25）\n\n- [[ 公众号 ] Spring Gateway、Sa-Token、Nacos 认证/鉴权方案](https://mp.weixin.qq.com/s/JpXtI75eANwkRAppQZJG9Q) （2025-9-17）\n\n- [[ 公众号 ] 告别 Spring Security！Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/hlBH1H6vX-KIlQGmeY8bIg) （2025-9-15）\n\n- [[ 公众号 ] Spring Gateway、Sa-Token、Nacos 认证/鉴权方案，yyds！](https://mp.weixin.qq.com/s/OYsxjEmLfkxH0NqZidb4fA) （2025-9-10）\n\n- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析：1.4 Sa-Token高级特性实现](https://mp.weixin.qq.com/s/0Fex83DyngC-mxIu346X1Q) （2025-8-30）\n\n- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析：1.3 权限控制与注解使用](https://mp.weixin.qq.com/s/f4iVDeIAZ-nixqR6BNtUfg) （2025-8-30）\n\n- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析：1.2 登录认证机制详解](https://mp.weixin.qq.com/s/gu5kT93WjEauut7xoXmuag) （2025-8-29）\n\n- [[ 公众号 ] Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析：1.1 Sa-Token框架基础](https://mp.weixin.qq.com/s/w8c5fvaap7ipMu2OdXNZng) （2025-8-29）\n\n- [[ 公众号 ] 告别 Spring Security！Sa-Token + Gateway + Nacos 极简鉴权实战](https://mp.weixin.qq.com/s/5nmEDAsFgEWk-Ymn_pE74w) （2025-8-22）\n\n- [[ 公众号 ] 搭建基于sa-token 的网关权限管理系统](https://mp.weixin.qq.com/s/LM6g3QaklSHpaVJXUz5Gvg) （2025-7-19）\n\n- [[ 公众号 ] 别再被 Spring Security 和 Shiro 劝退了！这款国产 Java 权限框架真香！](https://mp.weixin.qq.com/s/2C0WSlM8zpjqQDtgv59Kaw) （2025-7-1）\n\n- [[ 公众号 ] 一文精通Java集成Sa-Token实现SSO单点登录](https://mp.weixin.qq.com/s/-fex5XFm4wmTmuzZCtUiRw) （2025-5-22）\n\n- [[ 公众号 ] 47.8k star，一款接私活神器，10分钟搞定企业级鉴权！](https://mp.weixin.qq.com/s/KRz3-h6etPaKOwHN4Xz7qg) （2025-5-15）\n\n- [[ 公众号 ] Sa-Token：17.5k Star！轻量级Java权限认证框架，登录鉴权超简单](https://mp.weixin.qq.com/s/akPTgU8sQkwWmXm4GNSyyQ) （2025-5-11）\n\n- [[ 公众号 ] SaToken-微服务认证与授权](https://mp.weixin.qq.com/s/9WYg6iLDST7YDSSsCt1NxQ) （2025-4-3）\n\n- [[ 公众号 ] SpringBoot 整合 Sa-Token 快速实现 API 接口签名安全校验](https://mp.weixin.qq.com/s/2NS3axN1CbRYULHrv5Du2w) （2025-3-20）\n\n- [[ 公众号 ] SaToken 简化开发的身份认证与权限管理框架](https://mp.weixin.qq.com/s/Yxsswl4Zn8244j4XeV8_VA) （2025-1-24）\n\n- [[ 公众号 ] 使用 Sa-Token 平替 Spring Security，告别繁琐的认证与鉴权！](https://mp.weixin.qq.com/s/whKQPm09ApzkVBjlSwO2Tg) （2025-1-15）\n\n- [[ 公众号 ] sa-token之@SaIgnore注解失效的真正原因及正确姿势](https://mp.weixin.qq.com/s/c6eckHp2M4oz2x3Hea6pGg) （2025-1-14）\n\n- [[ 公众号 ] SpringBoot3.x+Vue3+Sa-Token实现登录认证](https://mp.weixin.qq.com/s/0GkDoOYW8KKxTfzV83J6UA) （2024-12-27）\n\n- [[ 公众号 ] 万字雄文：一次说清基于Sa-Token和MaxKey的统一认证中心实现](https://mp.weixin.qq.com/s/Gl2K47F9I6-Il-AieWLplw) （2024-11-15）\n\n<!-- 2026-2-3 搜集至 2024-11-15 公众号平台 -->\n\n\n- [[ 公众号 ] 集成sa-token前后端分离部署配置corsFliter解决跨域失效的真正原因](https://mp.weixin.qq.com/s/bSS4vmKlKM7ov_CUkjxkBg) （2024-07-08）\n\n- [[ 公众号 ] sa-token前后端分离解决跨域的正确姿势](https://mp.weixin.qq.com/s/96WbWL28T5_-xzyCfJ7Stg) （2024-07-06）\n\n- [[ 公众号 ] 集成sa-token实现登录和RBAC权限控制](https://mp.weixin.qq.com/s/SREjXoyL9s1JfddQnU38yA) （2024-04-16）\n\n- [[ CSDN ] springboot整合Sa-Token实现登录认证和权限校验（万字长文）](https://blog.csdn.net/2301_78646673/article/details/136008153) （2024-03-31）\n\n- [[ CSDN ]【Sa-Token】9、Sa-Token实现在线用户管理功能](https://blog.csdn.net/qq_40065776/article/details/132180932) （2023-11-01）\n\n- [[ CSDN ]【Sa-Token】9、Sa-Token实现在线用户管理功能](https://blog.csdn.net/qq_40065776/article/details/132180932) （2023-11-01）\n\n- [[ CSDN ] 【Sa-Token】9、Sa-Token实现在线用户管理功能](https://blog.csdn.net/qq_40065776/article/details/132180932) （2023-08-09）\n\n- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 31 - Sa-Token（五）登录验证拦截器之 Token 有效期及其续签（Sa-Token 源码）](https://blog.csdn.net/Michelle_Zhong/article/details/126071871) （2022-07-30）\n\n- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 29 - Sa-Token（四）V1.30.0 登录流程分析（Sa-Token 源码）](https://blog.csdn.net/Michelle_Zhong/article/details/125659797) （2022-07-07）\n\n- [[ 掘金 ] sa-token过期后WebSocket提示过期](https://juejin.cn/post/7103446095987998733) （2022-5-30）\n\n- [[ 掘金 ] Sa-Token 单点登录 SSO模式二 URL重定向传播会话示例](https://juejin.cn/post/7102733249088077854) （2022-5-28）\n\n- [[ 掘金 ] SaToken技术分享](https://juejin.cn/post/7097967875670933535) （2022-5-15）\n\n- [[今日头条] SpringCloud Gateway配置Nacos服务发现，Sa-Token实现接口授权](https://www.toutiao.com/article/7089584645368578567/) （2022-04-24）\n\n- [[ CSDN ] 使用sa-token 进行权限控制](https://blog.csdn.net/u012389318/article/details/124098705) （2022-4-13）\n\n- [[ 掘金 ] SpringMVC配置sa-Token](https://juejin.cn/post/7081471627766005790) （2022-4-1）\n\n- [[ CSDN ] 【SpringBoot】59、SpringBoot使用Sa-Token-Quick-Login插件快速登录认证](https://lizhou.blog.csdn.net/article/details/123571910) （2022-03-30）\n\n- [[ CSDN ] 【Sa-Token】1、Sa-Token实现登录功能](https://lizhou.blog.csdn.net/article/details/119301185) （2022-03-30）\n\n- [[ 掘金 ] 【SpringCloud-Alibaba系列教程】13.gateway网关结合Sa-token进行登录鉴权](https://juejin.cn/post/7070805258296885285) （2022-3-3）\n\n- [[ CSDN ] Sa-Token的Token有效期和临时有效期的区别](https://blog.csdn.net/ControlDemo/article/details/123177825) （2022-02-28）\n\n- [[ 掘金 ] Spring Cloud Gateway 集成Sa-Token](https://juejin.cn/post/7069748160087719967) （2022-2-28）\n\n- [[ 掘金 ] Java轻量级权限认证框架 Sa-Token 初体验](https://juejin.cn/post/7068105371839102983) （2022-2-24）\n\n- [[ CSDN ] Sa-Token获取当前所有可用Token](https://blog.csdn.net/ControlDemo/article/details/122940634) （2022-02-15）\n\n- [[ 掘金 ] 使用 Sa-Token 解决 WebSocket 握手身份认证](https://juejin.cn/post/7064232762664255525) （2022-2-14）\n\n- [[ CSDN ] sa-token配置路由拦截放行Swagger路径](https://blog.csdn.net/ControlDemo/article/details/122885782) （2022-02-11）\n\n- [[ CSDN ] sa-token 多端登录思路和遇到的坑](https://blog.csdn.net/ControlDemo/article/details/122428512) （2022-1-28）\n\n- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 13 - Sa-Token（三）退出登录流程（Sa-Token 源码）](https://blog.csdn.net/Michelle_Zhong/article/details/122691698) （2022-01-25）\n\n- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 12 - Sa-Token（二）通过注解校验用户权限（Sa-Token 源码）](https://blog.csdn.net/Michelle_Zhong/article/details/122526722) （2022-01-16）\n\n- [[ CSDN ] 【RuoYi-Vue-Plus】学习笔记 11 - 集成 Sa-Token 实现登录认证流程（Sa-Token 源码）](https://blog.csdn.net/Michelle_Zhong/article/details/122480703) （2022-01-13）\n\n- [[ 掘金 ] Springboot插件集成(三)-权限认证插件sa-token](https://juejin.cn/post/7051872914458542093) （2022-1-11）\n\n- [[ CSDN ] Sa-token简单介绍和基本使用](https://blog.csdn.net/weixin_43967582/article/details/122075950) （2021-12-21）\n\n- [[ CSDN ] sa-token使用（源码解析 + 万字）](https://blog.csdn.net/weixin_39570751/article/details/121291274) （2021-11-12）\n\n- [[ 公众号 ] 还在用Spring Security？推荐你一款使用简单、功能强大的权限认证框架](https://mp.weixin.qq.com/s/L2KOgwJcXCxrSAV8bPJsJQ) （2021-10-8）\n\n- [[ 公众号 ] Spring Security太复杂？试试这个轻量、强大、优雅的权限认证框架！](https://mp.weixin.qq.com/s/BWziNxRZH29F2v4Tmb5meA) （2021-09-22）\n\n- [[ 博客园 ] Sa-Token之注解鉴权：优雅的将鉴权与业务代码分离！](https://www.cnblogs.com/shengzhang/p/15260818.html) （2021-9-13）\n\n- [[ 掘金 ] 开箱即用！看看人家的微服务权限解决方案，那叫一个优雅！](https://juejin.cn/post/7003141949259513887) （2021-9-2）\n\n- [[ 掘金 ] 再见Spring Security！推荐一款功能强大的Java权限认证框架，用起来够优雅！](https://juejin.cn/post/7000174417846222878) （2021-8-25）\n\n- [[ 掘金 ] 史上功能最全的 Java 权限认证框架！](https://juejin.cn/post/6986174013647093773) （2021-7-18）\n\n- [[ 知乎 ] 一个项目搞定Java权限认证框架，二十多个特性开箱即用](https://zhuanlan.zhihu.com/p/390030149) （2021-7-15）\n\n- [[ 掘金 ] 从零搭建开发脚手架 集成认证授权 sa-token（尝鲜）](https://juejin.cn/post/6950163768533843999) （2021-4-12）\n\n- [[ 掘金 ] 权限认证就它了Sa-Token](https://juejin.cn/post/6938747514837434376) （2021-3-12）\n\n- [[ 掘金 ] sa-token之前后端分离模式下如何完成权限认证](https://juejin.cn/post/6937219472507797535) （2021-03-8）\n\n- [[ 掘金 ] 一个登录功能也能玩出这么多花样？sa-token带你轻松搞定多地登录、单地登录、同端互斥登录](https://juejin.cn/post/6917884159491276808) （2021-1-15）\n\n- [[ 掘金 ] sa-token v1.9.0 版本已发布，带来激动人心新特性：同端互斥登录](https://juejin.cn/post/6914612737020526599) （2021-1-6）\n\n- [[ 掘金 ] Spring Boot 系列教程 | 第一百一篇：SpringBoot整合sa-token权限框架](https://juejin.cn/post/6875525673897869319) （2020-9-23）\n\n"
  },
  {
    "path": "sa-token-doc/more/common-action.md",
    "content": "# 全局类、方法\n本篇介绍 Sa-Token 中一些常用的全局对象、类\n\n--- \n\n### SaManager\nSaManager 负责管理 Sa-Token 所有全局组件。\n``` java\nSaManager.getConfig();                 // 获取全局配置对象 \nSaManager.getSaTokenDao();             // 获取数据持久化对象 \nSaManager.getStpInterface();           // 获取权限认证对象 \nSaManager.getSaTokenContext();         // 获取SaTokenContext上下文处理对象\nSaManager.getSaTokenListener();        // 获取侦听器对象 \nSaManager.getSaTemp();                 // 获取临时令牌认证模块对象 \nSaManager.getSaJsonTemplate();         // 获取 JSON 转换器 Bean\nSaManager.getSaSignTemplate();         // 获取参数签名 Bean \nSaManager.getStpLogic(\"type\");         // 获取指定账号类型的StpLogic对象，获取不到时自动创建并返回 \nSaManager.getStpLogic(\"type\", false);  // 获取指定账号类型的StpLogic对象，获取不到时抛出异常 \nSaManager.putStpLogic(stpLogic);       // 向全局集合中 put 一个 StpLogic \n```\n\n\n### SaHolder\nSa-Token上下文持有类，通过此类快速获取当前环境的相关对象 \n``` java\nSaHolder.getContext();           // 获取当前请求的 SaTokenContext\nSaHolder.getRequest();           // 获取当前请求的 [Request] 对象 \nSaHolder.getResponse();          // 获取当前请求的 [Response] 对象 \nSaHolder.getStorage();           // 获取当前请求的 [Storage] 对象\nSaHolder.getApplication();       // 获取全局 SaApplication 对象\n```\n\n\n### SaRouter\n路由匹配工具类，详细戳：[路由拦截式鉴权](/use/route-check)\n\n\n### SaFoxUtil\nSa-Token内部工具类，包含一些工具方法 \n``` java\nSaFoxUtil.printSaToken();           // 打印 Sa-Token 版本字符画\nSaFoxUtil.getRandomString(8);       // 生成指定长度的随机字符串\nSaFoxUtil.isEmpty(str);             // 指定字符串是否为null或者空字符串\nSaFoxUtil.isNotEmpty(str);          // 指定字符串是否不是null或者空字符串\nSaFoxUtil.equals(a, b);             // 比较两个对象是否相等 \nSaFoxUtil.getMarking28();           // 以当前时间戳和随机int数字拼接一个随机字符串\nSaFoxUtil.formatDate(date);         // 将日期格式化为yyyy-MM-dd HH:mm:ss字符串\nSaFoxUtil.searchList(dataList, prefix, keyword, start, size, sortType);             // 从集合里查询数据\nSaFoxUtil.searchList(dataList, start, size, sortType);       // 从集合里查询数据\nSaFoxUtil.vagueMatch(patt, str);    // 字符串模糊匹配\nSaFoxUtil.getValueByType(obj, cs);    // 将指定值转化为指定类型\nSaFoxUtil.joinParam(url, parameStr);    // 在url上拼接上kv参数并返回 \nSaFoxUtil.joinParam(url, key, value);    // 在url上拼接上kv参数并返回 \nSaFoxUtil.joinSharpParam(url, parameStr);    // 在url上拼接锚参数 \nSaFoxUtil.joinSharpParam(url, key, value);    // 在url上拼接锚参数 \nSaFoxUtil.arrayJoin(arr);    // 将数组的所有元素使用逗号拼接在一起\nSaFoxUtil.isUrl(str);    // 使用正则表达式判断一个字符串是否为URL\nSaFoxUtil.encodeUrl(str);    // URL编码 \nSaFoxUtil.decoderUrl(str);    // URL解码 \nSaFoxUtil.convertStringToList(str);    // 将指定字符串按照逗号分隔符转化为字符串集合 \nSaFoxUtil.convertListToString(list);    // 将指定集合按照逗号连接成一个字符串 \nSaFoxUtil.convertStringToArray(str);    // String 转 Array，按照逗号切割 \nSaFoxUtil.convertArrayToString(arr);    // Array 转 String，按照逗号切割 \nSaFoxUtil.emptyList();    // 返回一个空集合\nSaFoxUtil.toList(... strs);    // String 数组转集合 \n```\n\n\n### SaTokenConfigFactory\n配置对象工厂类，通过此类你可以方便的根据 properties 配置文件创建一个配置对象 \n\n1、首先在项目根目录，创建一个配置文件：`sa-token.properties`\n\n``` properties\n# token 名称 (同时也是 cookie 名称)\ntokenName=satoken\n# token 有效期（单位：秒） 默认30天，-1 代表永久有效\ntimeout=2592000\n# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\nactiveTimeout=-1\n# 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\nisConcurrent=true\n# 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\nisShare=false\n# token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\ntokenStyle=uuid\n# 是否输出操作日志 \nisLog=false\n```\n\n2、然后使用以下代码获取配置对象 \n``` java\n// 设置配置文件地址 \nSaTokenConfigFactory.configPath = \"sa-token.properties\";\n\n// 获取配置信息到 config 对象\nSaTokenConfig config = SaTokenConfigFactory.createConfig();\n\n// 注入到 SaManager 中\nSaManager.setConfig(config);\n```\n\n\n### SpringMVCUtil\nSpringMVC操作的工具类，位于包：`sa-token-spring-boot-starter`\n``` java\nSpringMVCUtil.getRequest();           // 获取本次请求的 request 对象 \nSpringMVCUtil.getResponse();          // 获取本次请求的 response 对象 \nSpringMVCUtil.isWeb();                // 判断当前是否处于 Web 上下文中  \n```\n\n\n### SaReactorHolder & SaReactorSyncHolder\nSa-Token集成Reactor时的 ServerWebExchange 工具类，位于包：`sa-token-reactor-spring-boot-starter`\n``` java\n// 异步方式获取 ServerWebExchange 对象 \nSaReactorHolder.getMonoExchange().map(e -> {\n\tSystem.out.println(e);\n\treturn e;\n});\n```\n\n\n"
  },
  {
    "path": "sa-token-doc/more/common-questions.md",
    "content": "# 常见问题排查\n本篇整理大家在群聊里经常提问的一些问题，如有补充，欢迎提交pr\n\n[[toc]]\n\n--- \n\n<!-- ---------------------------- 常见报错 ----------------------------- -->\n\n## 一、常见报错\n\n\n### Q：报错：SaTokenContext 上下文尚未初始化\n\n可能1：:你在 异步上下文 / 响应式上下文 里调用了 Sa-Token 的同步 API，解决方案参考：[异步 & Mock 上下文](/fun/async--mock)\n\n可能2：访问了一个不存在的路由，而且 SaInterceptor 拦截器里有鉴权代码。\n\nSpringBoot 默认会把 404 请求转发到 `/error`，如果恰好 SaInterceptor 里有鉴权代码，就会造成：\n\n写入上下文 → 进入拦截器(有上下文，可调用鉴权代码) → 发现是404 → 清除上下文 → \n将请求转发至 /error -> 再次进入拦截器(无上下文，不可调用鉴权代码) → 报错：SaTokenContext 上下文尚未初始化。\n\n解决方案：将 \"/error\" 地址排除在拦截器之外：\n\n``` java\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\t\t// 鉴权代码 ...\n\t\t}))\n\t\t.addPathPatterns(\"/**\")\n\t\t.excludePathPatterns(\"/error\");\n\t}\n}\n```\n\n\n\n\n### Q：报错：NotLoginException：xxx\n\n这个错是说明调用接口的人没有通过登录校验，请注意：**通常情况下，异常提示语已经描述清楚了没有通过校验的具体原因：**\n   \n**如果是：未能读取到有效Token**\n- 可能1：前端没有提交 Token（最好从前端f12控制台看看请求参数里有 token 吗）。\n- 可能2：前端提交了 Token，但是参数名不对。默认参数名是 `satoken`，可通过配置文件 `sa-token.token-name: satoken` 来更改。\n- 可能3：前端提交了 Token，但是你配置了框架不读取，比如说你配置了 `is-read-header=false`（关闭header读取），此时你再从 header 里提交token，框架就无法读取到。\n- 可能4：前端提交了 Token，但是 Token前缀 不对，可参考：[自定义 Token 前缀](/up/token-prefix)\n- 可能5：你的项目属于前后端分离架构，此时浏览器默认不自动提交 Cookie，参考：[前后端分离](/up/not-cookie) \n- 可能6：你使用了 Nginx 反向代理，而且配置了 自定义Token名称，而且自定义的名称还带有下划线（比如 shop_token），而且还是你的项目还是从 Header头提交Token的，此时 Nginx 默认会吞掉你的下划线参数，可参考：[nginx做转发时，带下划线的header参数丢失](https://blog.csdn.net/zfw_666666/article/details/124420828)\n- 可能7：可能是跨域了，导致前端提交不上 token，看看前端浏览器有没有跨域的报错。\n\n**如果是：Token无效：6ad93254-b286-4ec9-9997-4430b0341ca0**\n- 可能1：前端提交的 token 是乱填的，或者从别的项目拷过来的，或者多个项目一起开发时彼此的 Token 串项目了。\n- 可能2：前端提交的 token 已过期（timeout超时了）。\n- 可能3：在不集成 Redis 的情况下：颁发 token 后，项目重启了，导致 token 无效。\n- 可能4：在集成 Redis 的情况下：颁发 token 后，Redis重启了，导致 token 无效。\n- 可能5：你提交的 token 和框架读取到的 token 不一致：\n\t- 可能5.1：比如说你配置了`is-read-header=false`（关闭header读取），然后你从header提交`token-A`，而框架从Cookie里读取`token-B`，导致鉴权不通过（框架读取顺序为`body->header->cookie`）\n\t- 可能5.2：比如说你配置了`token-name=x-token`（自定义token名称），此时你从header提交：`satoken:token-A`（参数名没对上），然后框架从header里读取不到你提交的token，转而继续从Cookie读取到了`token-B`。\n- 可能6：在集成 jwt 插件的情况下：\n\t- 如果使用的是 Simple 模式：情况和不集成jwt一样。\n\t- 如果使用的是 Mixin 和 Stateless 模式：查看这个 token 颁发后是否更改了 `jwtSecretKey` 配置项。\n- 可能7：同一账号登录数量超过12个，导致最先登录的被强制注销掉，这个值可以通过 `maxLoginCount` 来配置，默认值12，-1代表不做限制。\n- 可能8：在配置了 `is-concurrent=true, is-share=true`的情况下，你和别人共同登录了同一账号，此时对方注销了登录，由于你们使用的是同一个token，导致你这边的会话也失效了。\n- 可能9：可能是多账号鉴权的关系，在多账号模式下，如果是 `StpUserUtil.login()` 颁发的token，你从 `StpUtil.checkLogin()` 进行校验，永远都是无效token，因为账号体系没对上。\n\n**如果是：Token已过期：6ad93254-b286-4ec9-9997-4430b0341ca0**\n- 可能1：前端提交的 token 已被冻结（active-timeout超时了，比如配置了 active-timeout=120，但是超过了120秒没有访问接口）。\n- 可能2：集成jwt，而且使用的是 Mixin 或 Stateless 模式，而且token过期了（timeout超时了）。\n\n**如果是：Token已被顶下线：6ad93254-b286-4ec9-9997-4430b0341ca0**\n- 可能1：在项目配置了 `is-concurrent=false` 的前提下，这个账号又被别人登录了，导致旧登录被挤了下去。\n- 可能2：这个账号被 `StpUtil.replaced(loginId, device)` 方法强制顶下线了。\n\n**如果是：Token已被踢下线：6ad93254-b286-4ec9-9997-4430b0341ca0**\n- 可能1：这个账号被 `StpUtil.kickout(loginId)` 方法强制踢下线了。\n\n\n### Q：加了注解进行鉴权认证，不生效？\n1. 注解鉴权功能默认关闭，两种方式任选其一进行打开：注册注解拦截器、集成AOP模块，参考：[注解式鉴权](/use/at-check)\n2. 在Spring环境中, 如果同时配置了`WebMvcConfigurer`和`WebMvcConfigurationSupport`时, 也会导致拦截器失效.\n   - **常见场景**: 很多项目中会在`WebMvcConfigurationSupport`中配置`addResourceHandlers`方法开放Swagger等相关静态资源映射, 同时基于Sa-Token添加了`WebMvcConfigurer`配置`addInterceptors`方法注册注解拦截器, 这样会导致注解拦截器失效. \n   - **解决方案**: `WebMvcConfigurer`和`WebMvcConfigurationSupport`只选一个配置, 建议统一通过实现`WebMvcConfigurer`接口进行配置.\n3. 如果以上步骤处理后仍然没有效果，加群说明一下复现步骤 \n\n\n\n### Q：我加了拦截器鉴权，但是好像没有什么效果，请求没有被拦截住？\n- 可能1：这个拦截器可能没有注册成功。\n- 可能2：你访问的请求没有进入这个拦截器。\n\n尝试按照下面的代码测试一下看看：\n\n``` java\n// 注册拦截器 \n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\tSystem.out.println(\"--------- flag 1\");\n\t\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\t\tSystem.out.println(\"--------- flag 2，请求进入了拦截器，访问的 path 是：\" + SaHolder.getRequest().getRequestPath());\n\t\t\tStpUtil.checkLogin();  // 登录校验，只有会话登录后才能通过这句代码 \n\t\t}))\n\t\t.addPathPatterns(\"/user/**\")\n\t\t.excludePathPatterns(\"/user/doLogin\");\n\t}\n}\n```\n\n在启动时 `flag 1` 被打印出来，才证明拦截器注册成功了，在访问请求时 `flag 2` 被打印出来，才证明请求进入了拦截器。\n\n如果拦截器没有注册成功，则：\n<!-- - 可能1：SpringBoot 版本较高（`>= 2.6.0`），请尝试在启动类加上 `@EnableWebMvc` 注解再重新启动。 -->\n- 可能1：`SaTokenConfigure` 配置类不在启动类的同包或者子包下，导致没有被 SpringBoot 扫描到。\n- 可能2：你的项目启动类上加了 `@ComponentScan(\"com.xxx\")` 注解，导致包扫描范围不正确，请将此注解删除或移动到其它配置类上。\n- 可能3：项目属于 Maven 多模块项目，`SaTokenConfigure` 和启动类没有在一个模块，且启动类模块没有引入配置类的模块，导致加载不到。\n\n如果拦截器已经注册成功，但请求没有进入拦截器：\n- 可能1：你访问的 path，没有被 `.addPathPatterns(\"/user/**\")` 拦截住，或者被 `.excludePathPatterns(\"/xxx/xx\")` 排除掉了。\n- 可能2：你访问的是另一个项目，请把当前项目停掉，看看你的请求还能不能访问成功。\n\n如果请求进入拦截器也成功了，那可能是：\n- 可能1：前端访问时提交了会话 Token，且这个 Token 是有效的，通过了拦截器的代码校验。\n- 可能2：你访问的 path，和你预期不符，仔细观察一下打印出来的 path 信息，和你的预期相符吗。\n\n注：以上的排查步骤，对过滤器不生效的情形一样适用。\n\n\n### Q：我使用拦截器鉴权时，明明排除了某个路径却仍然被拦截了？\n- 可能1：你的项目可能是跨域了，先把跨域问题解决掉，参考：[解决跨域问题](/fun/cors-filter)\n- 可能2：你访问的接口可能是404了，SpringBoot环境下如果访问接口404后，会被转发到`/error`，然后被再次拦截。请确保你访问的 path 有对应的 Controller 承接！\n- 可能3：可能拦截器这里并没有拦截，但是又被其他地方拦截了。请先把这个拦截器给注释掉，看看还会不会拦截，如果依然拦截，那说明不是这个拦截器的锅，请仔细查看一下控制台抛出的堆栈信息，定位一下到底是哪行代码拦截住这个请求的。\n- 可能4：后端拦截的 path 未必是你前端访问的这个path（特别是经过网关转发后的path可能会有变化），建议先打印一下 path 信息，看看和你预想的是否一致，再做分析。\n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\ttry {\n\t\t\tSystem.out.println(\"-------- 前端访问path：\" + SaHolder.getRequest().getRequestPath());\n\t\t\tStpUtil.checkLogin();\n\t\t\tSystem.out.println(\"-------- 此 path 校验成功：\" + SaHolder.getRequest().getRequestPath());\n\t\t} catch (Exception e) { \n\t\t\tSystem.out.println(\"-------- 此 path 校验失败：\" + SaHolder.getRequest().getRequestPath());\n\t\t\tthrow e;\n\t\t}\n\t})).addPathPatterns(\"/**\"); \n}\n```\n- 可能5：可能你只提交了一个请求，但是浏览器自动帮你提交了其它请求，举个例子：首次访问网站时，浏览器一般会自动提交 `/favicon.ico`，所以**你需要找出是哪个path被拦截了**，怎么找呢？用【可能4】的代码来测试找。\n- 可能6：你的项目配置了 `context-path` 上下文地址，比如 `server.servlet.context-path=/shop`，注意这个地址是不需要加在拦截器上的：\n``` java\n// 这是错误示例，不需要把 context-path 上下文参数写在下面的 excludePathPatterns 地址上。\nregistry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin()))\n\t\t\t.addPathPatterns(\"/**\").excludePathPatterns(\"/shop/user/login\");\n// 这是正确示例，无论你的 context-path 上下文配置了什么样的值，下面的 excludePathPatterns 地址都不需要写上它\nregistry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin()))\n\t\t\t.addPathPatterns(\"/**\").excludePathPatterns(\"/user/login\");\n```\n- 可能7：你写了多个匹配规则，请求越过了第一个规则，但又被其它规则拦下来了，例如以下代码：\n``` java\n// 以下代码，当你未登录访问 `/user/doLogin` 时，会被第1条规则越过，然后被第2条拦下，校验登录，然后抛出异常：`NotLoginException：xxx`\nregistry.addInterceptor(new SaInterceptor(handler -> {\n\tSaRouter.match(\"/**\").notMatch(\"/user/doLogin\").check(r -> StpUtil.checkLogin());  // 第1个规则 \n\tSaRouter.match(\"/**\").notMatch(\"/article/getList\").check(r -> StpUtil.checkLogin());  // 第2个规则 \n\tSaRouter.match(\"/**\").notMatch(\"/goods/getList\").check(r -> StpUtil.checkLogin());  // 第3个规则 \n})).addPathPatterns(\"/**\");\n```\n- 可能8：你自定义的封装方法，并没有按照你的预想情况执行：\n``` java\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\t// 调用自定义的 excludePaths() 方法获取数据排除鉴权  \n\t\tSaRouter.match(\"/**\").notMatch(excludePaths()).check(r -> StpUtil.checkLogin());\n\t})).addPathPatterns(\"/**\");\n}\n// 自定义查询排查鉴权的地址方法 \npublic static List<String> excludePaths() {\n\tList<String> list = ... // 从数据源查询...;\n\treturn list;\n}\n```\n如上方法， `excludePaths()` 可能并不会像你预想的一样正确执行返回相应的值，请在 `.notMatch()` 处 `一律先硬编码写固定死值来测试`，这时就有两种情况：\n\t- 情况1：写固定死值时，代码能正常执行了，那说明你自定义的 `excludePaths()` 方法有问题，执行结果不正确。\n\t- 情况2：写固定也不行，那说明不是 `excludePaths()` 的问题，那再从其它地方开始排查。\n\n\n\n### Q：我在配置文件中加了一些关于 Sa-Token 的配置，但是没有生效。\n首先，有没有生效的最佳判断方式是，在main方法中加一个打印，看看打印出来的和你配置文件的一致吗：\n\n``` java\n@SpringBootApplication\npublic class SaTokenApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenApplication.class, args); \n\t\tSystem.out.println(\"\\n启动成功：Sa-Token配置如下：\" + SaManager.getConfig());\n\t}\n}\n```\n\n如果不一致，请排查：\n- 可能1：项目中还存在代码配置，而代码配置会覆盖 `application.yml` 中配置，详细参考：[框架配置](/use/config)。\n- 可能2：你的配置文件名字错误，SpringBoot 项目正常情况下配置文件名称应该是：`application.yml` 或 `application.properties`。\n- 可能3：可能是你的配置前缀不对，或者配置缩进不对：\n``` yaml\n# 错误示例，多加了 spring 前缀\nspring:\n\tsa-token: \n\t\ttoken-name: xxx-token\n# 错误示例，缩进不对\nsa-token: \ntoken-name: xxx-token\n# 正确的应该是以 sa-token 开头\nsa-token: \n\ttoken-name: xxx-token\n```\n\n\n### Q：我自定义了组件，但是好像没有生效？\n1、可能组件没有注入成功，排查方法为在 main 里打印这个组件，是否为自定义的class限定名：\n``` java\n@SpringBootApplication\npublic class SaTokenApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenApplication.class, args); \n\t\tSystem.out.println(SaManager.getStpInterface());  // 打印全局的 StpInterface 实现类 \n\t}\n}\n```\n如果打印出的是你的自定义实现类，则证明注入成功，如果不是，则证明没有注入成功，请排查：\n- 自定义的组件实现类上是否加上了 `@Component` 注解，只有加上这个注解，组件才会被 Spring 自动实例化并注入。\n- 自定义的组件实现类是否在启动类的同目录或者子目录上，如果不在则无法被 springboot 启动时扫描，扫描不到也就无法注入。\n- 启动类上是否加了 `@ComponentScan` 注解，导致包扫描范围不正确，请将此注解删除或移动到其它配置类上。\n\n2、这个组件注入成功了，但是还没到执行时机，比如 `StpInterface` 组件，只有在鉴权时才会触发，如果你的代码仅仅是登录校验，就不会执行到这个组件。\n\n\n### Q：集成 Redis 后，明明 Redis 中有值，却还是提示无效Token？\n\n根据以往的处理经验，发生这种情况 90% 的概率是因为你找错了Redis，即：代码连接的Redis和你用管理工具看到的Redis并不是同一个。\n\n你可能会问：我看配置文件明明是同一个啊？\n\n我的回答是：别光看配置文件，不一定准确，在启动时直接执行 `SaManager.getSaTokenDao().set(\"name\", \"value\", 100000);`，\n随便写入一个值，看看能不能根据你的预期写进这个Redis，如果能的话才能证明`代码连接的Reids` 和`你用管理工具看到的Redis` 是同一个，再进行下一步排查。\n\n\n### Q：报错：无效Same-Token：xxxxxxxxxxx\n与之类似的的报错还有：\n- SSO模式二时，报错：无效ticket：xxxxxxxxxx\n- OAuth2模块跨多个项目搭建Server时：报错无效 Access-Token：xxxxxx\n- 微服务做分布式 Session 认证时，报错：无效 Token：xxxxxxxxx\n- 等等等等.... \n\n这些功能有个统一的特点，就是需要多个项目连接同一个 Redis 才能搭建成功，如果连接的不是同一个 Redis，就会导致 Token / ticket 无法互相认证。\n\n你可能会问：我看配置文件明明就是连接的同一个 Redis 啊？\n\n别急，和上一个问题一样，**不要凭借肉眼检查下定论**，在你的两个服务之间，分别使用以下代码测试一下：\n\n``` java\n@SpringBootApplication\npublic class SaTokenApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenApplication.class, args);\n\t\t// 写值测试：注意一定要用下列方法测试，不要用自己封装的 RedisUtil 之类的测试 \n\t\tSaManager.getSaTokenDao().set(\"name\", \"value\", 100000); \n\t}\n}\n```\n\n如果都能根据你的预期写进同一个 Redis，那才能证明两个服务确实连接的是同一个 Redis。\n\n实际上，在交流群中提问这些问题的同学，90%的经过以上测试以后，都会发现两者连接的不是同一个 Reids，原因大多是：Redis配置没有生效、使用了 Alone-Redis 之类的……\n\n如果你是剩下的 10%，那么继续排查：两边的 sa-token 配置是否完全一致，比如 token-name 配置不一致，也会导致数据无法相互认证。最好是把所有 sa-token 相关的配置都复制过去，试验一下看看。\n\n\n\n### Q：我把 token 有效期设置为 30 天，但是总感觉不到 30 天的时候 token 就无效了，怎么回事？\n- 可能1：你没有为 sa-token 集成 Redis，框架默认将会话数据保存在内存中，项目重启后数据会消失。\n- 可能2：你为 sa-token 集成了 Redis，但是 Redis 重启了，导致会话消失。\n- 可能3：你配置了 `is-concurrent=false`，不允许同一账号多端登录，有别人登录了这个账号把你顶下去了。\n- 可能4：你配置了 `is-concurrent=true`，但是`is-share=false`，同一账号每次登录产生不同的 token，默认最高可以同时登录12个客户端，超过将自动注销最原先的会话。\n- 可能5：你的这个账号，别人也登录了，别人调用了注销方法，把你这边的也注销了。`StpUtil.logout()` 为单 token 注销，`StpUtil.logout(10001)` 为账号所有 token 注销。\n- 可能6：你虽然 `sa-token.timeout` 配置了 30 天，但是 `sa-token.active-timeout` 配置了较短的值，超过这个时间无操作，token 就过期了。\n- 可能7：你换了浏览器，或者换了电脑，或者清空了浏览器最近缓存记录，自然而然需要重新登录。\n- 可能8：你中途改了项目配置，比如改了 `sa-token.token-name` 配置项的值，会导致会话保存的 key 发生改变，效果等同于手动清空了 Redis 数据，需要重新登录。\n\n\n\n### Q：有时候我不加 Token 也可以通过鉴权，请问是怎么回事？\n- 可能1：你访问的这个接口，根本就没有鉴权的代码，所以可以安全的访问通过。\n- 可能2：可能是 Cookie 帮你自动提交了 Token，在浏览器或 Postman 中会自动维护Cookie模式，如不需要可以在配置文件：`is-read-cookie: false`，然后重启项目再测试一下。\n\n\n### Q：一个 User 对象存进 Session 后，再取出来时报错：无法从 User 类型转换成 User 类型？\n- 可能1：你的 User 类中途换了包名，导致存进去时和取出来时对不上，无法成功创建实例。\n- 可能2：你打开了代码热刷新模式，先存进去的对象，热刷新后再取出，会报错，关闭热刷新即可解决。\n\n\n### Q：在 SaServletFilter 中调用 SpringMVCUtil.getRequest() 报错：非Web上下文无法获取Request？\n\n- 可能1：项目中有配置类继承了： `extends WebMvcConfigurationSupport`。\n- 可能2：项目中有配置类添加了注解： `@EnableWebMvc`。\n\n解决方案：不要加 `@EnableWebMvc`，不要 `extends WebMvcConfigurationSupport`，要 `implements WebMvcConfigurer`\n\n如果一定要 `extends WebMvcConfigurationSupport` ，可以通过手动注册 Spring 上下文初始化过滤器试试：\n\n``` java\n@Configuration\npublic class SaTokenConfigure extends WebMvcConfigurationSupport {\n\n\t// Spring 上下文初始化过滤器 可能由于各种原因没有被注册到，这里手动帮忙注册一下 \n\t@Bean\n\t@ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class })\n\t@ConditionalOnMissingFilterBean(RequestContextFilter.class)\n\tpublic static RequestContextFilter requestContextFilter() {\n\t\tSystem.out.println(\"--------------------------- 注册了\"); // 加个打印语句或者断点确保这里注册到了\n\t\treturn new OrderedRequestContextFilter();\n\t}\n\t\n}\n```\n\n\n如果不是以上原因，可以加群提供复现demo。\n\n<!-- 目前能复现此问题的情况是：在项目中有配置类继承 `WebMvcConfigurationSupport` 时，再从 `SaServletFilter` 中调用\n `SpringMVCUtil.getRequest()` 就会报错：`非Web上下文无法获取Request`。\n\n解决方案是将 `extends WebMvcConfigurationSupport` 改为 `implements WebMvcConfigurer`。 -->\n\n\n### Q：我配置了 active-timeout 值，但是当我每次续签时 Redis 中的 ttl 并没有更新，是不是 bug 了？\n不更新是正常现象，`active-timeout`不是根据 ttl 计算的，是根据value值计算的，value 记录的是该 Token 最后访问系统的时间戳，\n每次验签时用：当前时间 - 时间戳 > active-timeout，来判断这个 Token 是否已经超时。 \n\n\n### Q：整合 Redis 时先选择了默认jdk序列化，后又改成 jackson 序列化，程序开始报错，SerializationException？\n两者的序列化算法不一致导致的反序列化失败，如果要更改序列化方式，则需要先将 Redis 中历史数据清除，再做更新。\n\n\n### Q：调用 `StpUtil.getExtra(\"name\")` 报错：`this api is disabled`。\n`StpUtil.getExtra(key)` 是给 sa-token-jwt 插件提供的，不集成这个插件就不能调用这个API，如果是普通模式需要存储自定义参数，请在 SaSession 上存储\n\n``` java\n// 在登录时缓存参数\nStpUtil.getSession().set(\"name\", \"zhangsan\");\n\n// 然后我们就可以在任意处获取这个参数 \nString name = StpUtil.getSession().getString(\"name\");\n```\n\n\n### Q：我加了 Sa-Token 的全局过滤器，浏览器报错跨域了怎么办？\n参考：[https://juejin.cn/post/7247376558367981627](https://juejin.cn/post/7247376558367981627)\n\n\n### Q：前后端分离项目中，前端使用 vue，如果不打开 porxy 代理的话，调用 Sa-Token 登录不会将 token 自动注入到 Cookie 中，是因为跨域么？\n是。\n\n参考：[前后端分离](/up/not-cookie) \n\n\n### Q：集成redis后对象模型序列化异常\n假设执行如下代码:\n``` java\n@Data\npublic class User implements Serializable {\n    private Long userId;\n    private String username;\n    private String password;\n}\n```\n\n``` java\nUser user = new User();\nuser.setUserId(10000L);\nuser.setUsername(\"oneName\");\nuser.setPassword(\"onePass\");        \nStpUtil.getSession().set(\"userObjKey\", user); // 这里报错\n```\n报错信息如下:\n```\nSerializationException: Could not read JSON: \nCannot deserialize value of type `java.lang.Long` from Array value (token `JsonToken.START_ARRAY`)\n```\n\nSpringboot 集成 Sa-Token Redis 后, 一旦 Springboot 切换版本就有可能出现此问题\n\n原因是 Redis 里面有之前的 Sa-Token 会话数据, 清空 Redis 即可。\n\n\n\n### Q：我实现了 StpInterface 接口，但是在登录时没有进入我的实现类代码？\n不进入是正常现象， StpInterface 是鉴权接口，在执行鉴权代码时才会进入 StpInterface 实现类，登录认证时不会进入。\n\n\n### Q：启动时报错，找不到 xx 类 xx 方法：\n``` java\nCaused by: java.lang.ClassNotFoundException: cn.dev33.satoken.same.SaSameTemplate\n```\n\n一般找不到类，或者找不到方法，都是版本冲突了，使用 Sa-Token 时一定要注意**版本对齐**，意思是所有和 Sa-Token 相关的依赖都需要版本一致。\n\n比如说你如果一个依赖是 1.32.0，一个是 1.31.0，就会造成无法启动：\n\n``` xml\n<!-- 如下样例：一个是 `1.32.0`，一个是 `1.31.0`， 版本没对齐，就会造成项目无法启动 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>1.32.0</version>\n</dependency>\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-core</artifactId>\n\t<version>1.31.0</version>\n</dependency>\n```\n\n请仔细排查你的 pom.xml 文件，是否有 Sa-Token 依赖没对齐，**请不要肉眼检查，用全局搜索 \"sa-token\" 关键词来找**，如果是多模块或者微服务项目，就整个项目搜索。\n\n\n### Q：在多账号模式的注解鉴权时，报错：未能获取对应StpLogic，type=xxx\n\n报这个错说明对应 type 的 StpLogic 尚未初始化到全局 StpLogicMap 中，一般会有两种原因造成这种情况：\n1. 注解里的 loginType 拼写错误，请改正 （建议使用常量）。\n2. 自定义 StpUtil 尚未初始化（静态类中的属性至少一次调用后才会初始化），解决方法两种：\n\t- (1) 从main方法里调用一次\n\t- (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 \n\n\n### Q：使用拦截器鉴权，访问一个不存在的 path 时，springboot 会自动在控制台打印一下异常。\n可尝试添加以下配置解决：\n``` properties\nspring.web.resources.add-mappings=false\nspring.mvc.throw-exception-if-no-handler-found=true\n```\n\n\n\n\n### Q：开启了全局懒加载后，能启动项目，但是访问接口报“未能获取有效的上下文处理器”\n开启了全局懒加载后，能启动项目，但是访问接口报异常 `InvalidContextException`: 未能获取有效的上下文处理器, 配置如下：\n``` yaml\nspring:\n  main:\n    lazy-initialization: true\n```\n原因是 Sa-Token 自动配置入口类 SaBeanInject 被延迟加载了，只需要手动指定懒加载排除掉 SaBeanInject 就可以了,实现代码如下:\n``` java\n@Configuration\nclass MyConfiguration {\n    @Bean\n    LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {\n        return LazyInitializationExcludeFilter.forBeanTypes(SaBeanInject.class);\n    }\n}\n```\n[经验来源](https://gitee.com/dromara/sa-token/issues/I7EXIU)\n\n\n### Q：SpringBoot 3.x 路由拦截鉴权报错：No more pattern data allowed after {*...} or ** pattern element\n\n\n报错原因：SpringBoot3.x 版本默认将路由匹配机制由 `ant_path_matcher` 改为了 `path_pattern_parser` 模式，\n而此模式有一个规则，就是写路由匹配符的时候，不允许 `**` 之后再出现内容。例如：`/admin/**/info` 就是不允许的。\n\n如果你的项目报了这个错，说明你写的路由匹配符出现了上述问题，有三种解决方案：\n1. 等待 SpringMVC 官方增强 `path_pattern_parser` 模式能力，使之可以支持 `**` 之后再出现内容。\n2. 在写路由匹配规则时，避免使 `**` 之后再出现内容。\n3. 将项目的路由匹配机制改为 `ant_path_matcher`。\n\n步骤1：先改项目的：\n``` yml\nspring:\n    mvc:\n        pathmatch:\n            matching-strategy: ant_path_matcher\n```\n\n步骤2：再改 Sa-Token 的：\n``` java\n/**\n * 重写路由匹配算法，切换为 ant_path_matcher 模式，使之可以支持 `**` 之后再出现内容\n */\n@PostConstruct\npublic void customRouteMatcher() {\n\tSaStrategy.instance.routeMatcher = (pattern, path) -> {\n\t\treturn SaPatternsRequestConditionHolder.match(pattern, path);\n\t};\n}\n```\n\n**注意点：**\n\nSpringBoot2.x 的 `WebFlux`或 `SC Gateway` 项目，按照上述步骤改造，可能会报错 \n\n``` html\njava.lang.NoClassDefFoundError: org/springframework/web/servlet/mvc/condition/PatternsRequestCondition\n```\n\n只需要将“步骤2”中的代码 `return SaPatternsRequestConditionHolder.match(pattern, path);` \n更换为 `return SaPathMatcherHolder.getPathMatcher().match(pattern, path);` 即可，例如：\n\n``` java\n/**\n * 重写路由匹配算法，切换为 ant_path_matcher 模式，使之可以支持 `**` 之后再出现内容\n */\n@PostConstruct\npublic void customRouteMatcher() {\n\tSaStrategy.instance.routeMatcher = (pattern, path) -> {\n\t\treturn SaPathMatcherHolder.getPathMatcher().match(pattern, path);\n\t};\n}\n```\n\n\n### Q：Webflux 环境集成，或者 SpringCloud Gateway 环境集成后，过滤器里路由拦截鉴权报错：`java.lang.NoSuchFieldError: defaultInstance`\n\n``` java\njava.lang.NoSuchFieldError: defaultInstance\n\tat cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil.match(SaPathPatternParserUtil.java:40)\n\tat cn.dev33.satoken.reactor.spring.SaTokenContextForSpringReactor.matchPath(SaTokenContextForSpringReactor.java:34)\n\tat cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:58)\n\tat cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:72)\n\t... \n```\n\n原因：SpringBoot 版本用的太低了，导致一些类不存在。\n\n- 方案一：升级项目的 SpringBoot 版本至 `2.3.x` 以上\n- 方案二：像上面的问题解决方案一样，重写一下相关类：\n\n``` java\n/**\n * 重写路由匹配算法，将 PathPatternParser.defaultInstance 改为 SaPathMatcherHolder.getPathMatcher()\n */\n@PostConstruct\npublic void customRouteMatcher() {\n\tSaStrategy.instance.routeMatcher = (pattern, path) -> {\n\t\treturn SaPathMatcherHolder.getPathMatcher().match(pattern, path);\n\t};\n}\n```\n\n\n### Q：过低的 SpringBoot 版本引入 Sa-Token 后报错\n\n在低于 2.2.0 时 (不包含2.2.0本身) 的 SpringBoot 项目中引入 Sa-Token 后，项目启动时会报错：\n\n``` txt\norg.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\n```\n\n这是由于缺少 jackson 相关依赖导致的，可以手动添加以下依赖来解决：\n\n``` xml\n<!-- SpringBoot 版本过低时，需要追加的包 (低于 2.2.0 时，不包含 2.2.0 本身) -->\n<dependency>\n\t<groupId>com.fasterxml.jackson.core</groupId>\n\t<artifactId>jackson-core</artifactId>\n\t<version>2.17.3</version>\n</dependency>\n<dependency>\n\t<groupId>com.fasterxml.jackson.core</groupId>\n\t<artifactId>jackson-annotations</artifactId>\n\t<version>2.17.3</version>\n</dependency>\n<dependency>\n\t<groupId>com.fasterxml.jackson.core</groupId>\n\t<artifactId>jackson-databind</artifactId>\n\t<version>2.17.3</version>\n</dependency>\n```\n\n\n\n### Q：在 idea 导入源码，运行报错：java: 程序包cn.dev33.satoken.oauth2不存在。\n\n在项目根目录进入 cmd，执行 `mvn package`，然后重新运行试试。\n\n如果不行，先执行 `maven clean` ，然后删除 .idea 文件夹里除 `icon.png` 外的所有文件，然后执行 `mvn package`，然后重新运行试试。\n\n如果还不行，删除整个项目，重新从 git 地址拉取一遍，再运行。\n\n\n\n### Q：报错：非 web 上下文无法获取 HttpServletRequest。\n\n报错原因解析：\n\nSa-Token 的部分 API 只能在 Web 上下文中才能调用，例如：`StpUtil.getLoginId()` 获取当前用户Id，这个方法第一步需要先从前端提交的参数里获取 token 值，\n当你在 main 方法里调用这个 API 时，由于 main 方法本质上不是一个 Controller 请求，所以框架无法完成 *“从前端提交的参数里获取 token 值”* 这一步骤，框架就只能抛出异常。\n\n按照此标准，Sa-Token 的 API 可粗浅的分为两大类：\n- 必须在 Web 上下文中才能调用的 API，例如：`StpUtil.getLoginId()`、`StpUtil.getTokenValue()` 等等。\n- 无需 Web 上下文也能调用的 API，例如：`StpUtil.getLoginType()`、`SaManager.getConfig()` 等等。\n\n此处无法逐一列出到底哪些 API 属于 *“必须依赖 Web 上下文的 API”*，因为太多了，你只需要记住关键的一点：\n**当一个 API 执行的代码需要先从前端请求中获取一些数据时，这个 API 就属于 *“必须依赖 Web 上下文的 API”*。**\n\n如果你的代码报这个错，说明你在不是 Web 上下文中的地方，调用了 *“必须依赖 Web 上下文的 API”*，请排查：\n\n1. 是否在 main 方法中调用了 *“必须依赖 Web 上下文的 API”*。\n2. 是否在带有 `@Async` 注解的方法中调用了 *“必须依赖 Web 上下文的 API”*。\n3. 是否在一些丢失 web 上下文的子线程中调用了 *“必须依赖 Web 上下文的 API”*，例如 `MyBatis-Plus` 的 `insertFill` 自动填充。\n4. 是否在一些非 Http 协议的 RPC 框架中（例如 Dubbo）调用了 *“必须依赖 Web 上下文的 API”*。\n5. 是否在 SpringBoot 启动初始化的方法中调用了 *“必须依赖 Web 上下文的 API”*，例如 `@PostConstruct` 修饰的方法。\n6. 是否在定时任务中调用了 *“必须依赖 Web 上下文的 API”*。\n\n\n### Q：报错：未能获取有效的上下文处理器。\n\n报错原因解析：\n\n在 sa-token-core 核心包中，Sa-Token 底层不能确认最终运行的 web 容器，所以抽象了 `SaTokenContext` 接口，对接不同容器时需要注入不同的实现，\n通常这个注入工作都是框架自动完成的，你只需要按照文档开始部分集成相应的依赖即可。例如：\n\n- 如果你使用的 `SpringBoot 2.x`，请引入 `sa-token-spring-boot-starter`。\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n- 如果你在基于 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`）。\n- 你要在 Solon 中使用 Sa-Token，就引入：`sa-token-solon-plugin`。\n- 等等等等……\n\n如果你的代码报 *“未能获取有效的上下文处理器”* 这个错，大概率是因为你没有正确引入所需的包，导致框架没有注入正确的 `SaTokenContext` 上下文实现，请排查：\n\n1. 如果你的项目是微服务项目，请直接参考：[微服务-依赖引入说明](/micro/import-intro)，如果是单体项目，请往下看：\n2. 请判断你的项目是 SpringMVC 环境还是 WebFlux 环境：\n\t- 如果是 SpringMVC 环境就引入 `sa-token-spring-boot-starter` 依赖，参考：[在SpringBoot环境集成](/start/example)\n\t- 如果是 WebFlux 环境就引入 `sa-token-reactor-spring-boot-starter` 依赖，参考：[在WebFlux环境集成](/start/webflux-example)\n3. 如果你还无法分辨你是哪个环境，就看你的 pom.xml 依赖：\n\t- 如果引入了`spring-boot-starter-web`就是 SpringMVC 环境。\n\t- 如果引入了 `spring-boot-starter-webflux` 就是WebFlux环境。\n\t- 什么？你说你两个都引入了？那你的项目能启动成功吗？\n4. 如果是 WebFlux 环境而且正确引入了依赖，依然报错，**请检查是否注册了 SaReactorFilter 全局过滤器，在 WebFlux 下这一步是必须的**，具体还是请参考上面的 [ 在WebFlux环境集成 ] 章节。\n5. 需要仔细注意，如果你使用的是 `SpringBoot 3.x` 或 `SpringBoot 4.x`，请分别引入 `sa-token-spring-boot3-starter` 或 `sa-token-spring-boot4-starter`，不要错误引入 `sa-token-spring-boot-starter`，不然会导致框架报错。\n6. 如果你的项目开启了全局懒加载(spring.main.lazy-initialization=true)后，能启动项目，但是访问接口报异常，请直接参考：[Q：开启了全局懒加载后，能启动项目，但是访问接口报未能获取有效的上下文处理器](/more/common-questions?id=q：开启了全局懒加载后，能启动项目，但是访问接口报未能获取有效的上下文处理器)\n7. 如果以上步骤排除无误后依然报错，请直接提 issue 或者加入QQ群求助。\n\n\n\n\n\n<!-- ---------------------------- 常见疑问 ----------------------------- -->\n\n## 二、常见疑问\n\n### Q：登录方法需要我自己实现吗？\n是的，不同于`shiro`等框架，`Sa-Token`不会在登录流程中强插一脚，开发者比对完用户的账号和密码之后，只需要调用`StpUtil.login(id)`通知一下框架即可\n\n``` java\n// 会话登录接口 \n@RequestMapping(\"doLogin\")\npublic SaResult doLogin(String name, String pwd) {\n    // 第一步：比对前端提交的账号名称、密码\n    if(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\n        // 第二步：比对成功后，调用通知框架，xxx账号登录成功 \n        StpUtil.login(10001);\n        return SaResult.ok(\"登录成功\");\n    }\n    return SaResult.error(\"登录失败\");\n}\n```\n\n### Q：框架抛出的权限不足异常，我想根据自定义提示信息，可以吗？\n可以，在全局异常拦截器里捕获`NotPermissionException`，可以通过`getPermission()`获取没有通过认证的权限码，可以据此自定义返回信息\n\n``` java\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n    // 全局 NotPermissionException 异常捕获 \n    @ExceptionHandler(NotPermissionException.class)\n    public SaResult handlerException(NotPermissionException e) {\n        e.printStackTrace();\n        return SaResult.error(\"缺少权限：\" + e.getPermission());\n    }\n}\n```\n\n### Q：在 SaInterceptor 中，注解鉴权总是先于路由拦截鉴权执行，能调整一下顺序吗？\n框架没有提供直接的 API，但你有以下两种方式可以做到这一点：\n- 方式1：将 SaInterceptor 里的代码复制出来一份，按照你的需求改一下，然后使用你这个自定义的拦截器，不再使用官方的。\n- 方式2：注册两次 SaInterceptor 拦截器，例如：\n\n``` java\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 路由拦截鉴权\n\t\tregistry.addInterceptor(new SaInterceptor(r -> {\n\t\t\t// 路由拦截鉴权的代码 ...\n\t\t}).isAnnotation(false)).addPathPatterns(\"/**\");\n\n\t\t// 打开注解鉴权\n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n\t}\n}\n```\n如上，第一个完成路由拦截鉴权功能，第二个完成注解鉴权功能。\n\n\n### Q：我的项目权限模型不是RBAC模型，很复杂，可以集成吗？\n无论什么模型，只要能把一个用户具有的所有权限塞到一个List里返回给框架，就能集成\n\n\n### Q：StpInterface 接口的  方法，在什么时候执行？\n每次鉴权时执行，例如你调用了 `StpUtil.checkgetPermission(\"xxx\")` 方法，框架就会调用底层的 `StpInterface#getPermissionList` 方法来获取权限数据。\n\n如果你的 `getPermissionList` 里有读数据库的代码，那么你每鉴一次权，系统将访问一次数据库。如果要减小性能消耗，可以把权限数据放在缓存中，参考：[把权限放在缓存里](/fun/jur-cache)。\n\n\n### Q：当我配置不并发登录时，每次登陆都会产生一个新的 Token，旧 Token 依然被保存在 Redis 中，框架为什么不删除呢？\n首先，不删除旧 Token 的原因是为了在旧 Token 再次访问系统时提示他：已被顶下线。\n\n而且这个 Token 不会永远留在 `Redis` 里，在其 TTL 到期后就会自动清除，如果你想让它立即消失，可以：\n\n- 方法一：配置文件把 `is-concurrent` 和 `is-share` 都打开，这样每次登陆都会复用以前的旧 Token，就不会有废弃 Token 产生了。 \n- 方法二：每次登录前把先调用注销方法 `StpUtil.logout(10001)` ，把这个账号的旧登录都给清除了。\n- 方法三：写一个定时任务查询 Redis 值进行删除。\n\n\n### Q：我使用过滤器鉴权 or 全局拦截器鉴权，结果 Swagger 不能访问了，我应该排除哪些地址？\n尝试加上排除 `\"/swagger-resources/**\", \"/webjars/**\", \"/v2/**\", \"/swagger-ui.html/**\" ,\"/doc.html/**\",\"/error\",\"/favicon.ico\"`\n\n不同版本可能会有所不同，其实在前端摁一下 `F12` 看看哪个 url 报错排除哪个就行了（另附：注解鉴权是不需要排除的，因为 `Swagger` 本身也没有使用 Sa-Token 的注解）\n\n\n### Q：SaRouter.match 有多个路径需要排除怎么办？\n可以点进去源码看一下，`SaRouter.match`方法有多个重载，可以放一个集合, 例如：\n``` java\nSaRouter.match(\"/**\").notMatch(\"/login\", \"/reg\").check(r -> StpUtil.checkLogin());\n```\n\n\n### Q：为什么StpUtil.login() 不能直接写入一个User对象？\n`StpUtil.login()`只是为了给当前会话做个唯一标记，通常写入`UserId`即可，如果要存储User对象，可以使用`StpUtil.getSession()`获取Session对象进行存储。 \n\n\n### Q：前后端分离模式下和普通模式有何不同？\n主要是失去了`Cookie`无法自动化保存和提交`token秘钥`，可以参考章节：[前后端分离](/up/not-cookie)\n\n\n### Q：前后端分离时，前端提交的 header 参数是叫 token 还是 satoken 还是 tokenName？\n默认是satoken，如果想换一个名字，更改一下配置文件的`tokenName`即可。\n\n\n### Q：一个账号拥有哪些权限，可以做成动态的吗？\n权限本来就是动态的，框架预留的 `StpInterface` 接口，就是为了让你可以写任意代码来获取数据\n\n\n### Q：路由拦截鉴权，可以做成动态的吗？\n参考：[把路由拦截鉴权动态化](/fun/dynamic-router-check)\n\n\n### Q：我不想让框架自动操作Cookie，怎么办？\n在配置文件将`isReadCookie`值配置为`false`\n\n\n### Q：怎么关掉每次启动时的字符画打印？\n在配置文件将`isPrint`值配置为`false`\n\n\n### Q：StpUtil.getSession()必须登录后才能调用吗？如果我想在用户未登录之前存储一些数据应该怎么办？\n`StpUtil.getSession()`获取的是`Account-Session`，必须登录后才能使用，如果需要在未登录状态下也使用Session功能，请使用`Token-Session` <br>\n步骤：先在配置文件里将`tokenSessionCheckLogin`配置为`false`，然后通过`StpUtil.getTokenSession()`获取Session 。或者直接调用 `StpUtil.getAnonTokenSession()` 获取匿名 Token-Session。\n\n\n### Q：我只使用header来传输token，还需要打开Cookie模式吗？\n不需要，如果只使用header来传输token，可以在配置文件关闭Cookie模式，例：`isReadCookie=false`\n\n\n### Q：我想让用户修改密码后立即掉线重新登录，应该怎么做？\n框架内置 [强制指定账号下线] 的APi，在执行修改密码逻辑之后调用此API即可: `StpUtil.logout()`\n\n\n### Q：代码鉴权、注解鉴权、路由拦截鉴权，我该如何选择？\n这个问题没有标准答案，这里只能给你提供一些建议，从鉴权粒度的角度来看：\n1. 路由拦截鉴权：粒度最粗，只能粗略的拦截一个模块进行权限认证\n2. 注解鉴权：粒度较细，可以详细到方法级，比较灵活\n3. 代码鉴权：粒度最细，不光可以控制到方法级，甚至可以if语句决定是否鉴权\n\nSo：从鉴权粒度的角度来看，需要针对一个模块鉴权的时候，就用路由拦截鉴权，需要控制到方法级的时候，就用注解鉴权，需要根据条件判断是否鉴权的时候，就用代码鉴权 \n\n\n### Q：Sa-Token的全局过滤器我应该怎么指定它的优先级呢？\n为了保证相关组件能够及时初始化，框架默认给过滤器注册的优先级为-100，如果你想更改优先级，直接在注册过滤器的方法上加上 `@Order(xxx)` 即可覆盖框架的默认配置\n\n\n### Q：timeout 过期了，获取到的 NotLoginException 场景值是-2，按照文档说的应该是-3吧。是我理解的不对还是操作有误？\n你的理解是对的，但是框架现在只能做到返回-2，因为 token 过期后，就从 Redis 中消失了，框架没法分辨这个 token 是曾经有过然后过期的，还是从来就没有在Redis中存在过，\n所以只能统一抛出-2，这个行为也和具体使用的 SaTokenDao 有关联，例如集成 sa-token-jwt 插件后，框架就能分辨出来是 token 过期了，抛出-3。\n\n\n### Q：Sa-Token 是否提供类似 RefreshToken 的概念，与 AccessToken 相互配合刷新令牌鉴权。\n关于长短 token，Sa-Token 没有提供直接的 API 支持，但是你可以利用 “临时 token 认证模块” 轻易的达到这一点：\n\n1. 把 `sa-token.timeout` 的值配置小一点，然后把 `StpUtil.login(10001)` 生成的 token 作为短 token ，用来鉴权。\n2. 用 “临时 token 认证模块” 生成长 token， `String refreshToken = SaTempUtil.createToken(10001, 2592000);`。\n3. 把这两个 token 一起返回到前端。\n4. 你再开个接口，可以让前端通过长 token，刷新短 token，参考代码：\n\n``` java\n@RequestMapping(\"/refreshToken\")\npublic SaResult refreshToken(String refreshToken) {\n\t// 1、验证\n\tObject userId = SaTempUtil.parseToken(refreshToken);\n\tif(userId == null) {\n\t\treturn SaResult.error(\"无效 refreshToken\");\n\t}\n\n\t// 2、为其生成新的短 token\n\tString accessToken = StpUtil.createLoginSession(userId);\n\n\t// 3、返回\n\treturn SaResult.data(accessToken);\n}\n```\n\n\n### Q：前后端一体项目下，在拦截未登录进入登录页面时，如何登录完成后原路返回？\n可以在拦截跳转登录页面时，把原 url 作为 back 参数挂载到登录页后方，登录完成后读取 back 参数并跳转\n``` java\n@RestControllerAdvice\npublic class GlobalException {\n\t// 未登录异常拦截 \n\t@ExceptionHandler(NotLoginException.class)\n\tpublic Object handlerException(NotLoginException e) {\n\t\te.printStackTrace();\n\t\treturn SaHolder.getResponse().redirect(\"/login?back=\" + SaHolder.getRequest().getUrl());\n\t}\n}\n```\n\n\n\n### Q：怎么改变请求返回的 http 状态码？\n``` java\nSaHolder.getResponse().setStatus(401)\n```\n\n\n\n### Q：Sa-Token 集成 Redis 如何集群？\n以 `sa-token-redis-template` 为例：Sa-Token 底层使用的是 RedisTemplate 对象来操作数据的，也就是说，你只要给 RedisTemplate 配置上集群模式，Sa-Token 自动就是集群模式了。\n\n\n### Q：多个项目共用同一个 redis，怎么防止冲突？\n\n首先，如无特殊需求，建议多个项目不要共用同一个 redis，如果非要共用，有以下方式避免数据冲突：\n\n- 方式 1：使用不同的 db 索引，Redis 默认提供 16 个 database 容器，每个项目配置不同的 db 索引即可。\n- 方式 2：给项目配置不同的 `sa-token.token-name` 值，此配置项默认为 `satoken`，是框架在 Redis 存储数据时使用的统一前缀。\n- 方式 3：使用 `sa-token-three-redis-jackson-add-prefix` 插件，参考：[sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin)。\n\n\n### Q：如何防止 CSRF 攻击？\nCSRF 攻击的核心在于利用浏览器自动提交 Cookie 的特性，代替用户发送自己不想发送的请求。\n\n**方案一：关闭 Cookie模式。**\n\n在配置文件里配置 `sa-token.is-read-cookie=false` 关闭 Cookie 读取模式，采用 localStorage 存储 token + header 头提交，即可避免 CSRF 攻击。\n\n**方案二：增加 csrf-token 验证**\n\n如果项目必须采用 Cookie 模式验证，可以在请求中增加 csrf-token 验证的环节：\n\n1、在登录时，生成一个 `csrf_token` 返回到前端：\n``` java\n// 测试登录 \n@RequestMapping(\"/login\")\npublic SaResult login() {\n\tStpUtil.login(10001);\n\tString csrfToken = StpUtil.getSession().get(\"csrf_token\", () -> SaFoxUtil.getRandomString(60));\n\treturn SaResult.ok().set(\"csrf_token\", csrfToken);\n}\n```\n\n2、前端将 csrf_token 存储在 localStorage 中（注意一定要存储在 localStorage 而非 Cookie 中，存储在 Cookie 中还是可能会被浏览器自动提交）\n``` java\nlocalStorage.setItem('csrf_token', csrf_token);\n```\n每次请求将 csrf_token 塞到 Header 中。\n\n3、在需要防止 CSRF 攻击的接口验证 csrf_token：\n``` java\n@RequestMapping(\"/test\")\npublic SaResult test() {\n\n\t// 先验证 csrfToken \n\tString csrfToken = SaHolder.getRequest().getHeader(\"csrf_token\");\n\tif (csrfToken == null || ! csrfToken.equals(StpUtil.getSession().get(\"csrf_token\")) ) {\n\t\tthrow new SaTokenException(\"csrf_token 不匹配\");\n\t}\n\n\t// 通过后再处理具体业务\n\t// ...\n\n\treturn SaResult.ok();\n}\n```\n\n也可以将验证代码写到全局拦截器中，为所有接口提供校验。\n\n\n\n### Q：如何自定义框架读取 token 的方式？\n**方式一：通过 StpUtil.getStpLogic().setTokenValueToStorage(\"abcdefgxxxxxxxx\") 自定义 token 值**\n\n如果你可以在框架读取 token 之前写一些代码，那么你可以通过如下代码自定义当前请求的 token 值：\n``` java\n@RequestMapping(\"/test\")\npublic SaResult test() {\n\tSystem.out.println(StpUtil.getTokenValue()); // 此时读取到的是前端提交的: cebcc930-c0f5-4009-8eb0-1b6aee63b4aa\n\tStpUtil.getStpLogic().setTokenValueToStorage(\"abcdefgxxxxxxxx\");\n\tSystem.out.println(StpUtil.getTokenValue()); // 此时读取到的是我们自定义的: abcdefgxxxxxxxx\n\treturn SaResult.ok();\n}\n```\n\n**方式二：重写 StpLogic 读取 token 的方法**\n\n``` java\n@Component\npublic class MyStpLogic extends StpLogic {\n    public MyStpLogic() {\n        super(\"login\");\n    }\n\t// 自定义 token 读取方式，例如此处改为读取请求头为 my-token 的值 \n    @Override\n    public String getTokenValue() {\n        String token = SaHolder.getRequest().getHeader(\"my-token\");\n        return token;\n    }\n}\n```\n\n\n\n\n### Q：文档是否能下载？是否有离线版？\n文档已完整开源，请访问 Sa-Token 官方仓库，根目录下的 sa-token-doc 文件夹就是文档。\n\n\n\n\n### Q：还是有不明白到的地方?\n请在`gitee` 、 `github` 提交 `issues`，或者加入qq群交流，[群链接](/more/join-group)\n\n"
  },
  {
    "path": "sa-token-doc/more/content-cooperation.md",
    "content": "# Sa-Token 内容合作群 \n\n**好内容值得被看见！**\n\n为感谢 Sa-Token 的内容创作者们，我们特别创建了「Sa-Token 内容合作群」，帮助大家的内容触达更多 Sa-Token 的使用者 (加群方式在最下方)。\n\n--- \n\n\n### 📖 1、一些碎碎念，想和写公众号/录视频的朋友们聊聊\n\n前几天，我在公众号上搜索 “Sa-Token”，想看看有没有人写过相关的教程或者踩坑心得。\n\n说实话，当时没抱太大期望。毕竟我们开发团队这几年来几乎将所有精力都放在了代码开发，而一直疏于内容运营建设。\n\n但结果让我挺意外的 —— **我发现了不少公众号都在写 Sa-Token 的文章，而且其中不少都写得很用心**。\n\n有从零开始的入门教程，有深入源码的解析文章，有对框架各个功能的介绍，还有一些结合真实业务场景的实战案例 …… 解决的都是实实在在的问题。\n\n但这些文章的阅读量… 很多都只有几百，有些甚至只有几十。\n\n我知道这很正常。**技术公众号起步非常不容易，粉丝少的时候，再好的内容也很难被看到**。我自己也经历过这个阶段，知道那种 [ 写了一整天，发出来没人看 ] 的感觉。\n\n为了不让这些有价值的内容埋没，我连夜将这些文章整理到了 Sa-Token 官网：[框架博客](/more/blog)。\n\n在整理这些博客的过程中，我突然有了一个想法。\n\n\n### ✨ 2、一个可能 [三赢] 的想法\n\n我在想，我为什么不拉一个群聊，把这些为 Sa-Token 写文章的博主们，给聚集起来呢？\n\n只要有朋友写了 Sa-Token 相关文章，都可以转发到群里，我们团队会把这个文章转发到 Sa-Token 所有粉丝群里：\n\n这可能是一个三赢的合作：\n\n- **对于 Sa-Token 来说**：能获得更多的优质内容，帮助新用户更快上手，生态也能更丰富。\n- **对写文章的朋友来说**：你的好文章能被更多人看到，公众号能涨涨粉，付出的时间更有价值。\n- **对 Sa-Token 的用户来说**：能看到更多的技术干货，学到更多知识，找到各种场景的解决方案，不用重复踩坑。\n\n听起来好像…还不错？\n\n所以我打算建个群，名字就叫 「Sa-Token 内容合作群」。不是什么正式的组织，就是一群对技术内容感兴趣的朋友，凑在一起互相帮帮忙。\n\n\n### 🤝 3、这个群主要用来做什么？\n\n1、如果你写了 Sa-Token 相关的文章(或录制了视频课程)，可以分享到群里，我们团队会把文章：\n\n- **转发到 Sa-Token 所有粉丝群里**：Sa-Token 目前拥有 30+ 微信粉丝群 (500人)，10+ QQ粉丝群 (1000人 or 2000人)。\n- **挂载到 Sa-Token 在线文档博客栏目**：Sa-Token 目前在线文档访问量月PV 20万+。\n\n相信这一定可以大大提高文章的曝光量。\n\n2、我们团队偶尔也会为 Sa-Token 撰写技术文章，发到群里：\n\n- 如果你觉得内容不错，想转载到自己的公众号，**直接转就行**。\n- 不用专门申请授权，Sa-Token 官方订阅号所有内容均开放版权，任何人都可以自由转载。\n\n\n### ❤️ 4、几个你可能关心的小问题\n\n#### Q：我没写过 Sa-Token 的文章，可以加入吗？\n可以。完全没问题。哪怕你之前从来没写过 Sa-Token 的文章，但只要你有公众号，想试试写相关的内容，都欢迎。\n\n如果你没有公众号，但是在别的平台，比如掘金、CSDN有写过文章，也可以加入。如果你在 B站/抖音录制过视频，也可以加入。\n\n总之：只要你有意向在任意平台创作 Sa-Token 相关内容，就可以加入。\n\n\n#### Q：有 KPI 吗？是不是进了群就要为 Sa-Token 写文章？\n没有。 想写就写，不想写就不写。哪怕加群后一篇都不写，也没关系。**这就是个「互助群」，不是「任务群」**。\n\n#### Q：文章有什么要求吗？\n就一点：认真写。 \n可以是入门教程、源码解析、实战心得、bug排查、常见踩坑、对比测评…什么形式都行。不需要多长的篇幅，能把一个知识点讲清楚就好。唯一的要求是：不要是纯 AI 生成的粗制滥造文，不要写明显错误的技术观点。\n\n#### Q：对粉丝量有要求吗？\n没有要求。 我自己也是从小号做起来的，完全理解起步的难处。群里不分大号小号，只看内容用不用心。\n\n#### Q：除了转发，还有别的吗？\n有。 我偶尔会分享一些 Sa-Token 的更新动态、设计思路，或者我发现的其他好的技术文章。大家也可以互相**聊聊技术写作、视频制作的心得，分享好用的工具**。就是个普通的交流群，只不过主题稍微聚焦一点。\n\n说到底，我希望这个群不只是一个内容分发渠道，更是一个 「Sa-Token内容共创伙伴」的聚集地。我们一起，让好的技术方案被更多人看见和使用。\n\n\n### 👋 5、怎么加群？\n\n你可以在 [加入讨论群](/more/join-group) 处，添加我们的微信账号， **请在添加时备注或者加好友成功后发送以下信息：[申请加入 Sa-Token 内容合作群]** 。\n\n一定要备注以上信息，否则我们团队人员只会把你拉入到普通粉丝交流群。\n\n<img class=\"s-w\" src=\"/big-file/contact/show/wx-sthzq-show.png\" style=\"max-width: 50%;\" alt=\"Sa-Token 生态合作群\" />\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/more/demand-commit.md",
    "content": "# 需求提交\n\n文档不清晰？功能不完善？脑袋里有好 idea？提！都可以提！\n\n比起浮夸的赞美，Sa-Token 更希望收到您的批评与建议。\n\n我们深知一个优秀的项目不能闭门造车，它需要海纳百川：[点我在线提交需求](https://wj.qq.com/s2/10852322/0d8b/)\n\n<!-- 我们深知一个优秀的项目需要海纳百川，请把你的不满、吐槽、创意纷纷砸过来：[点我在线提交需求](https://wj.qq.com/s2/10852322/0d8b/) -->\n\n我们将慎重对待每一位粉丝的珍贵意见 ❤️ ❤️ ❤️：\n\n- 对框架新增特性功能且比较简单，会在第一时间进行开发。\n- 对框架新增特性功能但比较复杂，会延后几个版本制定相应的计划后进行开发。\n- 与框架设计理念不太相符，或超出权限认证范畴，将会视需求人数决定是否开发。\n\n\n### 其它反馈途径\n除了问卷提交，你还可以从以下渠道向我们提交反馈：\n- Gitee：[issue 提交](https://gitee.com/dromara/sa-token/issues)  \n- GitHub：[issue 提交](https://github.com/dromara/sa-token/issues)  \n- AtomGit：[issue 提交](https://atomgit.com/dromara/sa-token/issues)\n- 交流群：[加群链接](/more/join-group)\n\n请大胆提交、大胆咨询，请在交流群中大胆艾特我们，请不要有任何害羞 🤭。就算我们不实现，你也不会损失什么，对吧！\n"
  },
  {
    "path": "sa-token-doc/more/join-group.md",
    "content": "# 加入讨论群\n\n加入 Sa-Token 专属讨论群，与众多大佬一起努力 (huá shuǐ) 成长 (mō yú)。\n\n---\n\n### 1、加入QQ交流群\n\n<!-- QQ交流群：685792424 [点击加入](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Y05Ld4125W92YSwZ0gA8e3RhG9Q4Vsfx&authKey=IomXuIuhP9g8G7l%2ByfkrRsS7i%2Fna0lIBpkTXxx%2BQEaz0NNEyJq00kgeiC4dUyNLS&noverify=0&group_code=685792424)\n -->\n \n<!-- QQ交流群：936523917 [点击加入](https://qm.qq.com/q/xfoMJA5Az0) -->\nQQ交流群：1081649142 [点击加入](https://qm.qq.com/q/SCAaZ6Ros2) \n\n### 2、加入微信交流群：\n\n<!-- <img class=\"s-w\" src=\"/big-file/contact/wx-qr-300.png\" style=\"width: 180px;\" alt=\"微信群\" /> -->\n\n<img class=\"s-w\" src=\"/big-file/contact/i-wx-qr2.jpg\" style=\"width: 180px;\" alt=\"微信群\" />\n\nPS：扫码添加微信 (备注：sa)，邀您加入群聊。\n\n<br>\n\n\n<img class=\"s-w\" src=\"/big-file/contact/show/wx-group-show3.png\" style=\"max-width: 50%;\" alt=\"微信群\" />\n\n\n加入群聊的好处：\n- 第一时间收到框架更新通知。\n- 第一时间收到框架 bug 通知。\n- 第一时间收到新增开源案例通知。\n- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。\n\n\n\n### 3、群规（碎碎念）：\n有同学质问我们，我加了群，为什么被踢了？我们很少踢人，一般只有严重违反群规了我们才会选择踢人。\n- 不要在群里发擦边图、视频。轻度我们会警告，重度我们会选择移出群聊。\n- 不要在群里聊代理、魔法上网等话题，如有需求请各自互相私聊，不要在群里聊。\n- 不要发和程序员无关的广告，和程序员有关的比如开源项目、IT网站等我们一般不管。\n\n请体谅我们，我们拉一个群也不容易，辛辛苦苦几个月才能拉满一个500人群，结果因为一些违规消息就导致封群，很难受的！\n\n被踢了还能再次加群吗？可以，只要你想加，并保证不再发布违规消息，就可以在被踢7天之后再次申请加群。\n\n\n\n\n### 4、内部群：\n\n为感谢对 Sa-Token 生态做出贡献的同学，我们特创建了内部群：【Sa-Token 生态共享与合作】\n\n加入群聊条件，以下满足其一即可：\n- 写过5篇以上有关 Sa-Token 的原创博客。\n- 为 Sa-Token 开发过插件。\n- 有开源项目集成了 Sa-Token，并在 [Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) 完成提交。\n- 有为 Sa-Token 录制过教程视频，发表在公共平台（总时长>30分钟，且播放量>2000）。\n- 其它一些您认为有对 Sa-Token 生态做出贡献的行为，可以直接联系我们，经内部投票评审通过即可加入（不要害羞，大胆联系我们哦 😊 ）\n\n加入群聊的好处：\n\n- 更及时的获知 Sa-Token 下一步更新计划。\n- 在 Sa-Token 遇到的任何疑问都可以当面与作者沟通，可协助解决问题。\n- 可提出未来版本更新需求，将具有更高的优先级进行评审与开发。\n\nQQ群聊号码：939849926 \n\n注：此为专属内部群聊，不满足上述条件的同学请勿过分申请打扰，谢谢合作。满足条件者可以在申请加入时备注上您的项目名称\n（例如：xx开源项目作者集成了 sa-token，申请加入群聊），如果字数太多无法写完，也可在开源交流群里@管理员协助交流。\n\n\n### 5、Sa-Token 内容合作群\n专门为 Sa-Token 内容创作者们准备的交流群：[Sa-Token 内容合作群](/more/content-cooperation) \n\n"
  },
  {
    "path": "sa-token-doc/more/link.md",
    "content": "# 使用 Sa-Token 的开源项目 \n\n\n> 集成 Sa-Token 的开源案例收集，取自 Awesome-Sa-Token，定期同步：\n> [Gitee](https://gitee.com/sa-tokens/awesome-sa-token)、\n> [GitHub](https://github.com/sa-tokens/awesome-sa-token)、\n> [AtomGit](https://atomgit.com/sa-tokens/awesome-sa-token)\n\n---\n\n\n### 📊 后台管理\n\n- [[ art-design-pro-java ]](https://github.com/anganing/art-design-pro-java)：SpringBoot17+Sa-token+Art-Design-Pro+Unibest 技术栈的企业级后台开发管理系统。\n\n- [[ wemirr-platform ]](https://gitee.com/battcn/wemirr-platform)：JDK17、SCA2023、SC2024、Sa-Token、VBen5.x 全网最炫酷，功能最多，最优雅地真开源 多租户、SAAS 微服务项目。\n\n- [[ Lucky-Admin-Vue ]](https://gitee.com/xiaodu6/lucky-admin-vue)：一个基于vue-admin-template的后台管理框架，集成了动态角色权限，动态路由，角色权限动态配置，日志框架，代码生成，Sa-Token权限校验，快速构建一个后台的开发框架。\n\n- [[ 灯灯]](https://github.com/dromara/lamp-cloud)：基于java + SpringCloudAlibaba +SpringBoot 开发的微服务中后台快速开发平台，专注于多租户 (SaaS架构) 解决方案，亦可作为普通项目（非SaaS架构）的基础开发框架使用，目前已实现 数据源隔离、字段隔离、无租户隔离 等几种模式。\n\n- [[ 橙单 ]](https://gitee.com/orangeform/orange-admin)：技术栈Boot3 + Flowable7 + Sa-Token + Mybatis-Flex/Mybatis-Plus + Vue3，支持开箱即用且功能完成的工作流和在线表单功能，提供高颜值的流程和表单编辑器全部前后端源码。\n\n- [[ Sz-Admin ]](https://github.com/feiyuchuixue)：一个开源RBAC中后台框架，专为现代应用设计。它结合了最新的技术栈，包括后端的Spring Boot 3、JDK 21、Mybatis Flex、Sa-Token、Knife4j和Flyway，以及前端的Vue 3、Vite5、TypeScript和Element Plus，致力于为您提供一个直观、流畅且功能强大的开发体验。\n\n- [[ newbie-boot3 ]](https://github.com/zhangyuge7/newbie-boot3)：企业级中大型项目快速开发平台，后端使用JDK21+SpringBoot3+SaToken+MybatisPlus等，前端基于FiveAdminV2后台管理系统模板开发，使用js+vue3+vite5+ElementPlus等最新技术栈。\n\n- [[ EuBackend ]](https://gitee.com/zhaoeryu/eu-backend)：EuBackend 是一套全部开源的前后端分离 Java EE 企业级快速开发平台，基于最新技术栈SpringBoot、Sa-Token、MyBatisPlus等作为后端框架，使用RBAC作为权限控制模型，并且毫无保留给个人及企业免费使用。\n\n- [[ srppms ]](https://gitee.com/cai-bin00/srppms)：基于SpringBoot+Vue+sa-token前后端分离的科研项目管理平台。\n\n- [[ twelvet-fast ]](https://gitee.com/twelvet/twelvet-fast)：基于Spring Boot 3 JDK17的单体服务极速开发管理平台脚手架，先行体验最新技术栈。\n\n- [[ Sa-Plus ]](https://gitee.com/click33/sa-plus)：一个基于 SpringBoot 架构的快速开发框架，内置代码生成器。\n\n- [[ dcy-fast ]](https://gitee.com/dcy421/dcy-fast)：一个基于 SpringBoot + Sa-Token + Mybatis-Plus 的后台管理系统，前端vue-element-admin，并且内置代码生成器。\n\n- [[ Helio-Boot ]](https://gitee.com/uncarbon97/helio-boot)：基于 SpringBoot + Sa-Token + Mybatis-Plus 的单体开发脚手架，带有配套后台管理前端模板及代码生成器；拥有对应微服务版脚手架`Helio-Cloud`\n\n- [[ EasyAdmin ]](https://gitee.com/lakernote/easy-admin)：一个基于SpringBoot2 + Sa-Token + Mybatis-Plus + Snakerflow + Layui 的后台管理系统，灵活多变可前后端分离，也可单体，内置代码生成器、权限管理、工作流引擎等\n\n- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus)：重写RuoYi-Vue所有功能 集成 Sa-Token+Mybatis-Plus+Jackson+Xxl-Job+knife4j+Hutool+OSS 定期同步\n\n- [[ SpringBoot_v2 ]](https://gitee.com/bdj/SpringBoot_v2)：SpringBoot_v2项目是努力打造springboot框架的极致细腻的脚手架。\n\n- [[ Ruoyi-Satoken ]](https://gitee.com/wangming123456/ruoyi-satoken)：为 ruoyi 进行配置 sa-token\n\n- [[ vue-satoken-admin ]](https://gitee.com/niluni/vue-satoken-admin)：基于Vue2和Sa-Token1.18.0的后台权限系统。\n\n- [[ bootx-platform ]](https://gitee.com/bootx/bootx-platform)：包含支付收单(支付宝、微信、聚合、组合支付)、工作流(Flowable)、三方对接(微信、钉钉、企微、短信)等模块，前端基于Vue2和Vue3分别打造，可应用在不同业务场景中，目标是致力实现媲美商业版应用脚手架。\n\n- [[ spba-admin ]](https://gitee.com/qkdja/spring-boot-admin)：基于SpringBoot、Vue开发的通用后台管理系统，做到开箱即用，为新项目开发省去了基础功能开发的步骤。主要使用Sa-Token权限认证、MyBatis-Plus、MySQL、Redis、validation、七牛云等技术。\n\n- [[ QForum-Core ]](https://github.com/Project-QForum/QForum-Core/)：QForum 论坛系统官方核心，可拓展性强、轻量级、高性能、前后端分离，基于 SpringBoot2 + Sa-Token + Mybatis-Plus\n\n- [[ ExciteCMS-Layui ]](https://gitee.com/ExciteTeam/ExciteCMS-SpringBoot-Layui)：ExciteCMS 快速开发脚手架：一款后端基于 SpringBoot2 + Sa-Token + Mybatis-Plus，前端基于 Layuimini 的内容管理系统，具备RBAC、日志管理、代码生成等功能，并集成常用的支付、OSS等第三方服务，拥有详细的开发文档\n\n- [[ sra-admin ]](https://github.com/CoCoTeaNet/sra-admin)：快速开发脚手架，核心依赖：springboot3+sqltoy+satoken+hutool | 轻量级 | 只实现了用户、字典、角色、权限等常见功能，能够快速搭建一个web项目。\n\n- [[ QuickBuild ]](https://gitee.com/CodeLiQing/custom-quick-build-platform): 快速构建 | 基于springboot+sa-token+neety+代码生产器（生成vue页面和增删改查代码）| 以及前端vue3和字节arco.design框架整合 \n\n- [[ magic-boot ]](https://gitee.com/ssssssss-team/magic-boot)：基于 magic-api + Sa-Token 搭建的快速开发平台，可以实现在浏览器编写Vue代码，既改即生效\n\n- [[ chaos ]](https://gitee.com/qishanor/chaos)：一个基于 SpringBoot + Sa-Token + Mybatis-Plus的快速开发框架，前端vue-element-avue,内置代码生成器，代码最简洁，最佳学习实践方案。\n\n- [[ xzadmin ]](https://gitee.com/xiaozhizxj/xzadmin)：一个基于 Spring Boot+mybatis-plus+sotaken+Redis+Thymeleaf+hutool+easy-captcha+log4j的后台管理系统\n\n- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy)：国内首个国密前后分离快速开发平台，采用 Vue3 + AntDesignVue3 + Vite + SpringBoot + Mp + HuTool + SaToken\n\n- [[ XyyAdmin ]](https://gitee.com/xyy12611/springboot-xyy-admin-v3)：开箱即用的前后端分离后台权限系统，关键技术SpringBoot、Sa-Token、MySql、Vue3、AntDesignVue。\n\n- [[ Frsimple ]](https://gitee.com/frsimple/springboot)：一个基于 SpringBoot + Sa-token +  Tdesign-next + vite + vue3 + typescript 的开箱即中后台服务解决方案。\n\n- [[ sa-admin-server ]](https://gitee.com/wlf213/sa-admin-server)：sa-admin-server是一个后台管理框架的服务端，核心技术：SpringBoot+SaToken+Quartz+Cache+Redis+Netty+MyBatisPlus; 亮点：RABC动态权限+零SQL+定时任务+缓存+在线IM; 前后端可分离也可一体部署，可选七牛云对象存储和本地存储两种方式。\n\n- [[ RuoYi-Vue-CMS ]](https://gitee.com/liweiyi/RuoYi-Vue-CMS)：RuoYi-Vue-CMS是前后端分离的内容管理系统，支持站群管理、多平台静态化、元数据模型扩展、多语言、全文检索，能轻松组织各种复杂内容形态。技术栈：SpringBoot3 + VUE2 + MybatisPlus + Sa-Token + xxl-job + Freemarker + ES + Redis + MySQL。\n\n- [[ springboot-multi-tenant-sa-token ]](https://gitee.com/willf/springboot-multi-tenant-sa-token)：轻量的多租户后台管理系统脚手架（SpringBoot，Sa-Token，mybatis-plus，Vue & Element）。\n\n- [[ solon_angis_beetlsql ]](https://gitee.com/smartcity/solon_angis_beetlsql)：并元国产开发平台 solon、sa-token、beetlsql、smart-http\n\n- [[ zeta-kotlin ]](https://gitee.com/xia5800/zeta-kotlin)：zeta-kotlin是使用kotlin语言基于spring boot、mybatis-plus、sa-token等框架开发的项目脚手架。\n\n- [[ nebula-swagger-demo ]](https://gitee.com/flgitee/nebula-swagger-demo)：springboot+nebula 集成knife4j案例\n\n- [[ warm-sun]](https://gitee.com/min290/warm-sun)：基于solon+vue3开发，jdk17+satoken+redisx/redisson+mybaits-flex+hutool+jackson+mapstruct+poi\n\n- [[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 等。\n\n- [[laymini-admin]](https://gitee.com/wlf213/laymini-admin)：基于layuimini前端框架开发的一个简单的后台管理前后端不分离框架，主体技术mybatisplus+sa_token+springboot+freemarker，主要功能：RABC认证授权，后台管理功能，集成Quartz动态定时任务。\n\n- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin)：SmartAdmin国内首个以「高质量代码」为核心，「简洁、高效、安全」中后台快速开发平台；基于SpringBoot + Sa-Token + Mybatis-Plus 和 Vue3 + Vite5 + Ant Design Vue 4.x (同时支持JavaScript和TypeScript双版本)；满足国家三级等保要求、支持登录限制、接口数据国产加解密、高防SQL注入等一系列安全体系。\n\n- [[ Halcyon-Admin ]](https://github.com/hhfb8848/halcyon-springboot)：基于 Spring Boot 3 和 Vue 3 的通用后台管理系统，专注于提供基本的管理功能，而非特定的部门管理或业务功能。\n\n- [ breeze-boot-satoken-xxx系统 ]：breeze-boot-satoken-xxx 是一个开源免费（前后端分离）中后台管理系统基础解决方案，前端技术栈：（ Vue3、 TypeScript、Element Plus、Pinia 、Vite）后端技术栈：（jdk17、 springboot3、SaToken、MybatisPlus等）\n    - SSO 版本，后端：https://gitee.com/breeze-boot/breeze-boot-satoken-sso \n    - SSO 版本，前端：https://gitee.com/breeze-boot/breeze-vite-ui-satoken-sso \n    - OAUTH 版本，后端：https://gitee.com/breeze-boot/breeze-boot-satoken-oauth \n    - OAUTH 版本，前端： https://gitee.com/breeze-boot/breeze-vite-ui-satoken-oauth \n- [[ Summer-Flowers · 夏花 ]](https://gitee.com/Luv404/summer-flowers)：基于 **Spring Boot 3 + JPA + QueryDSL + Sa-Token** 的企业级后台开发框架，前端采用 **SoybeanAdmin**。不同于常见 MyBatis 体系，Summer-Flowers 以 **Entity 作为业务第一表达**，通过 QueryDSL 实现类型安全的复杂查询，配合代码生成器与模块化架构，显著降低中长期项目的维护成本。\n\n\n\n\n### 🚀 微服务相关\n\n- [[ XHan Admin ]](https://gitee.com/sun-xiaohan/xh-admin-frontend)：XHan Admin 是一个开源免费（前后端分离）中后台管理系统基础解决方案, 无专业版收费，所有功能毫无保留的贡献给开源社区，使用最新技术栈全新开发，无任何历史代码包袱。\n\n- [[ RuoYi-Cloud-Plus ]](https://gitee.com/dromara/RuoYi-Cloud-Plus)：重写RuoYi-Cloud所有功能 整合 SpringCloudAlibaba + Sa-Token + Dubbo + Mybatis-Plus + Xxl-Job 全方位升级 定期同步\n\n- [[ Sp-Cloud ]](https://gitee.com/click33/sp-cloud)：Sa-Plus的微服务版本, 基于Spring-Cloud-Alibaba，微服务下使用Sa-Token的样例\n\n- [[ YC-Framework ]](http://framework.youcongtech.com/)：致力于打造一款优秀的分布式微服务解决方案\n\n- [[ falser-cloud ]](https://gitee.com/falser/falser-cloud): 基于 SpringCloud Alibaba + SpringCloud gateway + SpringBoot + Sa-Token + vue-admin-template + Nacos + Rabbit MQ + Redis 的一个后台管理系统，前后端分离，权限管理，菜单管理，数据字典，停车场系统管理等功能\n\n- [[ dcy-fast-cloud ]](https://gitee.com/dcy421/dcy-fast-cloud)：一个基于 SpringCloudAlibaba + Sa-Token + dubbo2.7.8 + Seata + knife4j + Mybatis-Plus + MapStruct +  的后台管理系统，前端vue-element-admin，并且内置代码生成器+动态路由权限等功能\n\n- [[ fhs-framework ]](https://gitee.com/fhs-opensource/fhs-framework)：基于Springboot+Springcloud + Mybatis Plus + Sa-Token + Vue + ElementUI 的快速开发平台(低代码开发平台)，本框架永远免费，永久全开源\n\n- [[ Pig-Satoken ]](https://gitee.com/wchenyang/cloud-satoken)：重写 Pig 授权方式为 Sa-Token，其他代码不变。\n\n- [[ Helio-Cloud ]](https://gitee.com/uncarbon97/helio-cloud)：基于 SpringBoot + SpringCloud Alibaba + Sa-Token + Mybatis-Plus 的微服务开发脚手架，带有配套后台管理前端模板及代码生成器\n\n- [[ BudWk-V7 ]](https://gitee.com/budwk/budwk)：基于 NutzBoot + Sa-Token + Dubbo + Nacos注册&配置中心 的微服务开发脚手架(同时提供单应用版本)，带有配套后台管理前端模板及代码生成器\n\n- [[ xr-satoken-cloud ]](https://gitee.com/fzhxfw/xr-satoken-cloud)：一款基于SaToken轻量级Java权限认证框架构建的微服务后台开发脚手架，基于SpringCloud + SpringCloudAlibaba + Nacos + SaToken + Mybatis等技术搭建，内置RBAC权限管理，代码生成器，文件分片速传等，本项目完全开源免费，定期提交代码到dev开发分支，由个人开发者业余时间维护升级。\n\n- [[ CloudEon ]](https://gitee.com/dromara/CloudEon)：一款基于kubernetes的开源大数据平台，旨在为用户提供一种简单、高效、可扩展的大数据解决方案。\n\n- [[ quick-boot ]](https://github.com/csx-bill/quick-boot)：一款基于 Spring Cloud 2022 、Spring Boot 3、AMIS 和 APIJSON 的低代码系统。\n\n- [[ linkin-platform ]](https://gitee.com/paohaizi/linkin-platform)：Springboot + Springcloud + nacos + Mybatis Plus + Sa-Token + Vue3 + ElementPlus\n微服务下使用Sa-Token的样例，是一套比较简洁的后台系统。\n\n- [[ LangChat ]](https://github.com/TyCoding/langchat)：( OpenAI / Gemini / Ollama / Azure / 智谱 / 阿里通义大模型 / 百度千帆大模型), Java生态下AI大模型产品解决方案，快速构建企业级AI知识库、AI机器人应用\n\n### 🛒 商城\n\n- [[ litemall-plus ]](https://gitee.com/ysling-org/litemall-plus)：微信小程序SaaS商城系统，可支持多小程序同时运行。\n\n- [[ mall4j ]](https://gitee.com/gz-yami/mall4j)：基于Spring Boot 3 JDK17的一个商城手脚架。\n\n- [[ Huanxing-mall ]](https://gitee.com/lijiaxing_boy/huanxing-mall)：HuanXing 商城基于SpringCloud 2021 & Alibaba  + Sa-token，前端基于 Vue3 +Element plus 的微服务商城 \n\n\n### 📝 博客\n\n- [[ jthink ]](https://gitee.com/wtsoftware/jthink)： 一个基于 SpringBoot + Sa-Token + Thymeleaf 的博客系统\n\n- [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog)：一款vue+springboot前后端分离的博客系统，博客后台管理系统使用了vue+elmentui开发，后端使用Sa-Token进行权限管理,支持动态菜单权限，动态定时任务，文件支持本地和七牛云上传，使用ElasticSearch作为全文检索服务，支持QQ、微博、码云登录。\n\n- [[ June 12 ]](https://gitee.com/hanshaung/ants)：June 12 是一个纯开源免费的资讯/博客类网站，基于Spring Boot + Sa-Token + Vue开发。\n\n- [[ YuanBlog ]](https://gitee.com/wlf213/yuan-blog)：一款代码简单，功能丰富的多人社交博客平台。前后端分离，Vue+SpringBoot3，博客前端使用Quasar，后台管理前端使用NaiveUI，博客后端，后台管理后端分为两个系统，均使用Sa-Token进行认证授权。支持邮箱验证码登录。\n\n- [[ 鸢尾博客 ]](https://gitee.com/lxwise/iris-blog_parent)：鸢尾博客是一个基于Spring Boot+Vue3 + TypeScript + Vite+JavaFx的客户端和服务器端的博客系统。项目采用前端与后端分离，支持移动端自适应，配有完备的前台和后台管理功能。后端使用Sa-Token进行权限管理,支持动态菜单权限，服务健康监控，数据流量统计，支持QQ、微博、码云、GitHub等三方登录。\n\n\n\n### 🔌 插件\n\n- [[ Sa-Token-Plugin ]](https://gitee.com/bootx/sa-token-plugin)：Sa-Token第三方插件实现，基于Sa-Token-Core，提供一些与官方不同实现机制的的插件集合，作为Sa-Token开源生态的补充\n\n- [[ quarkus-sa-token ]](https://github.com/quarkiverse/quarkus-sa-token)： quarkus 整合 Sa-Token。\n\n\n### 🌐 多语言\n\n- Rust：[[ sa-token-rust ]](https://github.com/sa-tokens/sa-token-rust)： 一个轻量级、高性能的 Rust 认证授权框架。\n\n- Go：[[ sa-token-go ]](https://github.com/sa-tokens/sa-token-go)： 一个轻量级、高性能的 Go 权限认证框架。\n\n- PHP：[[ real-token ]](https://gitee.com/jinan-jimeng-network_0/real-token)： 一个轻量级 thinkphp6 权限认证框架，让鉴权变得简单、优雅！\n\n\n\n### 📦 其它\n\n- [[ 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。\n\n- [[ FlyFlow ]](https://gitee.com/junyue/flyflow)：基于SaToken开发的开源工作流系统：FlyFlow借鉴了钉钉与飞书的界面设计理念，致力于打造一款用户友好、快速上手的工作流程工具。\n\n- [[ Sa-Token-Study ]](https://gitee.com/sa-tokens/sa-token-study)：以demo示例的方式讲解 Sa-Token 源码涉及到的技术点，连载中……\n\n- [[ SpringMvc+Sa-Token ]](https://gitee.com/SRD_01/spring-mvc-sa-token): Jsp+SpringMVC+SSO+Sa-Token+Redis | Spring MVC 集成 SaToken Demo 项目\n\n- [[ iot-kit ]](https://gitee.com/iotkit-open-source/iotkit-parent)：一个轻量级低门槛的物联网平台，包含了多协议设备接入、规则引擎、第三方平台接入、智能家居小程序等模块的项目，基于SpringBoot架构并集成了Sa-Token的OAuth2认证。\n\n- [[ cubic ]](https://gitee.com/dromara/cubic)：一站式问题定位平台，实时线程栈监控、线程池监控、动态arthas命令集、依赖分析等等等，助你快速定位问题。\n\n- [[ ChatGPT-WEB ]](https://github.com/dulaiduwang003/ChatGPT-WEB)：基于JDK17+SpringBoot3+UniApp 绘图 聊天 充值应用。（Web版本）\n\n- [[ SuperBot-ChatGPTApp ]](https://github.com/dulaiduwang003/SuperBot-ChatGPTApp)：基于JDK17+SpringBoot3+UniApp 绘图 聊天 充值应用。（小程序版本）\n\n- [[ ScribbleHub ]](https://github.com/dulaiduwang003/ScribbleHub)：基于SpringBoot+satoken+wxss开发的博客小程序\n\n- [[ TIME-SEA-chatgpt ]](https://github.com/dulaiduwang003/TIME-SEA-chatgpt)：基于SpringBoot+satoken+vue3+uniapp开发的多端Ai平台应用\n\n- [[ SUPERBOT-GPT]](https://github.com/dulaiduwang003/SUPERBOT-GPT)：基于SpringBoot3+satoken+uniapp开发的流量主小程序\n\n- [[ DaxPay ]](https://gitee.com/dromara/dax-pay)：一款免费开源的支付网关系统，支持支付宝、微信、云闪付等通道，提供收单、退款、聚合支付、对账、分账等功能。\n\n- [[ Dinky ]](https://github.com/DataLinkDC/dinky)：基于Apache Flink的实时数据开发平台，实现敏捷的数据开发、部署和运维\n\n- [[ mldong ]](https://gitee.com/mldong/mldong)：SpringBoot + Vue3 快速开发平台、自研工作流引擎\n\n"
  },
  {
    "path": "sa-token-doc/more/noun-intro.md",
    "content": "# Sa-Token 名词解释 \n\nSa-Token 无意发明任何晦涩概念提升逼格，但在处理 issue 、Q群解答时还是发现不少同学因为一些基本概念理解偏差导致代码出错，\n所以整理本篇针对一些比较容易混淆的地方加以解释说明。\n\n也希望各位同学在提交 issue、Q群提问之前充分阅读本篇文章，保证不要因为基本概念理解偏差，增加不必要的沟通成本。\n\n\n--- \n\n#### 几种 Token\n- token：指通过 `StpUtil.login()` 登录产生的身份令牌，用来维护用户登录状态，也称：satoken、会话Token。 \n- temp-token：指通过 `SaTempUtil.createToken()` 临时验证模块产生的Token，也称：临时Token。\n- Access-Token：在 OAuth2 模块产生的身份令牌，也称：访问令牌、资源令牌。\n- Refresh-Token：在 OAuth2 模块产生的刷新令牌，也称：刷新令牌。\n- Same-Token：在 SaSameUtil 模块生成的Token令牌，用于提供子服务外网隔离功能。\n\n\n#### 两种过期时间：\n- timeout：会话 Token 的长久有效期。\n- active-timeout：会话 Token 的最低活跃频率。\n\n两者的差别详见：[Token有效期详解](/fun/token-timeout)\n\n\n#### 三种Session：\n- Account-Session：框架为每个账号分配的 Session 对象，也称：账号Session。 \n- Token-Session：框架为每个 Token 分配的 Session 对象，也称：令牌Session。 \n- Custom-Session：以一个特定的值作为SessionId，来分配的 Session 对象，也称：自定义Session。\n\n三者差别详见：[Session模型详解](/fun/session-model)\n\n\n#### 账号标识：\n- loginId：账号id，用来区分不同账号，通过 `StpUtil.login(id)` 来指定。\n- device：登录设备类型，例如：`PC`、`APP`，通过 `StpUtil.login(id, device)` 来指定。\n- loginType：账号类型，用来区分不同体系的账号，如同一系统的 `User账号` 和 `Admin账号`，详见：[多账号认证](/up/many-account) \n\n\n#### 几种登录策略：\n- 单地登录：指同一时间只能在一个地方登录，新登录会挤掉旧登录，也可以叫：单端登录。\n- 多地登录：指同一时间可以在不同地方登录，新登录会和旧登录共存，也可以叫：多端登录。\n- 同端互斥登录：在同一类型设备上只允许单地点登录，在不同类型设备上允许同时在线，参考腾讯QQ的登录模式：手机和电脑可以同时在线，但不能两个手机同时在线。\n- 限量登录：限定账号登录设备总数量，低于此数量时可以正常登录，高于此数量后每次登录自动清退一个之前的登录。\n- 记住我模式：指在一个设备终端登录成功，该设备重启之后依然保持登录状态。 \n- 单点登录：在进入多个系统时，只需要登录一次即可。解决用户在不同系统间频繁登录的问题。\n- 同端多登录：指在一个终端可以同时登录多个账号。\n\n\n#### 几种注销策略：\n- 单端注销：只在调用登录的一端注销。\n- 全端注销：一端注销，全端下线。\n- 同端注销：发起注销后，同类型设备端一起下线，不同设备类型不受影响。\n- 单点注销：与单点登录对应，一个系统注销，所有系统一起下线。\n\n<p><a class=\"case-btn case-btn-video\" href=\"https://www.bilibili.com/video/BV1XrzABbEa1/\" target=\"_blank\">\n\t视频讲解：如何设计出最优的登录会话策略？单端登录、强制下线、多端互踢、记住我登录\n</a></p>\n\n\n#### 几种鉴权方式：\n- 代码鉴权：在代码里直接调用 `StpUtil.checkXxx` 相关 API 进行鉴权。\n- 注解鉴权：在方法或类上添加 `@SaCheckXxx` 注解进行鉴权。\n- 路由拦截鉴权：在全局过滤器或拦截里通过：`SaRouter.match()` 拦截路由进行鉴权。\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/more/sa-token-donate-old.md",
    "content": "# 赞助 Sa-Token\n\n--- \n\nSa-Token 采用 Apache-2.0 开源协议，**承诺框架本身与官网文档永久免费开放**，\n但是框架的日常更新与社区运营需要付出大量的精力，靠爱发电难以长久，如果 Sa-Token 帮助到了您，您可以友情支持一下 Sa-Token。\n\n\n### 友情赞助\n\n您可以在项目 [Gitee](https://gitee.com/dromara/sa-token) 主页进行捐赠\n\n<!-- ![gitee-zanzhu2.png](https://oss.dev33.cn/sa-token/doc/gitee-zanzhu2.png) -->\n\n\n\n**已捐赠列表：**\n\n<p class=\"zanzhu-pre\"></p>\n<div class=\"zanzhu-box zanzhu-box-fold\">\n\n| 赞助人\t\t\t\t\t\t\t\t\t\t\t| 赞助金额\t\t| 留言\t\t\t\t\t\t\t\t\t\t| 时间\t\t\t|\n| :--------\t\t\t\t\t\t\t\t\t\t| :--------\t\t| :--------\t\t\t\t\t\t\t\t\t| :--------\t\t|\n| [时间很快](https://gitee.com/frsimple)\t\t\t| ¥ 220.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-10-27 \t|\n| [立秋](https://gitee.com/code_wh)\t\t\t\t| ¥ 2.5\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-10-27 \t|\n| [PotatoLoofah](https://gitee.com/PotatoLoofah)\t| ¥ 10.0\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-10-27 \t|\n| [ly-chn](https://gitee.com/ly-chn)\t\t\t| ¥ 99.0\t\t| 一定的资金支持有助于开源项目走的更加长远\t\t| 2023-10-17 \t|\n| [yangs2w](https://gitee.com/yangs2w)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-10-10 \t|\n| [lee](https://gitee.com/cngeeklee)\t\t\t| ¥ 10.0\t\t| 真正的轻量级权限安全框架，希望继续更新\t\t| 2023-10-06 \t|\n| [yang](https://gitee.com/hansdm)\t\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-09-27 \t|\n| [明道云](https://gitee.com/lunan-yn)\t\t\t| ¥ 200.0\t\t| 明道云2023年伙伴大会，[报名链接](https://www.mingdao.com/event/mpc/2023)\t\t| 2023-09-25 \t|\n| [shenlicao](https://gitee.com/shenlicao)\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-09-15 \t|\n| [lostyue](https://gitee.com/lostyue)\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-09-14 \t|\n| [huni](https://gitee.com/simin_sizi)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-09-11 \t|\n| [T_T](https://gitee.com/wm26hua)\t\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-09-07 \t|\n| [Meteor](https://gitee.com/meteoroc)\t\t\t| ¥ 2.5\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-08-23 \t|\n| [刘斌](https://gitee.com/xuanfather)\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-08-17 \t|\n| [快快乐乐小码农](https://gitee.com/happy-little-farmer)\t| ¥ 1.0\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-08-17 \t|\n| [失败女神](https://gitee.com/failedgoddess)\t| ¥ 50.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-08-03 \t|\n| 结弦奏（微信打赏）\t\t\t\t\t\t\t\t| ¥ 50.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-08-07 \t|\n| [好心肠的老哥](https://gitee.com/ntdm)\t\t\t| ¥ 10.0\t\t| 非常好的开源项目，希望越来越好！\t\t\t\t| 2023-08-02 \t|\n| [XiaoYi](https://gitee.com/getianit)\t\t\t| ¥ 100.0\t\t| [亚洲云深圳BGP云服务器](https://www.asiayun.com/cart?action=configureproduct&pid=300)\t\t| 2023-07-24 \t|\n| [张兆伟](https://gitee.com/zhang865700)\t\t| ¥ 50.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-07-24 \t|\n| [mikeinshanghai](https://gitee.com/mikeinshanghai)| ¥ 50.0\t| Sa-Token, MeterSphere共成长，共辉煌！\t\t| 2023-07-14 \t|\n| 吴其敏（微信打赏）\t\t\t\t\t\t\t\t| ¥ 200.0\t\t| [CAT 是基于 Java 开发的实时应用监控平台，为美团点评提供了全面的实时监控告警服务。](https://github.com/dianping/cat)\t\t| 2023-07-11 \t|\n| [Dear胜哥](https://gitee.com/DearShengGe)\t\t| ¥ 10.0\t\t| 有幸在摸鱼时间认真看完了全文档，感觉很是不错。开源不易，望作者继续扩展该框架功能！\t| 2023-06-30 \t|\n| [SP](https://gitee.com/LSP1999)\t\t\t\t| ¥ 10.0\t\t| 就是需要这种简单上手的项目\t\t\t\t\t| 2023-06-15 \t|\n| [javahuang](https://gitee.com/javahrp)\t\t| ¥ 200.0\t\t| [SurveyKing：功能最强大的调查问卷系统和考试系统，开源](https://gitee.com/surveyking/surveyking)\t\t\t\t\t| 2023-06-08 \t|\n| [dyjgitdyjgit](https://gitee.com/qtinfogit)\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-05-22 \t|\n| 砰嚓嚓（QQ打赏）\t\t\t\t\t\t\t\t| ¥ 20.0\t\t| 一点打赏不成敬意\t\t\t\t\t\t\t| 2023-05-15 \t|\n| [xc_Moving](https://gitee.com/fireZhang)\t\t| ¥ 20.0\t\t| 感谢您的开源项目！感谢SA-token帮我度过项目的难关\t\t| 2023-05-11 \t|\n| [BeckJin](https://gitee.com/beckjin666)\t\t| ¥ 100.0\t\t| [明道云-零代码开发平台，快速响应业务需求。从“IT背锅侠”，变成“IT英雄”。](https://mingdao.com?s=st)\t\t\t\t\t| 2023-05-08 \t|\n| [SummerHy](https://gitee.com/hurumo)\t\t\t| ¥ 10.0\t\t| 国产，就是棒，：）\t\t\t\t\t\t\t| 2023-05-07 \t|\n| [gdl](https://gitee.com/gdl97)\t\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！作者牛逼！\t\t\t\t\t| 2023-04-29 \t|\n| [bootx](https://gitee.com/bootx)\t\t\t\t| ¥ 100.0\t\t| [Bootx-Platform：支付收单、三方对接、后端基于 Spring Boot、Spring Cloud 应用脚手架](https://gitee.com/bootx/bootx-platform)\t| 2023-04-18 \t|\n| c（微信打赏）\t\t\t\t\t\t\t\t\t| ¥ 100.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-04-17 \t|\n| [hurumo](https://gitee.com/hurumo)\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-04-17 \t|\n| [李广龙](https://gitee.com/ak47-b)\t\t\t\t| ¥ 20.0\t\t| 跟大哥学习一辈子学不完\t\t\t\t\t\t| 2023-04-14 \t|\n| [Admin](https://gitee.com/jinan-jimeng-network_0)\t| ¥ 20.0\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-04-12 \t|\n| [王宁波](https://gitee.com/wang-ningbo)\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-04-10 \t|\n| F（微信打赏）\t\t\t\t\t\t\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-04-09 \t|\n| [zhou](https://gitee.com/mrzhou1)\t\t\t\t| ¥ 50.0\t\t| 感谢答疑\t\t\t\t\t\t\t\t\t| 2023-03-29 \t|\n| [Java_小生](https://gitee.com/zhang_hanzhe)\t| ¥ 10.0\t\t| 感谢Sa-Token让我不用去B站肯几十个小时的教程，框架很优秀文档更优秀\t| 2023-03-09 \t|\n| 空空（微信打赏）\t\t\t\t\t\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-03-08 \t|\n| [李一博](https://gitee.com/haust_lyb)\t\t\t| ¥ 8.88\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-03-07 \t|\n| [陈乾](https://gitee.com/qianpou)\t\t\t\t| ¥ 50.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-03-07 \t|\n| [陈乾](https://gitee.com/qianpou)\t\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-03-05 \t|\n| [熊孩子](https://gitee.com/xhz1230)\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-02-17 \t|\n| [不问烟雨](https://gitee.com/xiaominfagui)\t\t| ¥ 10.0\t\t| 牛\t\t\t\t\t\t\t\t\t\t| 2023-01-12 \t|\n| [tsing](https://gitee.com/tsing666)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2023-01-08 \t|\n| [SWmachine](https://gitee.com/SWmachine)\t\t| ¥ 10.0\t\t| 您的开源很好用！\t\t\t\t\t\t\t| 2023-01-07 \t|\n| [Peter Z](https://gitee.com/zj1995)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-12-26 \t|\n| [ken](https://gitee.com/affction)\t\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-12-19 \t|\n| [刘涛](https://gitee.com/doILike)\t\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-12-13 \t|\n| [时间很快](https://gitee.com/frsimple)\t\t\t| ¥ 50.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-11-29 \t|\n| [ThatYear](https://gitee.com/wangmuqing)\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-11-24 \t|\n| [IlovePea](https://gitee.com/IlovePea)\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-11-22 \t|\n| [feel](https://gitee.com/xujiahuim)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-11-17 \t|\n| [laruui](https://gitee.com/laruui)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-10-28 \t|\n| [就眠儀式](https://gitee.com/Jmysy)\t\t\t| ¥ 50.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-10-26 \t|\n| [王文博](https://gitee.com/rl520)\t\t\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-10-24  \t|\n| [feyong](https://gitee.com/feyong)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-10-18 \t|\n| [xueshize](https://gitee.com/xueshize)\t\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-10-12 \t|\n| [西东](https://gitee.com/noear_admin)\t\t\t| ¥ 99.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-10-05 \t|\n| [BlueRose](https://gitee.com/Bluerose_2)\t\t| ¥ 20.0\t\t| 感谢您的付出，项目非常棒！\t\t\t\t\t| 2022-09-22 \t|\n| [邱道长](https://gitee.com/qiudaozhang)\t\t| ¥ 20.0\t\t| 优秀的项目，赞\t\t\t\t\t\t\t\t| 2022-09-09 \t|\n| [jerrydo](https://gitee.com/jerrydo)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！很强大！\t\t\t\t\t| 2022-08-10 \t|\n| [小北宸呀](https://gitee.com/a_aas)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！我就喜欢你这种把我当白痴的官方文档\t\t\t| 2022-07-08 \t|\n| [jwc_gitee](https://gitee.com/jwc-gitee)\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-07-07 \t|\n| [zhihong](https://gitee.com/zzh13520704819)\t| ¥ 20.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-06-20 \t|\n| [风如歌](https://gitee.com/the-wind-is-like-a-song)\t| ¥ 10.0\t| 这个框架简直满足了我所有对于安全框架的需求,赞一个,加油sa-token加油中国开源!\t| 2022-06-17 \t|\n| [qiuyue](https://gitee.com/bmlt)\t\t\t\t| ¥ 10.0\t\t| satoken牛逼\t\t\t\t\t\t\t\t| 2022-06-16 \t|\n| [刘时立](https://gitee.com/liu-shili)\t\t\t| ¥ 10.0\t\t| 非常棒的开源项目!\t\t\t\t\t\t\t| 2022-06-13 \t|\n| [yuncai929](https://gitee.com/null_448_5562)\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-06-10 \t|\n| [sun_2020](https://gitee.com/sun-two-thousand-and-twenty)\t| ¥ 50.0\t| 感谢您的开源项目！\t\t\t\t\t| 2022-06-08 \t|\n| [LZ](https://gitee.com/FUNKBOY)\t\t\t\t| ¥ 6.66\t\t| 感谢您的开源项目！顺便踩一脚Spring Security，sa加油！\t\t| 2022-05-18 \t|\n| [cray](https://gitee.com/hyy6300)\t\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-05-10 \t|\n| [别处理](https://gitee.com/zshnb)\t\t\t\t| ¥ 10.0\t\t| 非常好的项目，希望能一直做下去\t\t\t\t| 2022-05-01 \t|\n| [李洪星](https://gitee.com/li_hong_xing)\t\t| ¥ 10.0\t\t| 解决了很多之前项目中遇到的问题。感谢您的开源项目！\t| 2022-04-29 \t|\n| [乡村阿土哥](https://gitee.com/895995040)\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-04-29 \t\t|\n| [Horatio201](https://gitee.com/horatio201)\t| ¥ 20.0\t\t| 太牛了！\t\t\t\t\t\t\t\t\t| 2022-04-25 \t|\n| [阿文](https://gitee.com/qq921124136)\t\t\t| ¥ 20.0\t\t| 很好的框架，在开发文档里学到了很多知识点\t| 2022-04-21 \t|\n| 行长 （微信打赏）\t\t\t\t\t\t\t\t| ¥ 20.0\t\t| 微信打赏\t\t\t\t\t\t\t\t\t| 2022-04-15\t|\n| [xq584](https://gitee.com/xq584)\t\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-04-08 \t|\n| [yukihane](https://gitee.com/yukihane)\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-04-07 \t|\n| [alkinn](https://gitee.com/alkinn)\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-03-29 \t|\n| [lele](https://gitee.com/lelez)\t\t\t\t| ¥ 10.0\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-03-29  \t|\n| Robin Tin （微信打赏）\t\t\t\t\t\t\t| ¥ 28.88\t\t| 微信打赏\t\t\t\t\t\t\t\t\t| 2022-03-24\t|\n| [刘嘉威](https://gitee.com/liu_jiawei)\t\t\t| ¥ 6.66\t\t| 真滴好用~\t\t\t\t\t\t\t\t\t| 2022-03-23 \t|\n| 秦政 （微信打赏）\t\t\t\t\t\t\t\t| ¥ 20.00\t\t| 微信打赏\t\t\t\t\t\t\t\t\t| 2022-03-22\t|\n| 秦政 （微信打赏）\t\t\t\t\t\t\t\t| ¥ 6.66\t\t| 微信打赏\t\t\t\t\t\t\t\t\t| 2022-03-22\t|\n| 黎子豪 （微信打赏）\t\t\t\t\t\t\t| ¥ 18.88\t\t| 请你喝杯咖啡\t\t\t\t\t\t\t\t| 2022-03-21\t|\n| [Charles7c](https://gitee.com/Charles7c)\t\t| ¥ 20.0\t\t| 感谢您的开源项目！希望 SSO 模块越来越好！\t| 2022-03-17 \t|\n| [晓辉](https://gitee.com/zxhShow)\t\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-03-07 \t|\n| [老杨](https://gitee.com/yangs914)\t\t\t| ¥ 6.66\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-03-01\t|\n| 赵津 （微信打赏）\t\t\t\t\t\t\t\t| ¥ 16.00\t\t| 微信打赏\t\t\t\t\t\t\t\t\t| 2022-02-20\t|\n| [前世男友](https://gitee.com/lanbaba666)\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2022-02-17\t|\n| 两岁 （微信打赏）\t\t\t\t\t\t\t\t| ¥ 188\t\t\t| 微信打赏\t\t\t\t\t\t\t\t\t| 2021-12-27\t|\n| 刚子 （微信打赏）\t\t\t\t\t\t\t\t| ¥ 50\t\t\t| 微信打赏\t\t\t\t\t\t\t\t\t| 2021-12-27\t|\n| [网络小渣渣](https://gitee.com/a9777)\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-12-24\t|\n| [周周周杨](https://gitee.com/ChaoGeWanJiu)\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-12-18\t|\n| [MrXionGe](https://gitee.com/MrXionGe)\t\t| ¥ 10\t\t\t| SA加油~~\t\t\t\t\t\t\t\t\t| 2021-12-17\t|\n| [duyiliu](https://gitee.com/duyiliu)\t\t\t| ¥ 10\t\t\t| 化繁为简，是门艺术。\t\t\t\t\t\t| 2021-12-16\t|\n| [liu](https://gitee.com/liuliuliu123456)\t\t| ¥ 50\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-12-15\t|\n| [fuhouyin](https://gitee.com/fuhouyin)\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-12-01\t|\n| [图灵谷](https://gitee.com/stephenson37)\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-30\t|\n| [luyuan](https://gitee.com/meitesi)\t\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-29\t|\n| [xiaoyan](https://gitee.com/l-yun)\t\t\t| ¥ 200\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-26\t|\n| [yijunzhao](https://gitee.com/yijunzhao)\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-21\t|\n| [万声鹉](https://gitee.com/wanshengwu)\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-15\t|\n| [Taller](https://gitee.com/evilatom)\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-13\t|\n| [公子骏](https://gitee.com/dt_flys)\t\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-08\t|\n| [铂赛东](https://gitee.com/bryan31)\t\t\t| ¥ 20\t\t\t| 开源加油！\t\t\t\t\t\t\t\t\t| 2021-11-08\t|\n| [孔孔的空空](https://gitee.com/kongmr)\t\t\t| ¥ 100\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-11-02\t|\n| [songfazhun](https://gitee.com/fzsong)\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-10-28\t|\n| [ithorns](https://gitee.com/ithorns)\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-10-25\t|\n| [xiaoyan](https://gitee.com/l-yun)\t\t\t| ¥ 200\t\t\t| 节日快乐\t\t\t\t\t\t\t\t\t| 2021-10-24\t|\n| [apifox001](https://gitee.com/apifox001)\t\t| ¥ 200\t\t\t| [Apifox：API 文档、API 调试、API Mock、API 自动化测试](https://apifox.com/)\t| 2021-10-15\t|\n| [永夜](https://gitee.com/cn-src)\t\t\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-09-18 \t|\n| [苏永晓](https://gitee.com/suyongxiao)\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-09-01 \t|\n| [xiaoyan](https://gitee.com/l-yun)\t\t\t| ¥ 200\t\t\t| 好的作者理应被认可\t\t\t\t\t\t\t| 2021-08-24 \t|\n| [xiaoyan](https://gitee.com/l-yun)\t\t\t| ¥ 50\t\t\t| be better\t\t\t\t\t\t\t\t\t| 2021-07-31 \t|\n| [孔孔的空空](https://gitee.com/kongmr)\t\t\t| ¥ 500\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-07-30 \t|\n| [Wizzer](https://gitee.com/wizzer)\t\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-05-22 \t|\n| [二范先生](https://gitee.com/mr-er-fan)\t\t| ¥ 20\t\t\t| 省长加油啊 喝杯茶\t\t\t\t\t\t\t| 2021-03-16 \t|\n| [萧瑟](https://gitee.com/fengduidui)\t\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-03-16 \t|\n| [xue1992wz](https://gitee.com/xue1992wz)\t\t| ¥ 20\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2021-03-16 \t|\n| [whcrow](https://gitee.com/whcrow)\t\t\t| ¥ 20\t\t\t| 军师加油！\t\t\t\t\t\t\t\t\t| 2021-03-16 \t|\n| [RockMan](https://gitee.com/njx33)\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2020-12-17 \t|\n| [zhangjiaxiaozhuo](https://gitee.com/zhangjiaxiaozhuo)| ¥ 10\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2020-12-15 \t|\n| [知知](https://gitee.com/double_zhi)\t\t\t| ¥ 10\t\t\t| 感谢您的开源项目！\t\t\t\t\t\t\t| 2020-12-15 \t|\n| [省长](https://gitee.com/click33)\t\t\t\t| ¥ 10\t\t\t| java中最好用的权限认证框架！\t\t\t\t| 2020-12-15 \t|\n\n</div>\n\n<div class=\"zhankai-btn-box\">\n共 <span class=\"zanzhu-count\">0</span> 位用户赞助，\n<a href=\"javascript: expandZanZhu();\" class=\"zk-btn--1\"> 点击展开 ↓</a>\n<a href=\"javascript: foldZanZhu();\" class=\"zk-btn--2\"> 点击收起 ↑ </a>\n</div>\n\n\n感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ ！\n\n\n\n### 商业赞助\n\n一次性赞助 200 元或以上，可帮助您的产品在 Sa-Token 交流群艾特全体成员推广一次。\n\n并同时在赞助列表处高亮产品链接。\n\nSa-Token 目前总计14+微信交流群（每个群人数大约400以上，总计人数5000+），4个QQ交流群（2000人群和1000人群，总计人数6000+），大部分为 java 开发工程师，可有效推广您的产品。\n\n- 优先推广和程序员相关的互联网产品，比如：低代码开发平台、网课、开发软件、云服务器、个人博客等等，实体产品如键盘、显示器、耳机等等，如果是和程序员无关的产品，可酌情考虑是否推广。\n- 拒绝接受违反法律法规、以及灰色相关的产品推广，为避免不必要的麻烦，目前也拒绝推广IP代理、上网工具等等。\n- 为避免过多打扰群友，目前一天内至多在群里推广两次，超过次数的可顺延到第二天。\n\n有意见合作者可直接在 gitee 发起赞赏后，将您的产品信息发至 [ sa-pro 小助手 ] ，加好友链接点 [这里](https://sa-pro.yun94.cn/)，点击后往下拉有二维码，添加时请备注：Sa-Token 商业赞助。\n\n注：在群发消息时，我们会明确对群友声明，此条消息为赞助商的产品，属于硬广信息。如您介意这一点，我们将暂时无法与您合作。\n\n群发消息类似于以下：\n``` txt\n感谢 xx 老板对 Sa-Token 的商业赞助，以下是老板的产品，大家感兴趣的可以关注一下：\nxxx 商品名称\n链接：https://xxxxxn.com/xxx \n```\n\n> 目前不接受 Sa-Token 官网文档插入广告推广，只接受一次性商业赞助推广。"
  },
  {
    "path": "sa-token-doc/more/sa-token-donate.md",
    "content": "# 赞助 Sa-Token\n\n--- \n\nSa-Token 采用 Apache-2.0 开源协议，<green>**承诺框架本身与在线文档永久免费开放**</green>，\n但是框架的日常更新与社区运营需要付出大量的精力，靠爱发电难以长久，如果 Sa-Token 帮助到了您，您可以友情支持一下 Sa-Token。\n\n\n### 友情赞助\n\n<!-- 您可以在项目 [Gitee](https://gitee.com/dromara/sa-token) 主页进行捐赠\n\n<img src=\"/big-file/doc/more/gitee-zanzhu2.png\" alt=\"gitee-zanzhu2.png\" /> -->\n\n<img src=\"/big-file/contact/wx-zsm.png\" alt=\"微信赞赏码\" style=\"width: 280px;\" />\n\n\n**已捐赠列表：**\n\n<div class=\"zanzhu-box\">\n<div class=\"zanzhu-sort-box\">\n\t<span class=\"zanzhu-sort-btn zz-sort-native\" sort-value=\"1\">日期排序</span>\n\t<span> | </span>\n\t<span class=\"zanzhu-sort-btn\" sort-value=\"2\">赞助额排序</span>\n</div>\n<table class=\"zanzhu-table\" cellspacing=\"0\" border=\"1\" bordercolor=\"e9e9e9\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th>赞助人</th>\n\t\t\t<th>赞助金额</th>\n\t\t\t<th>留言</th>\n\t\t\t<th>时间</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t</tbody>\n</table>\n<!-- 一些按钮 -->\n<div class=\"zz-btn-box\">\n\t<button onclick=\"prevPageRDT()\"> < </button>\n\t<span class=\"zz-pageInfo\">第 1/1 页</span>\n\t<button onclick=\"nextPageRDT()\"> > </button>\n</div>\n</div>\n\n感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ ！\n\n\n\n<!-- ### 商业赞助\n\n一次性赞助 200 元或以上，可帮助您的产品在 Sa-Token 交流群艾特全体成员推广一次。\n\n并同时在赞助列表处高亮产品链接。\n\nSa-Token 目前总计18+微信交流群（每个群人数大约400以上，总计人数7000+），6个QQ交流群（2000人群和1000人群，总计人数8000+），大部分为 java 开发工程师，可有效推广您的产品。\n\n- 优先推广和程序员相关的互联网产品，比如：低代码开发平台、网课、开发软件、个人博客等等，如果是和程序员无关的产品，可酌情考虑是否推广。\n- 拒绝接受违反法律法规、以及灰色相关的产品推广，为避免不必要的麻烦，目前也拒绝推广IP代理、上网工具等等。\n- 为避免过多打扰群友，目前一天内至多在群里推广一次，超过次数的可顺延到第二天。\n\n有意见合作者可直接在 gitee 发起赞赏后，将您的产品信息发至 [ sa-pro 小助手 ] ，加好友链接点 [这里](https://sa-pro.yun94.cn/)，点击后往下拉有二维码，添加时请备注：Sa-Token 商业赞助。\n\n注：在群发消息时，我们会明确对群友声明，此条消息为赞助商的产品，属于硬广信息。如您介意这一点，我们将暂时无法与您合作。\n\n群发消息类似于以下：\n``` txt\n感谢 xx 老板对 Sa-Token 的商业赞助，以下是老板的产品，大家感兴趣的可以关注一下：\nxxx 商品名称\n链接：https://xxxxxn.com/xxx \n```\n\n**其它：**\n- 1、目前不接受 Sa-Token 官网文档插入广告推广，只接受一次性商业赞助推广。\n- 2、助力开源项目：如果您要推广的是一个开源项目，赞助金额可打五折，即：赞助 100 元即可。\n- 3、如果你的开源项目集成了 Sa-Token，可以免费得到推广机会一次。**请大胆联系我们，不要害羞！**\n- 4、此为半公益性推广活动，旨在帮助一些开源项目和IT公司产品在起步阶段快速获取一些用户，我们不打算靠此长久盈利，因此针对同一个产品我们至多接受推广三次，超过三次不再接受推广。\n\n<button class=\"syzz-show-btn\" onclick=\"showSyzz()\">部分推广效果截图(点击查看)</button> -->"
  },
  {
    "path": "sa-token-doc/more/tj-gzh-hz.md",
    "content": "# 公众号合作\n\n--- \n\n### 推荐须知：\nSa-Token作为一个新兴项目，迫切需要一定的途径进行项目推广<br>\n如果您也是java公众号运营者，欢迎您将Sa-Token框架推荐给您的粉丝：\n\n1. 您无需为Sa-Token专门撰写文案，只需要复制项目仓库的 Readme 内容即可，可参考：[链接](https://mp.weixin.qq.com/s/xMCedNj6Nti2BwGzS9A0mg)\n2. 在文章底部或内容中留下项目官网或者GitHub仓库链接\n3. 文章需至少 1000+ 的阅读量\n\n作为回报，Sa-Token将：\n1. 在框架官方文档 [[推荐公众号]](/more/tj-gzh) 处留下您的公众号二维码（按照推荐日期倒叙排列）\n2. 在框架官方交流群里@全体成员推广您的公众号一次\n3. 您的公众号所有新推文章都可以将链接发送到Sa-Token交流群中，增加阅读量（为避免频繁推送连接，请不要超过一周三次）\n\n<br>\n如果您还有除公众号以外的其它途径可以与Sa-Token相互推荐，欢迎加群交流……（群链接在首页）\n\n"
  },
  {
    "path": "sa-token-doc/more/tj-gzh.md",
    "content": "# 推荐公众号\n\n--- \n\n### Java技术公众号：\n\n\n<table class=\"gzh-table\" style=\"text-align: center;\">\n    <tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzg2Mzg0NjEwOA==&mid=2247485024&idx=1&sn=11883396c1844fd2c4bd208c299bb075&send_time=\"/>\n\t\t\t<b>Java大飞哥</b>\n\t\t\t<span>专注软件开发、技术架构设计、源码分享、JAVA技术。</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=Mzg4Nzg2MjQzNg==&mid=2247484545&idx=1&sn=c6101effb31b639fe2699b56bcc090c7&send_time=\"/>\n\t\t\t<b>小简聊开发</b>\n\t\t\t<span>偶尔更新前后端技术文章，偶尔发布生活散文，更新随缘。浏览器搜索JanYork了解我更多。</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU2MTI4MjI0MQ==&mid=2247508620&idx=1&sn=500448fce310a6012aa58616c304dec2&send_time=\"/>\n\t\t\t<b>Java笔记虾</b>\n\t\t\t<span>专注于Java技术栈，推送 Spring全家桶，Dubbo等相关技术知识</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzAxODcyNjEzNQ==&mid=2247541818&idx=2&sn=2b0e7190ed40fa07196de26be51bb432&send_time=\"/>\n\t\t\t<b>程序猿DD</b>\n\t\t\t<span>一线技术工作者的学习、生活与见闻</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU5Mjc5NTIzMA==&mid=2247485007&idx=1&sn=d3bc0cc74a4efbc88b52cc5812c6573a&send_time=\"/>\n\t\t\t<b>TJ君</b>\n\t\t\t<span>一个励志推荐10000款开源项目与免费工具的程序猿</span>\n\t\t</td>\n    </tr>\n    <tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzg2OTA0Njk0OA==&mid=2247511204&idx=2&sn=e1c689f459474fcc1b8ee2ca20a52d3a&send_time=\"/>\n\t\t\t<b>JavaGuide</b>\n\t\t\t<span>专注Java后端学习和大厂面试的公众号！</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI3NzE0NjcwMg==&mid=2650166553&idx=3&sn=c8457e76c3fea17235fb7370cd2279d8&send_time=\"/>\n\t\t\t<b>Hollis</b>\n\t\t\t<span>《Java工程师成神之路》系列作者</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU1Nzg4NjgyMw==&mid=2247493156&idx=1&sn=571db231d274a90ae9a47ac9c4f0a034&send_time=\"/>\n\t\t\t<b>macrozheng</b>\n\t\t\t<span>专注Java技术分享，作者Github开源项目mall（40K+Star）</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU2OTMyMTAxNA==&mid=2247496684&idx=2&sn=be1520743589ca43c129fde828af16ef&send_time=\"/>\n\t\t\t<b>终码一生</b>\n\t\t\t<span>分享Java开发技术（JVM，多线程，高并发，性能调优）</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU4ODI1MjA3NQ==&mid=2247502759&idx=2&sn=87ef078b86f5a0015d97807e76691b09&send_time=\"/>\n\t\t\t<b>CodeSheep</b>\n\t\t\t<span>一只爱技术的程序羊，想把分享变成一种习惯！</span>\n\t\t</td>\n    </tr>\n    <tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzI4Njk5OTg1MA==&mid=2247489188&idx=1&sn=0098f458660d194817d4fc40bca55e4d&send_time=\"/>\n\t\t\t<b>Java开发宝典</b>\n\t\t\t<span>分享Java基础、Java框架、数据库、微服务、中间件、分布式、架构等技术干货</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIwODkzOTc1MQ==&mid=2247489717&idx=1&sn=1c295f070123c84ee3791ac4d4898ae9&send_time=\"/>\n\t\t\t<b>MarkerHub</b>\n\t\t\t<span>专注于梳理java知识，解析开源项目。 </span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzI3MDE0NzYwNA==&mid=2651444525&idx=3&sn=e266f94ada851a5ba6a61c1d71e9a62f&send_time=\"/>\n\t\t\t<b>架构师必备</b>\n\t\t\t<span>分享干货文章，做一个有逼格的架构师社区！ </span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzk0MjE5MTk5Mw==&mid=2247485576&idx=1&sn=d955d60ab193d895fbd15c37bedebcb8&send_time=\"/>\n\t\t\t<b>Github导航站</b>\n\t\t\t<span>分享好玩有趣、新奇、实用的开源项目 </span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI1MTA3Mzk4Mg==&mid=2651026326&idx=1&sn=3244e18a9a799cd8e0ec375c6c730670&send_time=\"/>\n\t\t\t<b>Java爱好者</b>\n\t\t\t<span>分享Java开发编程资源和Java技术文章 </span>\n\t\t</td>\n    </tr>\n    <tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIxODQxMjc0MA==&mid=2247511029&idx=1&sn=42fbb1b8180dc2c995429dabc6f97500&send_time=\"/>\n\t\t\t<b>IT大咖说</b>\n\t\t\t<span>大咖干货，不再错过。 </span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIxNjA5MTM2MA==&mid=2652444329&idx=1&sn=2a040e87f49a977a116c90e1107a9fdc&send_time=\"/>\n\t\t\t<b>Java编程</b>\n\t\t\t<span>专注Java技术分享，Java基础知识/数据结构/算法</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzA4MDMyODg4OQ==&mid=2649482871&idx=2&sn=b376585faaf814d9072af539efda68fe&send_time=\"/>\n\t\t\t<b>大侠学JAVA</b>\n\t\t\t<span>道阻且长，行则将至，专注分享JAVA领域的干货</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzg2MDIxNjAzNg==&mid=2247485810&idx=1&sn=afd46d5924afbc1030a87b5d56265fdf&send_time=\"/>\n\t\t\t<b>Java大厂面试官</b>\n\t\t\t<span>一名一直奋斗在一线的程序员, 记录工作用遇到的问题和解决方案</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI2ODQwMTI0MA==&mid=2247486194&idx=2&sn=3fef462f77bdf0354a223e2e6d9d9136&send_time=\"/>\n\t\t\t<b>程序员大神</b>\n\t\t\t<span>有一个在程序员圈混了10年的老程序员, 分享程序员相关的精选内容教程</span>\n\t\t</td>\n    </tr>\n    <tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzUyMDk4OTU5OA==&mid=2247499610&idx=6&sn=bc12403d060cba70be5739b808057d14&send_time=\"/>\n\t\t\t<b>码农学习联盟</b>\n\t\t\t<span>码农学习联盟，程序员码农学习第一站！Java、Python、大数据、机器学习、人工智能</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MjM5NzA1MTcyMA==&mid=2651175957&idx=3&sn=93343b003a8a3c2c600b909499cb554a&send_time=\"/>\n\t\t\t<b>程序猿</b>\n\t\t\t<span>传播编程经验，挖掘程序员优秀的学习资源。</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI3MDE0NzYwNA==&mid=2651443022&idx=2&sn=65db50021c95493d2df03dc58f6dae49&send_time=\"/>\n\t\t\t<b>架构师必备</b>\n\t\t\t<span>分享干货文章，做一个有逼格的架构师社区！</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI4MjI1MTI0Mw==&mid=2247493681&idx=1&sn=4257550a48bf3dfc5c312b0750171c61&send_time=\"/>\n\t\t\t<b>GitHuboy</b>\n\t\t\t<span>专注于分享 Python、Java、Web、AI、数据分析等多个领域的优质学习资源</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzA3NzA2MDMyNA==&mid=2650359032&idx=1&sn=79eea2f0cb16d5054bbb731885c310e5&send_time=\"/>\n\t\t\t<b>开源最前线</b>\n\t\t\t<span>推荐热门开源软件，介绍新开源项目，报导开源资讯！</span>\n\t\t</td>\n    </tr>\n    <tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzUzNTY2NjAzMg==&mid=2247484321&idx=1&sn=52e7e5e0dc03437e94908b6a67985500&send_time=\"/>\n\t\t\t<b>Dromara开源组织</b>\n\t\t\t<span>Dromara开源组织官方公众号</span><br>\n\t\t\t<a href=\"https://gitee.com/dromara\" target=\"_blank\">Gitee</a>\n\t\t\t<a href=\"https://github.com/dromara\" target=\"_blank\">GitHub</a>\n\t\t\t<a href=\"https://dromara.org/zh/projects/\" target=\"_blank\">官网</a>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzAxNjk4ODE4OQ==&mid=2247503088&idx=3&sn=f7e82b05d8f155b1fa79601393c437dc&send_time=\"/>\n\t\t\t<b>方志朋</b>\n\t\t\t<span>主要分享Java、Python等技术，用大厂程序员的视角来探讨技术进阶、面试指南、职业规划等。</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzkyNzExODM3OA==&mid=2247485166&idx=1&sn=fe7ff42336d050a7fbbe6b06fdd8c3ec&send_time=\"/>\n\t\t\t<b>Java仓库</b>\n\t\t\t<span>专注Java全栈开发，分享实用技术干货。</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzU3MDc3OTI1NA==&mid=2247490668&idx=1&sn=cd9efecdf1ac34cc8cac04902a9f8319&send_time=\"/>\n\t\t\t<b>Java技术江湖</b>\n\t\t\t<span>一位阿里 Java 工程师的技术小站, 分享技术干货和学习经验</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MjM5MTM0NjQ2MQ==&mid=2650152326&idx=2&sn=62643fd0987a56095663b12a2ec622c5&send_time=\"/>\n\t\t\t<b>java那些事</b>\n\t\t\t<span>分享java中各种新技术的应用方法，做一个潮流的java技术人！</span>\n\t\t</td>\n    </tr>\n    <tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzIwNTk5NjEzNw==&mid=2247494012&idx=1&sn=378001dabae76b2df4de9a0dadf5842d&send_time=\"/>\n\t\t\t<b>Java研发军团</b>\n\t\t\t<span>Java系列文章个人博客，MySQL、SSM、Redis、Spring</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzIwMTY0NDU3Nw==&mid=2651952104&idx=1&sn=315a840285b4f5b243d68e31cd0f2008&send_time=\"/>\n\t\t\t<b>Java团长</b>\n\t\t\t<span>分享些技术干货，致力于Java全栈开发！</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzAwMjk5Mjk3Mw==&mid=2247496042&idx=1&sn=3c246a6feea74d24b92d49b564509fe8&send_time=\"/>\n\t\t\t<b>武哥聊编程</b>\n\t\t\t<span>你若对得起时间，时间便对得起你~ 我是武哥！谢谢你的关注~每天进步一点点！</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzU2MTI4MjI0MQ==&mid=2247501218&idx=3&sn=599c40e5cd1acf597c8f392f1c5bd150&send_time=\"/>\n\t\t\t<b>Java笔记虾</b>\n\t\t\t<span>专注于Java技术栈，推送 Spring全家桶，Dubbo，Zookeeper，Redis，Linux，多线程</span>\n\t\t</td>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI1MDQwMDE3MQ==&mid=2247489606&idx=1&sn=d215fce776c5f56fc439cfdc024de504&send_time=\"/>\n\t\t\t<b>Java项目学习</b>\n\t\t\t<span>关注我，我来带你从零开始做Java项目！</span>\n\t\t</td>\n    </tr>\n\t<tr>\n        <td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIwNjg4MzY4NA==&mid=2247490744&idx=2&sn=2cea21ce873cae0cc4a1c5097265e678&send_time=\"/>\n\t\t\t<b>程序员追风</b>\n\t\t\t<span>专注于分享Java各类学习笔记、面试题以及IT类资讯。</span>\n\t\t</td>\n\t\t<td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzU2OTYxNjk0Mg==&mid=2247489042&idx=1&sn=3526217770b7ec0661dc53b18eb98500&send_time=\"/>\n\t\t\t<b>程序员编程</b>\n\t\t\t<span>每天下午13：30分发文，主要发布开源项目、面试题、最新技术资讯、干货学习资源～</span>\n\t\t</td>\n\t\t<td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzUzMDE4NjE4Mg==&mid=2247486739&idx=1&sn=339aed2b872c5c2f291e83b7ab107b4d&send_time=\"/>\n\t\t\t<b>写代码的渣渣鹏</b>\n\t\t\t<span>关注我，学好Java。Spring Boot、 微服务、高并发、多线程、JVM、Spring Cloud</span>\n\t\t</td>\n\t\t<td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzA3MzE4ODY0Mg==&mid=2455988316&idx=1&sn=6d70a967f1ff757cdda320472c1deb87&send_time=\"/>\n\t\t\t<b>GitHub精选</b>\n\t\t\t<span>专注于分享优质的开源项目、学习资源，Java、Python、Go、Web 前端、AI、数据分析</span>\n\t\t</td>\n\t\t<td>\n\t\t\t<img src=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzA5MzYyNzQ0MQ==&mid=2247498873&idx=1&sn=8cb67e3057f81fc68ec5fd407076c7eb&send_time=\"/>\n\t\t\t<b>HelloGitHub</b>\n\t\t\t<span>分享 GitHub 上有趣、入门级的开源项目。</span>\n\t\t</td>\n\t</tr>\n</table>\n\n<br>\n\n感谢以上公众号对 Sa-Token 项目的推荐<!-- ，如果您也是java公众号运营者，欢迎 [相互推荐](/more/tj-gzh-hz) -->\n\n"
  },
  {
    "path": "sa-token-doc/more/update-log.md",
    "content": "# 更新日志 \n\n### v1.45.0 @2026-3-8\n- core：\n\t- 新增：新增重复登录处理策略，当同一账号不允许多客户端同时登录时支持选择踢人下线或拦截本次登录。  **[重要]** merge: [pr 349](https://gitee.com/dromara/sa-token/pulls/349)\n\t- 修复：修复 `StpUtil.getLoginIdByTokenNotThinkFreeze` 方法缺少 `static` 修饰符的问题。\n\t- 优化：优化路由匹配 pattern 缓存算法，消除魔法值。merge: [pr 907](https://github.com/dromara/Sa-Token/pulls/907)\n\t- 优化：移除冗余导包。\n- 插件：\n\t- 新增：新增 `sa-token-jackson3` 插件，用于 Jackson 3 的 JSON 操作。  **[重要]**\n\t- 新增：新增 `sa-token-jackson3-test` 单元测试。\n\t- 新增：新增 `sa-token-snack4` 插件。  **[重要]** merge: [pr 356](https://gitee.com/dromara/sa-token/pulls/356)\n\t- 修复：修复 Dubbo 上下文清理问题。 merge: [pr 889](https://github.com/dromara/Sa-Token/pulls/889)\n\t- 新增：loveqq-framework 版本更新。merge: [pr 351](https://gitee.com/dromara/sa-token/pulls/351)\n- starter：\n\t- 新增：新增 `sa-token-spring-boot4-starter` 集成包，支持 Spring Boot 4 环境集成。  **[重要]**\n\t- 新增：新增 `sa-token-reactor-spring-boot4-starter` 集成包，支持 Reactor + Spring Boot 4 环境集成。  **[重要]**\n\t- 新增：新增 `sa-token-demo-springboot4`、`sa-token-demo-webflux-springboot4` 示例。\n\t- 新增：新增 Spring Boot 4 整合 demo 示例。\n- 重构：\n\t- 重构：重构 `sa-token-dependencies` 相关模块，优化依赖关系。  **[重要]**\n\t- 重构：重构 Spring Boot WebMVC/Reactor 相关集成包，优化依赖关系。 **[重要]**\n\t- 优化：优化整体模块依赖关系。\n- Solon：\n\t- 优化：`sa-token-solon-plugin` 优化 Gateway 接口的处理，避免使用路由接口。merge: [pr 348](https://gitee.com/dromara/sa-token/pulls/348)\n- SSO：\n\t- 新增：sso-server 前后端分离模式下 平台中心模式 demo 示例。\n\t- 修复：SSO 模块 msgType 参数说明、API 说明修正。\n\t- 新增：SSO 模块视频讲解链接：B站 王清江唷 SSO篇（29集）。  **[重要]**\n\t- 补全：SSO 模块内置消息处理器相关文档。\n\t- 新增：文档为 `sa-token-sso` 模块定义 STS 协议。  **[重要]**\n- OAuth2：\n\t- 修复：修复 `sa-token-oauth2` 组件使用 `sa-token-fastjson2` 序列化导致的类型转换问题。merge: [pr 355](https://gitee.com/dromara/sa-token/pulls/355)\n\t- 优化：修改 `ClientIdSecretModel` 的读取构建逻辑。merge: [pr 346](https://gitee.com/dromara/sa-token/pulls/346)\n- 文档：\n\t- 同步：同步公众号文章列表、博客列表、赞助者名单、企业登记案例。\n\t- 新增：新增 Sa-Token 内容合作者群。  **[重要]**\n\t- 新增：新增《Gitee 2025 年度开源项目 Web 应用开发 Top 2》证书展示。\n\t- 新增：新增赞赏码展示、文档首页 stars 对比图。\n\t- 新增：新增解决跨域专题文章。\n\t- 新增：增加微信群聊信息展示。\n\t- 优化：优化框架 Slogan。\n\t- 优化：优化 README、案例库展示。\n\t- 优化：文档主题切换增加水滴特效，调整主题色块顺序。\n\t- 优化：文档优化 [登录认证]、[权限认证]、[路由拦截鉴权] 篇。\n\t- 优化：补全全局策略说明、数据结构说明。\n\t- 新增：目录树增加专门栏目记录项目架构设计。\n\t- 优化：功能结构图增加点击事件跳转到对应功能文档。\n\t- 优化：子服务外网隔离章节增加示意图。\n\t- 优化：Same-Token 同源系统认证图示说明。\n\t- 修复：更换 GitCode logo 为 AtomGit。\n\t- 修复：更换 QQ 群链接、微信群聊展示图。\n\t- 修复：文档图片地址更换为本地文件。\n\t- 修复：错别字修复。\n\t- 修复：maven-pull.md 文档，解决父子项目依赖下载问题。\n\t- 新增：Maven 父子项目无法下载依赖的问题解决方案。merge: [pr 358](https://gitee.com/dromara/sa-token/pulls/358)\n\t- 修复：订正文档错别字。merge: [pr 354](https://gitee.com/dromara/sa-token/pulls/354)\n\t- 修复：文档内代码示例修正。merge: [pr 347](https://gitee.com/dromara/sa-token/pulls/347)\n- AI: \n\t- 新增：新增 organize-update-log SKILL，用于格式化整理版本更新日志信息。\n\t- 新增：新增 commit-message SKILL，用于整理 git commit 日志信息。\n\t- 新增：新增 upgrade-version SKILL，用于统一升级修改版本号。\n\t- 新增：新增 remove-redundancy-import SKILL，用于检查 Java 类中无效冗余导包并移除。\n- 其它：\n\t- 新增：readme 增加快问快答区域。\n\t- 新增：增加忽略 .vscode 目录。\n\t- 优化：注释优化。\n\t- 重构：备忘录重构为专门的文件夹。\n\t- 重构：调整项目发布配置至 Maven Central Portal。merge: [pr 792](https://github.com/dromara/Sa-Token/pull/792)\n\t- 优化：部分构建配置升级到最新版。\n\n\n\n\n\n\n### v1.44.0 @2025-6-7\n\n- 修复：修复 sso-server 前后端分离示例无法正常登录的问题。\n- 修复：修复 SSO 模式三全端注销失效的问题。\n- 修复：修复 SSO `SaSsoClientModel` 部分场景下无法序列化的问题。\n- 新增：OAuth2 模块新增支持从 `SaOAuth2DataLoader` 接口获取高级权限与低级权限的方法。merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/339) \n- 修复：修复 `sa-token-dubbo` 与 `sa-token-dubbo3` 每次调用都强制需要上下文的问题。\n- 文档：新增 `sa-token-dubbo3` 的说明。\n- 文档：更新赞助者名单。\n- 文档：新增 `loveqq-framework` 框架集成包。 **[重要]**  merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/340) \n\n\n\n### v1.43.0 @2025-5-17\n- core: \n\t- 新增：`SaLogoutParameter` 新增 `deviceId` 参数，用于控制指定设备 id 的注销。  **[重要]**\n\t- 新增：新增 `SaHttpTemplate` 请求处理器模块。\n\t- 新增：TOTP 增加 `issuer` 字段。  merge: [pr 329](https://gitee.com/dromara/sa-token/pulls/329) \n\t- 修复：修复 `Http Digest` 认证时 url 上带有查询参数时认证无法通过的问题。merge: [pr 334](https://gitee.com/dromara/sa-token/pulls/334) \n\t- 新增：@SaCheckOr 注解添加 `append` 字段，用于抓取未预先定义的注解类型进行批量注解鉴权。\n\t- 新增：侦听器 `doRenewTimeout` 方法添加 loginType 参数。\n\t- 新增：`SaInterceptor` 新增 `beforeAuth` 认证前置函数。\n- SSO：\n\t- 新增：单点注销支持单设备注销。   **[重要]**  fix: [#IA6ZK0](https://gitee.com/dromara/sa-token/issues/IA6ZK0) 、[#747](https://github.com/dromara/Sa-Token/issues/747)\n\t- 新增：新增消息推送机制。  **[重要]**   fix: [#IBGXA7](https://gitee.com/dromara/sa-token/issues/IBGXA7) \n\t- 新增：配置项 clients 用于单独配置每个 client 的授权信息。  **[重要]** \n\t- 新增：配置项 `allowAnonClient` 决定是否启用匿名 client。\n\t- 新增：SSO 模块新增配置文件方式启用“不同 client 不同秘钥”能力。\n\t- 重构：sso-client 封装化获取 client 标识值。\n\t- 新增：新增 SSO Strategy 策略类。\n\t- 新增：sso-client 新增 `convertCenterIdToLoginId`、`convertLoginIdToCenterId` 策略函数，用于描述本地 LoginId 与认证中心 loginId 的转换规则。\n\t- 新增：sso-server 新增 `jumpToRedirectUrlNotice` 策略，用于授权重定向跳转之前的通知。\n\t- 优化：调整整体 SSO 示例代码。\n\t- 新增：新增 ReSdk 模式对接示例：`sa-token-demo-sso3-client-resdk`。  **[重要]** \n\t- 新增：新增匿名应用模式对接示例：`sa-token-demo-sso3-client-anon`。  **[重要]** \n- OAuth2：\n\t- 新增：`SaClientModel` 新增 `isAutoConfirm` 配置项，用于决定是否允许应用可以自动确认授权。 **[重要]** \n\t- 新增：多 `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) \n\t- 新增：Scope 分割符支持加号。merge: [pr 333](https://gitee.com/dromara/sa-token/pulls/333) \n\t- 修复：修复 oidc 协议下，当用户数据变动后，id_token 仍是旧信息的问题。\n\t- 优化：对 `OAuth2 Password` 认证模式需要重写处理器添加强提醒。\n\t- 优化：将认证流程回调从 `SaOAuth2ServerConfig` 转移到 `SaOAuth2Strategy`。\n\t- 新增：新增 `SaOAuth2Strategy.instance.userAuthorizeClientCheck` 策略，用于检查指定用户是否可以授权指定应用。fix: [#553](https://github.com/dromara/Sa-Token/issues/553) \n\t- 优化：优化调整 `sa-token-oauth2` 模块代码结构及注释。\n\t- 新增：`currentAccessToken()`、`currentClientToken()`，简化读取 `access_token`、`client_token` 步骤\n- 插件：\n\t- 新增：新增 `sa-token-forest` 插件，用于在 Http 请求处理器模块整合 Forest。\n\t- 新增：新增 `sa-token-okhttps` 插件，用于在 Http 请求处理器模块整合 OkHttps。\n\t- 拆分：API Key 模块拆分独立插件包：`sa-token-apikey`。\n\t- 拆分：API Sign 模块拆分独立插件包：`sa-token-sign`。\n\t- 修复：修复 `sa-token-dubbo` 插件部分场景上下文控制出错的问题。\n\t- 修复：修复 `sa-token-sanck3` `SaSessionForSnack3Customized:getModel` 接收 map 值时会出错的问题。 merge: [pr 330](https://gitee.com/dromara/sa-token/pulls/330) \n\t- 修复：修复使用 `sa-token-redis-template-jdk-serializer` 时反序列化错误。merge: [pr 331](https://gitee.com/dromara/sa-token/pulls/331) \n\t- 修复：`sa-token-snack3` 优化 `objectToJson` 序列化处理（增加类名，但不增加根类名）。\n\t- 重构：重构 `sa-token-redis-template`、`sa-token-redis-template-jdk-serializer` 插件中 update 方法 ttl 获取方式改为毫秒，以减少 update 时的 ttl 计算误差。  **[重要]** \n- 示例：\n\t- 新增：新增 SSE 鉴权示例。\n- 文档：\n\t- 新增：新增文档离线版下载。\n\t- 新增：新增框架功能列表插图。\n\t- 新增：新增示例：如何在响应式环境下的 Filter 里调用 Sa-Token 同步 API。\n\t- 新增：新增 QA：在 idea 导入源码，运行报错：java: 程序包cn.dev33.satoken.oauth2不存在。\n\t- 新增：新增 QA：新增QA：报错：SaTokenContext 上下文尚未初始化。\n\t- 新增：新增 QA：在 idea 导入源码，运行报错：java: 程序包cn.dev33.satoken.oauth2不存在。\n\t- 新增：重写路由匹配算法修正为最新写法。\n\t- 新增：修复 OAuth2 UnionId 章节相关不正确描述。\n\t- 优化：完善 QA：访问了一个不存在的路由，报错：SaTokenContext 上下文尚未初始化。   fix: [#771](https://github.com/dromara/Sa-Token/issues/771)\n\t- 优化：补充 sso 模块遗漏的配置字段介绍。\n\t- 优化：OAuth2-Server 示例添加真正表单。\n\t- 新增：文档新增重写 `PasswordGrantTypeHandler` 处理器示例。\n\t- 新增：sso 章节和 oauth2 章节文档增加可重写策略说明。\n- 其它：\n\t- 新增：readme 新增框架功能介绍图。\n\t- 新增：SSO 模块新增思维导图说明。\n\t- 新增：readme 新增 Forest 的友情链接。\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\n\n### v1.42.0 @2025-4-11\n更新导读：[视频](https://www.bilibili.com/video/BV1h85izzEe8/)、[文字版](https://juejin.cn/post/7491971657201451062)\n\n- core: \n\t- 新增: 新增 `API Key` 模块。   **[重要]**\n\t- 新增: 新增 `TOTP` 实现。   **[重要]**\n\t- 重构：重构 `TempToken` 模块，新增 value 反查 token 机制。   **[重要]**\n\t- 升级: 重构升级 `SaTokenContext` 上下文读写策略。   **[重要]**\n\t- 新增: 新增 Mock 上下文模块。   **[重要]**\n\t- 删除: 删除二级上下文模块。\n\t- 新增: 新增异步场景使用 demo。   **[重要]**\n\t- 新增: 新增 `Base32` 编码工具类。\n\t- 新增：新增 `CORS` 跨域策略处理函数，提供不同架构下统一的跨域处理方案。\n\t- 新增：`renewTimeout` 续期方法增加 token 终端信息有效性校验。\n\t- 新增: 全局配置项 `cookieAutoFillPrefix`：cookie 模式是否自动填充 token 前缀。\n\t- 新增: 全局配置项 `rightNowCreateTokenSession`：在登录时，是否立即创建对应的 `Token-Session`。\n\t- 优化：优化 `Token-Session` 获取算法，减少缓存读取次数。\n\t- 新增：`SaLoginParameter` 支持配置 `SaCookieConfig`，以配置 Cookie 相关参数。\n\t- 修改：防火墙校验过滤器的注册顺序 修改为 -102。\n\t- 新增：防火墙 `hook` 注册新增 `registerHookToFirst`、`registerHookToSecond` 方法，以便更灵活的控制 hook 顺序。\n- 插件：\n\t- 新增: `sa-token-quick-login` 插件支持 `Http Basic` 方式通过认证。\n- 单元测试：\n\t- 补全：补全 `Temp Token` 模块单元测试。\n- 文档：\n\t- 补全：补全赞助者名单。\n\t- 修复：修复 `Thymeleaf` 集成文档不正确的依赖示例说明。\n\t- 修复：修复 `unionid` 章节错误描述。\n\t- 优化：采用更细致的描述优化SSO模式三单点注销步骤。\n\t- 新增：登录认证文档添加 Cookie 查看步骤演示图。\n\t- 新增：多账号模式新增注意点：运行时不可更改 `LoginType`。\n\t- 新增: 多账号模式QA：在一个接口里获取是哪个体系的账号正在登录。\n\t- 新增：新增QA：解决低版本 `SpringBoot (<2.2.0)` 引入 Sa-Token 报错的问题。\n\t- 新增：新增QA：前后端一体项目下，在拦截未登录进入登录页面时，如何登录完成后原路返回？\n\t- 新增：新增QA：Sa-Token 集成 Redis 如何集群？\n\t- 新增：新增QA：如何自定义框架读取 token 的方式？\n\t- 新增：新增QA：修改 `hosts` 文件无效可能原因排查。\n\t- 新增：新增QA：如何防止 CSRF 攻击。\n\t- 新增: “异步 & Mock 上下文” 章节。\n\t- 升级：升级“自定义 SaTokenContext 指南”章节文档。\n\n\n\n### v1.41.0 @2025-3-21\n更新导读：[视频](https://www.bilibili.com/video/BV1aNo4YCEM1/)、[文字版](https://juejin.cn/post/7484191942358499368)\n\n- core: \n\t- 修复：修复 `StpUtil.setTokenValue(\"xxx\")`、`loginParameter.getIsWriteHeader()` 空指针的问题。 fix: [#IBKSM0](https://gitee.com/dromara/sa-token/issues/IBKSM0)\n\t- 修复：将 `SaDisableWrapperInfo.createNotDisabled()` 默认返回值封禁等级改为 -2，以保证向之前版本兼容。\n\t- 新增：新增基于 SPI 的插件体系。   **[重要]** \n\t- 重构：JSON 转换器模块。   **[重要]** \n\t- 新增：新增 serializer 序列化模块，控制 `Object` 与 `String` 的序列化方式。   **[重要]** \n\t- 重构：重构防火墙模块，增加 hooks 机制。   **[重要]** \n\t- 新增：防火墙新增：请求 path 禁止字符校验、Host 检测、请求 Method 检测、请求头检测、请求参数检测。重构目录遍历符检测算法。\n\t- 重构：重构 `SaTokenDao` 模块，将序列化与存储操作分离。   **[重要]**\n\t- 重构：重构 `SaTokenDao` 默认实现类，优化底层设计。\n\t- 新增：`isLastingCookie` 配置项支持在全局配置中定义了。\n\t- 重构：`SaLoginModel` -> `SaLoginParameter`。    **[不向下兼容]** \n\t- 重构：`TokenSign` -> `SaTerminalInfo`。    **[不向下兼容]** \n\t- 新增：`SaTerminalInfo` 新增 `extraData` 自定义扩展数据设置。\n\t- 新增：`SaLoginParameter` 支持配置 `isConcurrent`、`isShare`、`maxLoginCount`、`maxTryTimes`。\n\t- 新增：新增 `SaLogoutParameter`，用于控制注销会话时的各种细节。  **[重要]**\n\t- 新增：新增 `StpLogic#isTrustDeviceId` 方法，用于判断指定设备是否为可信任设备。\n\t- 新增：新增 `StpUtil.getTerminalListByLoginId(loginId)`、`StpUtil.forEachTerminalList(loginId)` 方法，以更方便的实现单账号会话管理。\n\t- 升级：API 参数签名配置支持自定义摘要算法。\n\t- 新增：新增 `@SaCheckSign` 注解鉴权，用于 API 签名参数校验。\n\t- 新增：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)   **[重要]**\n\t- 重构：全局配置 `is-share` 默认值改为 false。    **[不向下兼容]** \n\t- 重构：踢人下线、顶人下线默认将删除对应的 token-session 对象。\n\t- 优化：优化注销会话相关 API。\n\t- 重构：登录默认设备类型值改为 DEF。   **[不向下兼容]** \n\t- 重构：`BCrypt` 标注为 `@Deprecated`。\n\t- 新增：`sa-token-quick-login` 支持 `SpringBoot3` 项目。 fix: [#IAFQNE](https://gitee.com/dromara/sa-token/issues/IAFQNE)、[#673](https://github.com/dromara/Sa-Token/issues/673)\n\t- 新增：`SaTokenConfig` 新增 `replacedRange`、`overflowLogoutMode`、`logoutRange`、`isLogoutKeepFreezeOps`、``isLogoutKeepTokenSession`` 配置项。\n- OAuth2：\n\t- 重构：重构 sa-token-oauth2 插件，使注解鉴权处理器的注册过程改为 SPI 插件加载。\n- 插件：\n\t- 新增：`sa-token-serializer-features` 插件，用于实现各种形式的自定义字符集序列化方案。\n\t- 新增：`sa-token-fastjson` 插件。\n\t- 新增：`sa-token-fastjson2` 插件。\n\t- 新增：`sa-token-snack3` 插件。\n\t- 新增：`sa-token-caffeine` 插件。\n- 单元测试：\n\t- 新增：`sa-token-json-test` json 模块单元测试。\n\t- 新增：`sa-token-serializer-test` 序列化模块单元测试。\n- 文档：\n\t- 新增：QA “多个项目共用同一个 redis，怎么防止冲突？” \n\t- 优化：补全 OAuth2 模块遗漏的相关配置项。\n\t- 优化：优化 OAuth2 简述章节描述文档。\n\t- 优化：完善 “SSO 用户数据同步 / 迁移” 章节文档。\n\t- 修正：补全项目目录结构介绍文档。\n\t- 新增：文档新增 “登录参数 & 注销参数” 章节。\n\t- 优化：优化“技术求助”按钮的提示文字。\n\t- 新增：新增 `preview-doc.bat` 文件，一键启动文档预览。\n\t- 完善：完善 Redis 集成文档。\n\t- 新增：新增单账号会话查询的操作示例。\n\t- 新增：新增顶人下线 API 介绍。\n\t- 新增：新增 自定义序列化插件 章节。\n- 其它：\n\t- 新增：新增 `sa-token-demo/pom.xml` 以便在 idea 中一键导入所有 demo 项目。\n\t- 删除：删除不必要的 `.gitignore` 文件\n\t- 重构：重构 `sa-token-solon-plugin` 插件。\n\t- 新增：新增设备锁登录示例。\n\n\n### v1.40.0 @2025-2-1\n更新导读：[视频](https://www.bilibili.com/video/BV1uNATeeEvg/)、[文字版](https://juejin.cn/post/7467969744307306505)\n\n- core: \n\t- 新增：新增 `Cookie` 自定义属性支持。  fix: [#693](https://github.com/dromara/Sa-Token/issues/693)   **[重要]** \n\t- 新增：`SaFirewallStrategy` 防火墙策略：请求 path 黑名单校验、非法字符校验、白名单放行。  **[重要]** \n\t- 修复：新增对分号字符的 path 路径校验。   参考：[Sa-Token对url过滤不全存在的风险点](https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA)   **[漏洞修复]** \n\t- 修复: 修复部分场景下登录后已存在的 `token-session` 没有被续期的问题。  fix: [#IA8U1O](https://gitee.com/dromara/sa-token/issues/IA8U1O)\n\t- 优化：优化 `active-timeout` 的检查与续期操作，同一请求内只会检查与续期一次。\n\t- 修复：`SaFoxUtil.joinSharpParam` 方法中不正确的注释。\n\t- 新增：封禁模块新增支持实时从数据库查询数据。\n- SSO：\n\t- 优化：SSO 示例代码的跨域处理由原生方式改为 Sa-Token 过滤器模式。\n\t- 新增：文档新增 “SSO整合 - NoSdk 模式与非 java 项目” 章节。\n\t- 新增：“不同 SSO Client 配置不同秘钥” 章节增加部分异常的处理方案提示，fix: [#IAFZXL](https://gitee.com/dromara/sa-token/issues/IAFZXL)\n\t- 删除：sso demo 示例中部分不必要的代码内容。\n- OAuth2：\n\t- 新增：OAuth2 Client 前端测试页。   **[重要]**\n\t- 新增：`UnionId` 联合id 实现。   **[重要]** \n\t- 新增：`oauth2-server` 端前后台分离示例与文档。 fix: [#I9DQGA](https://gitee.com/dromara/sa-token/issues/I9DQGA)、[#I9W2RU](https://gitee.com/dromara/sa-token/issues/I9W2RU)    **[重要]**\n\t- 新增：`OIDC` 模式 `nonce` 随机数响应校验。 merge: [pr311](https://gitee.com/dromara/sa-token/pulls/311)\n\t- 修复：错误方法名 `deleteGrantScope(String state)` -> `deleteState(String state)`。\n\t- 修复：全局配置项 `sa-token.oauth2-server.oidc.iss` 无效的问题。\n\t- 新增：回收 Refresh-Token 方法: `revokeRefreshToken`、`revokeRefreshTokenByIndex`。\n\t- 新增：为 `CodeModel`、`AccessTokenModel`、`RefreshTokenModel`、`ClientTokenModel` 添加 `createTime` 字段，以记录该数据的创建时间。\n\t- 新增：为 Access-Token、Client-Token 添加 `grantType` 字段，以记录该数据的授权类型。\n\t- 新增：`SaOAuth2Util.getCode` 等方法，以更方便的获取、校验授权码。\n- 插件：\n\t- 新增：新增 `sa-token-freemarker` 插件，整合 `Freemarker` 视图引擎。 fix: [#651](https://github.com/dromara/sa-token/issues/651)   **[重要]**\n\t- 新增：新增 `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)   **[重要]**\n- 文档：\n\t- 新增：新增 `MongoDB` 集成示例。 感谢 `@lilihao` 提供的示例。 merge: [pr322](https://gitee.com/dromara/sa-token/pulls/322)、[pr667](https://github.com/dromara/Sa-Token/pull/667)   **[重要]**\n\t- 新增：“fox说技术” 视频教程链接。\n\t- 新增：“API接口参数签名”章节 视频讲解链接（B站抓蛙师）。\n\t- 优化：文档首页首屏增加需求提交按钮。\n\t- 其它：补全赞助者名单、`Dromara` 项目链接等信息。\n\t- 新增：`SpringBoot3.x` 版本配置 Redis 注意事项。fix: [#688](https://github.com/dromara/Sa-Token/issues/688)\n\t- 新增：`gitcode` g-star badge 展示。\n\t- 修复：`OAuth2` 滞后的配置信息示例。\n\t- 新增：新增视频账号链接。\n\t- 新增：新增团队成员展示。\n\n\n\n\n### v1.39.0 @2024-8-28\n- 核心：\n\t- 升级：重构注解鉴权底层，支持自定义鉴权注解了。  **[重要]**\n\t- 修复：修复前端提交同名 `Cookie` 时的框架错读现象。\n\t- 更名：`NotBasicAuthException` -> `NotHttpBasicAuthException`。\n- 插件：\n\t- 修复：修复 `sa-token-quick-login` 插件无法正常拦截的问题。\n- SSO：\n\t- 优化：优化 sso-server 前后端分离 demo 代码。\n\t- 优化：优化 sso-server 前后端分离时的跳转流程。\n- OAuth2：\n\t- 重构：`sa-token-oauth2` 模块整体重构。   **[重要]**  **[不向下兼容]**\n\t- 新增：新增支持自定义 `scope` 处理器。\t **[重要]**\n\t- 新增：新增支持自定义 `grant_type`。\t **[重要]**\n\t- 新增：新增 `scope` 划分等级。\t\t **[重要]**\n\t- 新增：新增 `oidc` 协议支持。\t\t **[重要]**\n\t- 新增：新增支持默认 `openid` 生成算法。\t **[重要]**\n\t- 新增：新增 `OAuth2` 注解鉴权支持。\t\t **[重要]**\n\t- 修复：`redirect_url` 参数校验增加规则：不允许出现@字符、*通配符只能出现在最后一位。关联 [issue](https://github.com/dromara/Sa-Token/issues/529) **[重要]**\n\t- 优化：优化代码注释、异常提示信息。\n\t- 升级：兼容 `Http Basic` 提交 `client` 信息的场景。感谢 github `@CuiGeekYoung` 提交的pr。\n\t- 升级：兼容 `Bearer Token` 方式提交 `access_token` 和 `client_token`。\n\t- 升级：适配拆分式路由。\n\t- 新增：将 `scope` 字段改为 List 类型。\n\t- 重构：抽离 `SaOAuth2Strategy` 全局策略接口，定义一些创建 token 的算法策略。\n\t- 新增：新增 `addAllowUrls` `addContractScopes` 方法，简化 `SaClientModel` 构建代码。\n\t- 重构：抽离 `SaOAuth2Dao` 接口，负责数据持久。\n\t- 重构：抽离 `SaOAuth2DataLoader` 数据加载器接口。\n\t- 重构：抽离 `SaOAuth2DataGenerate` 数据构造器接口。\n\t- 重构：抽离 `SaOAuth2DataConverter` 数据转换器接口。\n\t- 重构：抽离 `SaOAuth2DataResolver` 数据解析器接口。\n\t- 重构：重构 `SaOAuth2Handle` -> `SaOAuth2ServerProcessor` 更方便的逻辑重写。\n\t- 重构：重构 `PastToken` -> `LowerClientToken`。\n\t- 新增：新增 `state` 值校验，同一 `state` 参数不可重复使用。\n\t- 优化：完善 `SaOAuth2Util` 相关方法，更方便的二次开发。\n\t- 新增：新增部分异常类，细分异常 `ClassType`。\n\t- 优化：优化 `sa-token-oauth2` 异常细分状态码。\n- 文档：\n\t- 新增：新增数据结构说明。\n\t- 新增：新增不同 `client` 不同登录页说明。\n\t- 优化：优化文档 [将权限数据放在缓存里] 示例。\n\t- 新增：新增 从 Shiro、SpringSecurity、JWT 迁移 示例。  **[重要]**\n\n\n\n### v1.38.0 @2024-5-12\n- sa-token-core：\n\t- 修复：修复 `StpUtil.getSessionByLoginId(xx)` 参数为 null 时创建无效 `SaSession` 的 bug。\n\t- 优化：在 `SpringBoot 3.x` 版本下错误的引入依赖时将得到启动失败的提示。 （感谢`Uncarbon`提交的pr）\n\t- 优化：进一步优化权限校验算法，hasXxx API 只会返回 true 或 false，不再抛出异常。\n\t- 重构：`InvalidContextException` 更名为 `SaTokenContextException`。 **[已做向下兼容处理]**\n\t- 重构：彻底删除 `NotPermissionException` 异常中的 `getCode()` 方法。 **[过期API清理]**\n\t- 重构：重构 `SaTokenException` 类方法 `throwBy-`>`notTrue`、`throwByNull-`>`notEmpty`。**[已做向下兼容处理]**\n\t- 重构：`StpUtil.getSessionBySessionId` 提供的 `SessionId` 为空时将直接抛出异常，而不是再返回null。**[不向下兼容]**\n\t- 新增：新增 `Http Digest` 认证模块简单实现。\t**[重要]**\n\t- 重构：更换 `HttpBasic` 认证模块包名。\t**[已做向下兼容处理]**\n\t- 新增：新增 `StpUtil.getLoginDeviceByToken(xxx)` 方法，用于获取任意 token 的登录设备类型。\n\t- 新增：新增 `StpUtil.getTokenLastActiveTime()` 方法，获取当前 token 最后活跃时间。\n\t- 修复：修复“当登录时指定 timeout 小于全局 timeout 时，`Account-Session` 有效期为全局 timeout”的问题。\n\t- 优化：首次获取 `Token-Session` 时，其有效期将保持和 token 有效期相同，而不是再是全局 timeout 值。\n\t- 移除：移除 `SaSignConfig` 的 `isCheckNonce` 配置项。 **[不向下兼容]**\n\t- 新增：`SaSignTemplate#checkRequest` 增加“可指定参与签名参数”的功能。\n\t- 重构：将部分加密算法设置为过期。\n\t- 重构：优化 token 读取策略，空字符串将视为没有提交token。\n\t- 修复：`sa-token-bom` 补全缺失依赖。\n\t- 优化：二级认证校验之前必须先通过登录认证校验。\n\t- 修复：修复 `StpUtil.getLoginId(T defaultValue)` 传入 null 时无法正确返回值的bug。\n- sa-token-sso：\n\t- 优化：SSO 模式三，API 调用签名校验时，限定参与签名的参数列表，更安全。\n\t- 新增：新增 `autoRenewTimeout` 配置项：是否在每次下发 ticket 时，自动续期 token 的有效期（根据全局 timeout 值）\n\t- 新增：`SaSsoConfig` 新增配置 `isCheckSign`（是否校验参数签名），方便本地开发时的调试。\n\t- 新增：`SaSsoConfig` 新增配置 `currSsoLogin`，用于强制指定当前系统的 sso 登录地址。\n\t- 重构：整体重构 `sa-token-sso` 模块，将 `server` 端和 `client` 端代码拆分。 **[重要]** **[不向下兼容]**\n\t- 新增：`SaSsoConfig` 配置项 `ssoLogoutCall` 重命名为 `currSsoLogoutCall`。**[已做向下兼容处理]**\n\t- 重构：模式三在校验 Ticket 时，也将强制校验签名才能调通请求。**[不向下兼容]**\n\t- 新增：新增 `maxRegClient` 配置项，用于控制模式三下 client 注册数量。\n\t- 新增：新增不同 SSO Client 配置不同 `secret-key` 的方案。 **[重要]**\n\t- 重构：匿名 client 将不再能解析出所有应用的 ticket。**[不向下兼容]**\n\t- 新增：新增 `homeRoute` 配置项：在 ``/sso/auth`` 登录后不指定 redirect 参数的情况下默认跳转的路由。\n\t- 优化：优化登录有效期策略，SSO Client 端登录时将延续 SSO Server 端的会话剩余有效期。\n\t- 新增：新增 `checkTicketAppendData` 策略函数，用于在校验 ticket 后，给 sso-client 端追加返回信息。\n\t- 新增：SSO章节文档新增用户数据同步/迁移方案的建议。\n\t- 修复：修复利用@字符可以绕过域名允许列表校验的漏洞。 **[漏洞修复]**\n\t- 修复：禁止 `allow-url` 配置项 * 符号出现在中间位置，因为这有可能导致校验被绕过。 **[漏洞修复]**\n- 新增插件/示例：\n\t- 新增：新增插件 `sa-token-hutool-timed-cache`，用于整合 Hutool 缓存插件 TimedCache。 **[重要]**\n\t- 新增：新增 SSM 架构整合 Sa-Token 简单示例。 \t**[重要]**\n\t- 新增：新增 beetl 整合 Sa-Token 简单示例。 \t**[重要]**\n- 文档：\n\t- 部分章节将 `@Autowired` 更换为更合适的 `@PostConstruct`\n\t- 新增过滤器执行顺序更改示例。\n- 其它：\n\t- 优化：将跨域处理demo拆分为独立仓库。\n\t- 优化：解决 springboot 集成 sa-token 后排除 jackson 依赖无法成功启动的问题。\n\t- 优化：解决 `sa-token-jwt` 模块重复设置 keyt 秘钥问题。（感谢`KonBAI`提交的pr）\n\t- 优化：jwt模式 token 过期后，抛出的异常描述是 token 已过期，而不再是 token 无效。\n\t- 修复：补齐 `sa-token-spring-aop` 模块中遗漏监听的注解。\n\n\n### v1.37.0 @2023-10-18\n- 修复：修复路由拦截鉴权可被绕过的问题。 **[漏洞修复]**\n- 重构：未登录时调用鉴权 API 抛出未登录异常而不再是无权限异常。\n- 优化：优化 SaTokenDao 组件更换时的逻辑。\n- 文档：提供 SpringBoot3.x 路由匹配出错的解决方案。\n\n\n### v1.36.0 @2023-9-22\n- sa-token-core：\n\t- 修复：API接口签名校验参数接口NPE问题，增加必须参数的非空校验处理。\n\t- 新增：加密工具类新增 sha384、sha512 实现。 感谢 `@若初995` 提交的pr。   **[重要]**\n\t- 修复：`SaFoxUtil.vagueMatch()` 正则匹配的一些问题。  **[漏洞修复]**\n\t- 修复：`SaRouter.match()` 路由匹配的一些问题。  **[漏洞修复]**\n- 其它：\n\t- 优化：`sa-token-alone-redis` 去掉不必要的配置项判断。\n\t- 新增：`sa-token-solon-plugin` 增加对 solon 网关的支持。\n\t- 新增：新增第三方插件专用仓库：`sa-token-three-plugin` 。\n\t- 升级：`sa-token-solon-plugin` 增加对 solon 网关的支持。\n- 文档：\n\t- 新增：新增开启全局懒加载时不能注入上下文处理器的处理方案 。\n\t- 新增：新增 RefreshToken 示例。 **[重要]**\n\t- 新增：文档新增 sa-token 小助手，可在线实时技术提问。 **[重要]**\n\t- 优化：其它一些优化。\n- 新增插件：\n\t- `sa-token-redisson-jackson2`：通用 redisson 集成方案 （spring, solon, jfinal 等都可用）\n\n\n\n### v1.35.0 @2023-6-23\n- sa-token-core：\n\t- 优化：前端未提供 token 时，`getTokenSession()` 将抛出未登录异常，而不是返回 null。 **[不向下兼容]**\n\t- 新增：SaSession 新增字段：`type`、`loginType`、`loginId`、`token`。\n\t- 重构：全局过滤器抽离 SaFilter 统一接口。\n\t- 重构：全局过滤器 `includeList`、`excludeList` 改为 public，同时移除对应的 getter 方法。 **[不向下兼容]** \n\t- 重构：将全局过滤器的 BeforeAuth 认证设为不受 `includeList` 与 `excludeList` 的限制，所有请求都会进入。 **[不向下兼容]** \n\t- 新增：新增循环生成 token 的算法，用于确保 Token 的唯一性。 **[重要]** \n\t- 重构：API 接口签名所有方法均迁移至 core 核心模块。 **[重要]** \n\t- 新增：新增彩色日志打印，更方便的分辨不同日志等级。 **[重要]** \n\t- 重构：重构概念：临时有效期 -> token 最低活跃频率，过期后 token 冻结。\n\t- 重构：重构概念：`User-Session` -> `Account-Session`。\n\t- 新增：新增 `getTokenTimeout(String token)` 方法，获取任意 token 剩余有效期。\n\t- 优化：在登录时增加判断当前 StpLogic 是否支持 extra 扩展参数模式，如果不支持则打印警告信息。\n\t- 新增：NotLoginException 增加新场景值 -6、-7，提供更精确的未登录异常描述信息。\n\t- 新增：TokenSign 新增 tag 挂载参数，可在登录时方便的存储一些客户端特有数据。  **[重要]** \n\t- 新增：新增 `SaStrategy#createStpLogic`，用于指定动态创建 StpLogic 时的算法策略。\n\t- 新增：新增 `@SaCheckOr` 批量注解鉴权：只要满足其中一个注解即可通过验证。  **[重要]** \n\t- 重构：重命名：`SaStrategy.me` -> `SaStrategy.instance`。\n\t- 重构：在登录时强制性检查账号 id 是否为异常值，如果是则登录失败。\n\t- 重构：重构概念：`activity-timeout` -> `active-timeout`。  **[重要]** \n\t- 新增：新增动态 `active-timeout` 能力，可在每次登录时指定 `active-timeout` 值。  **[重要]** \n\t- 优化：将 `SaStrategy` 所有策略声明抽离为单独的函数式接口。\n\t- 新增：增加为 StpLogic 单独配置 `SaTokenConfig` 参数的能力。\n\t\n- sa-token-sso：\n\t- 修复：在 SSO 模式三中 `ticket` 校验地址配错时，会出现 NPE 的问题 \n\t- 新增：新增 `getData` 接口配置，在模式三拉取数据时可以传递任意参数。 **[重要]** \n\t- 重构：模式三秘钥配置更改：`sa-token.sso.secretkey=xxx` -> `sa-token.sign.secret-key=xxx`。 **[不向下兼容]**\n\t- 重构：模式三校验签名方法更改：`SaSsoUtil.checkSign(req)` -> `SaSignUtil.checkRequest(req)`。 **[不向下兼容]**\n\t- 新增：新增 `sa-token.sso.mode` 配置项，用于约定此系统使用的 SSO 模式。\n\t- 优化：优化校验 ticket 的逻辑。\n\t\n- sa-token-jwt：\n\t- 修复：jwt 令牌的签名类型可以被篡改的问题。 **[重要]** \n\t\n- 其它：\n\t- 优化：所有模块优化注释，更方便开发者阅读源码。\n\t- 优化：在所有 .java 文件中添加 license 头说明。\n\t- 优化：修复大部分代码警告。\n\t- 新增：新增 thymeleaf 标签方言命名空间，增强 ide 代码提示。 **[重要]** \n\t- 新增：定义 `sa-token-bom` 包，方便引入 sa-token 时对齐版本。\n\t- 新增：sa-token-dubbo3 插件新增代码示例。\n\t- 新增：新增跨域文章和示例：Header 参数版和第三方 Cookie 版。 **[重要]** \n\t- 修复：修复 `sa-token-alone-redis` 在低版本 springboot 下无法启动成功(缺少 `username` 属性)的问题。\n\t\n- 新增插件：\n\t- 新增：新增 `sa-token-context-dubbo3` 插件。 感谢 `@qiudaozhang` 提交的 pr。 **[重要]** \n\n- 文档：\n\t- 新增：部分常见报错排查。\n\t- 新增：首页图片增加懒加载效果，节省流量。\n\t- 新增：增加 Cookie 配置示例。\n\t- 修复：整理 demo 结构目录结构，修复不正确的路径说明。\n\t- 新增：新增 api-sign 模块文档。  **[重要]**  \n\t\n- 简化包名  **[重要]**  **[不向下兼容]** \n\t- `sa-token-dao-redis` -> `sa-token-redis`\n\t- `sa-token-dao-redis-jackson` -> `sa-token-redis-jackson`。\n\t- `sa-token-dao-redis-fastjson` -> `sa-token-redis-fastjson`。\n\t- `sa-token-dao-redis-fastjson2` -> `sa-token-redis-fastjson2`。\n\t- `sa-token-dao-redisson-jackson` -> `sa-token-redisson-jackson`。\n\t- `sa-token-dao-redisx` -> `sa-token-redisx`。\n\t- `sa-token-context-dubbo` -> `sa-token-dubbo`。\n\t- `sa-token-context-dubbo3` -> `sa-token-dubbo3`。\n\t- `sa-token-context-grpc` -> `sa-token-grpc`。\n\n\n### v1.34.0 @2023-1-11\n\n新增插件：\n- 新增：新增 `SpringBoot3.x` 集成插件，感谢 `@jry` 提供的参考思路。   **[重要]**\n- 新增：新增 `sa-token-dao-redisson-jackson` 插件，感谢 `@疯狂的狮子Li` 提交的pr。   **[重要]**\n\nsa-token-core 核心包：\n- 升级：升级 Sign 签名模块，增加部分重载方法。\n- 重构：`SaSignTemplate#joinParams` 更名为 `joinParamsDictSort`。  **[不向下兼容]**\n- 升级：升级临时 Token 认证模块，可指定 service 参数。\n- 删除：彻底删除过期类 `SaAnnotationInterceptor` 和 `SaRouteInterceptor`。\n- 修复：修复源码注释和文档的部分不合适之处。\n\nsa-token-sso 单点登录：\n- 删除：SSO 模块移除过期类 `SaSsoHandle` 类。\n- 新增：SSO 模块增加 ticket 的 client 锁定功能，解决部分场景下的 ticket 劫持问题。  **[重要]**\n- 修复：修复 SSO 模式2，在 client 端配置 `is-share=false` 时无法单点注销的问题。\n- 修复：修复 SSO 模式3 部分场景下注销时无法正常回退页面的问题。\n\n其它模块：\n- sa-token-oauth2：修复 OAuth2 模块示例 getClientModel 方法 clientId 写错的问题。\n- sa-token-alone-redis：新增：Alone-Redis 新增集群配置能力，感谢 `@appleOfGray` 提交的pr。   **[重要]**\n- sa-token-jwt：重构：使用 jwt-simple 模式后 is-share 恒等于 false，无论是否有设定 `setExtra` 数据。\n\n\n\n### v1.33.0 @2022-11-16\n- 重构：重构异常状态码机制。   **[重要]**\n- 重构：重构 sa-token-sso 模块异常码改为 300 开头，sa-token-jwt 异常码改为 302 开头。  **[不向下兼容]**\n- 新增：新增全局 Log 模块。   **[重要]**\n- 重构：`SaTokenListenerForConsolePrint` 改名 `SaTokenListenerForLog`。   **[不向下兼容]**\n- 修复：修复多线程下 `SaFoxUtil.getRandomString()` 随机数重复问题。\n- 修复：修复 sa-token-demo-sso3-client-nosdk 项目中单点注销 url 配置错误的问题\n- 文档：文档优化。\n\n\n\n### v1.32.0 @2022-10-28\n- 修复：修复 sa-token-dao-redis-fastjson 插件多余序列化 `timeout` 字段的问题。\n- 修复：修复 sa-token-dao-redis-fastjson 插件 `session.getModel` 无法反序列化实体类的问题。\n- 修复：修复 `sa-token-quick-login` 插件指定拦截排除路由不生效的问题。\n- 修复：修复 `sa-token-alone-redis` + `sa-token-dao-redis-fastson` 时 Redis 无法分离的问题。\n- 修复：修复在配置了 cookie.path 后，注销时无法彻底清除 Cookie 的问题。\n- 升级：`SaFoxUtil.getValueByType()` 新增对 char 类型的转换。\n- 新增：新增 `sa-token-dao-redis-fastjson2` 插件。 **[重要]** \n- 新增：新增全局配置 `is-write-header`，控制登录后是否将 Token 写入响应头。 **[重要]** \n- 新增：二级认证模块新增指定业务标识能力。  **[重要]** \n- 重构：Id-Token 模块更名为 Same-Token。 **[重要]** **[不向下兼容]**\n- 重构：重构会话查询参数作用：由`start=-1`时查询全部会话，改为 `start=0,size=-1` 时查询全部。 **[不向下兼容]** \n- 重构：`SaManager.getStpLogic(\"type\")` 默认当对应type不存在时不再抛出异常，而是自动创建并返回。\n- 重构：重构SSO模块，静态式API改为实例式：SaSsoHandle -> SaSsoProcessor。 **[重要]** **[不向下兼容]** \n- 重构：SSO-Server 端单点注销地址修改 `/sso/logout` -> `/sso/signout`，避免与 SSO-Client 端同 path 的冲突。 **[不向下兼容]** \n- 新增：文档新增 SSO 平台中心模式示例，跳连接进入子系统。 **[重要]** \n- 新增：新增SSO前后端分离集成示例 vue2 & vue3 版本。  **[重要]** \n- 重构：SSO 示例项目 http 请求工具改为 Forest。\n- 新增：SSO模块文档新增单个项目同时搭建 `sso-server` 和 `sso-client` 的示例。 **[重要]** \n- 新增：SSO模块文档新增一个项目同时搭建两个 `sso-server` 服务 的示例。 **[重要]** \n- 文档：在线文档新增代码示例。\n- 文档：在线文档增加全局调色功能。\n- 文档：[自定义 SaTokenContext 指南] 章节新增对三种模型的解释。\n- 文档：新增多账号体系混合鉴权代码示例。\n- 文档：文档增加 `Gradle` 依赖方式和 `properties` 风格配置。\n- 新增：新增 `sa-token-dependencies`，统一定义依赖版本。 **[重要]** \n\n##### 已知问题：\n\n> 部分场景下 Token 重复问题，受影响版本 `=v1.32.0`\n> - 受影响模块：\n> \t- sa-token-core 切换了 Token 风格：tik、random-32、random-64、random-128，如果使用 默认uuid、simple-uuid 风格则不受影响。\n> \t- sa-token-core 使用了临时 Token 认证模块，如果集成了 sa-token-temp-jwt 则不受影响。\n> \t- sa-token-core 使用了 Same-Token 模块。\n> \t- sa-token-jwt 全模块\n> \t- sa-token-oauth2 全模块\n> \t- sa-token-sso 模式二和模式三\n\n\n\n### v1.31.0 @2022-9-8\n- 文档：新增优秀开源案例展示。\n- 文档：新增博客展示，欢迎大家投稿。 \n- 新增：新增 `SaInterceptor` 综合拦截器。   **[重要]**  **[不向下兼容]**\n- 新增：新增 新增 `@SaIgnore` 忽略鉴权注解。   **[重要]** \n- 新增：新增插件 `sa-token-dao-redis-fastjson`，感谢 `@sikadai` 提交的pr。  **[重要]**\n- 新增：新增插件 `sa-token-context-grpc`，感谢 `@LiYiMing666` 提交的pr。  **[重要]**\n- 重构：SaSession 取消 `tokenSignList` 的 final 修饰符。\n- 新增：SaSession 添加 `setTokenSignList` 方法。\n- 重构：TokenSign 新增 `setValue` 和 `setDevice` 方法。\n- 修复：修复多账号模式下不能正确重置 `StpLogic` 的问题。\n- 修复：修复 SaSession 对象中 TokenSign 判断有可能空指针的问题。 \n- 修复：解决当权限码为 null 时可能带来的空指针问题。\n- 新增：新增 `StpUtil.getExtra(tokenValue, key)` 方法，用于获取任意 token 的扩展参数。 \n- 优化：优化 `StpLogic#logoutByTokenValue` 方法逻辑，精简代码。\n- 重构：`SaTokenConfig` 配置类字段 `isReadHead` 改为 `isReadHeader`。 **[不向下兼容]** \n- 修复：修复部分场景下踢人下线会抛出异常 `非Web上下文无法获取Request` 的问题。 \n- 新增：新增方法 `StpLogic#getAnonTokenSession`，可在未登录情况下安全的获取 Token-Session。  **[重要]** \n- 新增：新增 `SaApplication` 对象，用于全局作用域存取值。   **[重要]** \n- 重构：将 `SaTokenListener` 改为事件发布订阅模式，允许同时注册多个侦听器。  **[重要]**  **[不向下兼容]**\n- 重构：**StpUtil.login(id) 不再强制校验账号是否禁用，需要手动校验。** **[不向下兼容]** \n- 重构：`DisableLoginException` 更换名称为 `DisableServiceException`。 **[不向下兼容]** \n- 新增：新增对账号限制、分类封禁、阶梯封禁功能。\t **[重要]** \n- 新增：会话查询API增加反序获取会话方式。\n- 新增：SSO模块增加 server-url 属性，用于简化各种 url 配置。  **[重要]**\n- 修复：修复单点登录模块 `ssoLogoutCall` 配置项无效的问题。 \n- 优化：优化 `SaSsoHandle.checkTicket(ticket, currUri);` 方法，使其不提供 currUri 参数时将不再注册单点注销回调。\n- 修复：修复 `SaOAuth2Handle` 类中 `doLogin` 方法没有使用 `Param.pwd` 常量的问题。 \n- 新增：新增 `SaOAuth2Util.checkClientTokenScope(clientToken, scopes)` 方法，校验 Client-Token 是否含有指定 Scope。\n- 删除：删除 `sa-token-jwt` 模块过期 class。\n- 重构：`sa-token-jwt` 模块依赖改为 `hutool-jwt`，并升级版本为 5.8.5。\n- 重构：`sa-token-jwt` 模块改为 `Util + Template` 形式，方便针对部分代码重写。  **[重要]** \n- 新增：在线文档添加API手册。\n- 重构：`sa-token-oauth2` 模块密码模式新增 `client_secret` 参数校验。**[不向下兼容]** \n- 新增：集成 `jacoco` 插件，核心包单元测试覆盖率提高至 90% 以上。\n- 优化：开源案例分离专属仓库：[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token)\n\n\n\n### v1.30.0 @2022-05-9\n- 新增：新增集成 Web-Socket 鉴权示例。 **[重要]**\n- 新增：新增集成 Web-Socket（Spring封装版） 鉴权示例。\n- 新增：新增 jfinal 集成包 `sa-token-jfinal-plugin`  **[重要]**\n- 新增：新增 jboot 集成包 `sa-token-jboot-plugin` （感谢 @nxstv 提交的pr）\n- 修复：修复整合 sa-token-jwt Style 模式时，`StpUtil.getExtra(\"key\")` 无效的bug  \n- 升级：升级 `sa-token-context-dubbo` dubbo版本：`2.7.11` -> `2.7.15`\n- 升级：借助 `flatten-maven-plugin` 统一版本号定义 （感谢 @ruansheng8 提交的pr）   **[重要]**\n- 修复：修复在 `springboot 2.6.x` 下 `quick-login` 插件循环依赖无法启动的问题 \n- 优化：`sa-token-spring-aop` 依赖改为 `sa-token-core`，避免在webflux环境下启动报错的问题 \n- 优化：源码注释 设备标识 改为 设备类型 更符合语义 \n- 修复：解决部分协议下 dubbo 参数变为小写导致 `Id-Token` 鉴权无效的问题 \n- 升级：单元测试升级为 JUnit5 \n- 新增：新增 `maxLoginCount` 配置，指定同一账号可同时在线的最大数量   **[重要]** \n- 升级：彻底删除 SaTokenAction 接口，完全由 SaStrategy 代替 \n- 新增：新增 `sa-token-dao-redisx` 插件，感谢 @noear 提交的pr  **[重要]** \n- 优化：增加 parseToken 未配置 jwt 密钥时的异常提示，感谢 @BATTLEHAWK00 提交的pr \n- 优化：sso,oauth2 插件中调用配置类使用 getter 方法，感谢 @Naah 提交的pr \n- 新增：新增 json 转换器模块 \n- 重构：SaTokenListener#doLogin 方法新增 tokenValue 参数  **[不向下兼容]** \n- 升级：SpringBoot 相关组件依赖版本升级至 `2.5.12` \n- 文档：在线文档所有 `AjaxJson` 改为 `SaResult` \n- 文档：“多账号认证” -> 改为 “多账户认证” \n- 文档：部分章节新增动态演示图  **[重要]** \n- 升级：顶级异常类 `SaTokenException` 增加 code 异常细分状态码。[详见](/fun/exception-code) **[重要]** \n- **注意升级：受异常细分状态码影响，`NotPermissionException` 类中 `getCode()` 方法改为 `getPermission()`。** **[不向下兼容]**\n- SSO 模块升级：\n\t- 重构：SSO 模块从核心包拆分为独立插件 `sa-token-sso` **[重要]** \n\t- 优化：SSO模式三单点注销回调方法中，注销语句改为：`stpLogic.logout(loginId)` 更符合情景  \n\t- 修复：解决 sso 构建认证地址时，部分 Servlet 版本内部实现不一致带来的双 back 参数问题。\n\t- 升级：SSO 模块提供精细化异常处理 \n\t- 重构：SSO 模式三接口 `/sso/checkTicket`、`/sso/logout`，更改响应体格式   **[不向下兼容]** \n\t- 优化：SSO 模式三单点注销搭建示例增加 `try-catch`，提高容错性  \n\t- 优化：`SsoUtil.singleLogout` 改为 `SsoUtil.ssoLogout`，且无需再提供 secretkey 参数   **[不向下兼容]** \n\t- 升级：将 SSO 模式三的接口调用改为签名式校验。  **[重要]**  **[不向下兼容]** \n\t- 新增：新增 SSO 模式三下无 sdk 的对接示例， 感谢 @Sa-药水 的建议反馈 \t**[重要]** \n- sa-token-jwt 模块升级：\n\t- 重构：`sa-token-jwt` 的创建，强制校验loginType  **[不向下兼容]** \n\t- 重构：`StpLogicJwtForStateless` 由重写 login 方法改为重写 createLoginSession \n\t- 重构：`SaJwtUtil` 工具类不再吞并异常消息，且提供精细化异常 code 码。\n\t- 重构：改名：StpLogicJwtForStyle -> StpLogicJwtForSimple\n\t- 重构：改名：StpLogicJwtForMix -> StpLogicJwtForMixin\n\t- 修复：修复 `StpLogicJwtForSimple` 模式下 Extra 数据可能受到旧 token 影响的bug\n\n\n### v1.29.0 @2022-02-10\n- 升级：sa-token-jwt插件可在登录时添加额外数据。\n- 重构：优化Dubbo调用时向下传递Token的规则，可避免在项目启动时由于Context无效引发的bug。\n- 重构：OAuth2 授权模式开放由全局配置和Client单独配置共同设定。\n- 重构：OAuth2 模块部分属性支持每个 Client 单独配置。\n- 重构：OAuth2 模块部分方法名修复单词拼写错误：converXxx -> convertXxx。\n- 重构：修复 OAuth2 模块 `deleteAccessTokenIndex` 回收 token 不彻底的bug。\n- 新增：OAuth2 模块新增 `pastClientTokenTimeout`，用于指定 PastClientToken 默认有效期。\n- 文档：常见报错章节增加目录树，方便查阅。\n- 文档：优化文档样式。\n- 新增：新增 BCrypt 加密。\n- 修复：修复StpUtil.getLoginIdByToken(token) 在部分场景下返回出错的bug。\n- 重构：优化OAuth2模块密码式校验步骤。\n- 新增：新增Jackson定制版Session，避免timeout属性的序列化。\n- 新增：SaLoginModel新增setToken方法，用于预定本次登录产生的Token。 \n- 新增：新增 StpUtil.createLoginSession() 方法，用于无Token注入的方式创建登录会话。 \n- 新增：OAuth2 与 StpUtil 登录会话数据互通。\n- 新增：新增 `StpUtil.renewTimeout(100);` 方法，用于 Token 的 Timeout 值续期。 \n- 修复：修复默认dao实现类中 `updateObject` 无效的bug \n- 完善：完善单元测试。\n\n\n### v1.28.0 @2021-11-5\n- 新增：新增 `sa-token-jwt` 插件，用于与jwt的整合 **[重要]**\n- 新增：新增 `sa-token-context-dubbo` 插件，用于与 Dubbo 的整合 **[重要]**\n- 文档：文档新增章节：Sa-Token 插件开发指南 **[重要]**\n- 文档：文档新增章节：名称解释\n- 优化：抽离 `getSaTokenDao()` 方法，方便重写 \n- 新增：单元测试新增多账号模式数据不互通测试\n- 优化：优化在线文档，修复部分错误之处 \t\n- 优化：优化未登录异常抛出提示，标注无效的Token值 \n- 修复：修复单词拼写错误 `getDeviceOrDefault` \n- 优化：优化 jwt 集成示例 \n- 文档：新增常见问题总结\n\n\n### v1.27.0 @2021-10-11\n- 升级：增强 SaRouter 链式匹配能力  \t**[重要]**  \t\n- 新增：新增插件 Thymeleaf 标签方言   **[重要]**  \t\n- 新增：@SaCheckPermission 增加 orRole 字段，用于权限角色“双重or”匹配    **[重要]**\n- 升级：Cookie模式增加 `secure`、`httpOnly`、`sameSite`等属性的配置 \t**[重要]**  \t\n- 重构：重构SSO三种模式，抽离出统一的认证中心   **[重要]**   \n- 新增：新增 SaStrategy 策略类，方便内部逻辑按需重写 **[重要]**\t\t\n- 新增：临时认证模块新增 deleteToken 方法用于回收 Token  \n- 新增：新增 kickout、replaced 等注销会话的方法，更灵活的控制会话周期  **[重要]** \n- 新增：权限认证增加API：`StpUtil.hasPermissionAnd`、`StpUtil.hasPermissionOr` \n- 新增：角色认证增加API：`StpUtil.hasRoleAnd`、`StpUtil.hasRoleOr` \n- 新增：新增 `StpUtil.getRoleList()` 和 `StpUtil.getPermissionList()` 方法  \n- 新增：新增 StpLogic 自动注入特性，可快速方便的扩展 StpLogic 对象 \n- 优化：优化同端互斥登录逻辑，如果登录时没有指定设备类型标识，则默认顶替所有设备类型下线  \n- 优化：在未登录时调用 hasRole 和 hasPermission 不再抛出异常，而是返回false \n- 升级：升级注解鉴权算法，并提供更简单的重写方式    \n- 文档：新增常见报错排查，方便快速排查异常报错 \n- 文档：文档新增SSO单点登录与OAuth2技术选型对比  \n- 破坏式更新：\n\t- [向下兼容] 废弃 SaTokenAction 接口，替代方案： SaStrategy  \n\t- [向下兼容] 移除 `StpUtil.logoutByLoginId()` 更换为 `StpUtil.kickout()`;\n\t- [不向下兼容] 侦听器 doLogoutByLoginId 与 doReplaced 方法移除 device 参数 \n\t- [不向下兼容] 侦听器 doLogoutByLoginId 方法重命名为 doKickout  \n\n\n### v1.26.0 @2021-9-2\n- 优化：优化单点登录文档 \n- 新增：新增 `Http Basic` 认证 **[重要]** \n- 新增：文档新增跨域解决方案 \n- 文档：新增 Nginx 转发请求丢失uri的解决方案\n- 文档：新增 SSO 自定义 API 路由示例  **[重要]** \n- 示例：新增 `SSO-Server` 端前后端分离示例  **[重要]** \n\n\n### v1.25.0 @2021-8-16\n- 新增：`SaRequest`新增`getHeader(name, defaultValue)`方法，用于获取header默认值 \n- 新增：`SaRequest` 添加 `forward` 转发方法  \n- 新增：Readme新增源码模块介绍、友情链接、正在使用Sa-Token的项目 \n- 重构：重构SSO单点登录模块源码，增加可读性 \n- 新增：SSO配置表新增所属端说明 \n- 新增：SSO模式三新增账号资料同步示例  **[重要]** \n- 新增：前后端分离模式下接入SSO的示例  **[重要]** \n- 优化：优化SSO单点注销重定向逻辑 \n- 重构：重构SSO单点登录模块部分API \n- 优化：优化SaQuickBean中过滤器处理逻辑  \n- 文档：优化文档样式，增加示例  \n- 文档：代码鉴权、注解鉴权、路由拦截鉴权，选择指南 \n- 文档：文档新增 SSO旧有系统改造指南 \n- 文档：SSO集成文档里添加API列表 \n- 文档：新增 `Sa-Token-Study` 链接，讲解 Sa-Token 源码涉及到的技术点 \n- 不兼容更新重构：\n\t- 重构：修复 `SaReactorHolder.getContent()` 拼写错误：`content` -> `context` \n\n\n### v1.24.0 @2021-7-24\n- 修复：修复部分场景下Alone-Redis插件导致项目无法启动的问题\n- 优化：增加对SpringBoot1.x版本的兼容性 \n- 新增：SaOAuth2Util新增checkScope函数，用于校验令牌是否具备指定权限 \n- 新增：OAuth2.0模块新增revoke接口，用于提前回收 Access-Token 令牌 \n- 新增：新增`Sa-Id-Token` 模块，解决微服务内部调用鉴权  **[重要]**\n- 文档：新增OAuth2.0模块常用方法说明  \n- 优化：大幅度优化文档示例 \n\n\n### v1.23.0 @2021-7-19\n- 新增：Sa-Token-OAuth2 模块正式发布   **[重要]** \n- 修复：修复jwt集成demo无法正确注册StpLogic的bug\n- 修复：修复登录时某些场景下Session续期可能不正常的bug  \n- 优化：代码注释优化，文档优化  \n\n\n### v1.22.0 @2021-7-10\n- 新增：SaSsoConfig 部分属性增加set连缀风格 \n- 优化：SaSsoUtil 可定制化底层的 `StpLogic`\n- 新增：新增 `SaSsoHandle` 大幅度简化单点登录整合步骤  **[重要]** \n- 新增：新增Sa-Token在线测评，链接：[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx)  **[重要]**\n- 新增：Sa-Token-Quick-Login 插件新增拦截与放行路径配置\n- 优化：大幅度优化文档示例 \n\n\n### v1.21.0 @2021-7-2\n- 新增：新增Token二级认证 \t**[重要]** \n- 新增：新增`Sa-Token-Alone-Redis`独立Redis插件   **[重要]**  \n- 新增：新增SSO三种模式，彻底解决所有场景下的单点登录问题   **[重要]**  \n- 新增：新增多账号模式下，注解合并示例\t\t**[重要]**  \n- 新增：新增`SaRouter.back()`函数，用于停止匹配返回结果  \n- 不兼容更新重构：\n\t- 更改yml配置前缀：原`[spring.sa-token.]` 改为 `[sa-token.]`，目前版本暂时向下兼容，请尽快更新 \n\n\n### v1.20.0 @2021-6-17\n- 新增：新增Solon适配插件，感谢大佬 `@刘西东` 提供的pr **[重要]** \n- 新增：新增`SaRouter.stop()`函数，用于一次性跳出匹配链功能 **[重要]** \n- 新增：新增单元测试   **[重要]** \n- 新增：新增临时令牌验证模块   **[重要]**  \n- 新增：新增`sa-token-temp-jwt`模块整合jwt临时令牌鉴权    **[重要]**  \n- 新增：会话 `SaSession.get()` 增加缓存API，简化代码 \n- 新增：新增框架调查问卷 \n- 修复：修复同时引入 `Spring Cloud Bus` 与 `Sa-Token` 冲突的问题   **[重要]** \n- 修复：修复`SaServletFilter`异常函数中无法自定义`Content-Type`的问题 \n- 文档：新增微服务依赖引入说明 \n- 文档：新增认证流程图 \n- 不兼容更新重构：\n\t- 方法：`StpUtil.setLoginId(id)` -> `StpUtil.login(id)` \n\t- 方法：`StpUtil.getLoginKey()` -> `StpUtil.getLoginType()` (注意其它所有地方的`LoginKey`均已更改为`loginType`)\n\t- 工具类：`SaRouterUtil` -> `SaRouter` \n\t- 配置类：`allowConcurrentLogin` -> `isConcurrent` \n\t- 配置类：`isV` -> `isPrint` \n\t- 为保证平滑更新，旧API仍旧保留，但已增加`@Deprecated`注解，请尽快更新至新API  \n\n\n### v1.19.0 @2021-5-10\n- 新增：注解鉴权新增定制loginType功能  **[重要]** \n- 重构：重构目录结构，抽离`plugin`模块  **[重要]** \n- 新增：新增 `sa-token-quick-login` 插件，零代码集成登录功能  **[重要]** \n- 优化：所有函数式接口增加`@FunctionalInterface`注解，感谢群友`@MrXionGe`提供的建议 \n- 优化：文档优化... \n\n\n### v1.18.0 @2021-4-24\n- 新增：新增权限通配符功能，灵活设置权限  **[重要]** \n- 修复：修复自动续签处的逻辑错误 \n- 新增：新增Web开发常见漏洞防护建议 \n- 修复：修复`SaRequest`中缺少`getMethod()`的bug \n- 修复：修复自动续签时的逻辑错误，感谢群成员`@N`的建议 \n- 新增：全局过滤器新增 `beforAuth` 前置函数 \n- 修复：修复在带有上下文的项目中无法正确获取请求路径的bug，感谢群成员`@dlwlrma`提供的建议\n- 新增：新增`SaHolder`上下文持有类，可方便的在上下文中读写数据 \n- 重构：`SaTokenManager` -> `SaManager` \n- 重构：`SaTokenInsideUtil` -> `SaFoxUtil` \n\n\n### v1.17.0 @2021-4-17\n- 修复：在WebFlux环境中引入Redis集成包无法启动的问题 \n- 修复：修复JWT集成示例中版本升级API的变更 \n- 优化：优化启动时字符画打印\n- 文档：新增集成环境说明\n- 文档：新增功能介绍图  \n- 新增：全局过滤器增加限定[拦截路径]与[排除路径]功能 \n- 重构：全局过滤器执行函数放到成员变量里，连缀风格配置 \n- 新增：新增全局侦听器，可在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作 **[重要]** \n\n\n### v1.16.0 @2021-4-12\n- 新增：新增账号封禁功能，指定时间内账号无法登陆 \t\t\t**[重要]**\n- 新增：核心包脱离`ServletAPI`，彻底零依赖！  \t\t\t\t**[重要]**\n- 新增：新增基于`ThreadLocal`的上下文容器\t\t\t\t\t**[重要]**\n- 新增：新增`Reactor`响应式编程支持，`WebFlux`集成！\t\t\t**[重要]** \n- 新增：新增全局过滤器，解决拦截器无法拦截静态资源的问题\t\t\t**[重要]** \n- 新增：新增微服务网关鉴权方案！可接入`ShenYu`、`Gateway`等网关组件!\t**[重要]** \n- 新增：AOP切面定义`Order`顺序为`-100`，可保证在多个自定义切面前执行 \n- 文档：新增推荐公众号列表 \n\n\n### v1.15.0 @2021-3-23\n- 新增：文档添加源码涉及技术栈说明 \n- 优化：优化路由拦截器模块文档，更简洁的示例\n- 修复：修复非web环境下的错误提示，Request->Response\n- 修复：修复Cookie注入时path判断错误，感谢@zhangzi0291提供的PR\n- 新增：文档集成Redis章节新增redis配置示例说明，感谢群友 `@-)` 提供的建议\n- 新增：增加token前缀模式，可在配置token读取前缀，适配`Bearer token`规范 **[重要]**\n- 优化：`SaTokenManager`初始化Bean去除`initXxx`方法，优化代码逻辑\n- 新增：`SaTokenManager`新增`stpLogicMap`集合，记录所有`StpLogic`的初始化，方便查找\n- 新增：`Session`新增timeout操作API，可灵活修改Session的剩余有效时间 \n- 新增：token前缀改为强制校验模式，如果配置了前缀，则前端提交token时必须带有\n- 优化：精简`SaRouteInterceptor`，只保留自定义验证和默认的登陆验证，去除冗余功能 \n- 优化：`SaRouterUtil`迁移到core核心包，优化依赖架构\n- 优化：默认Dao实现类里`Timer定时器`改为子线程 + sleep 模拟 \n- 新增：`Session`新增各种类型转换API，可快速方便存取值  **[重要]** \n- 升级注意：\n\t- `SaRouterUtil`类迁移到核心包，注意更换import地址\n\t- `SaRouteInterceptor`去出冗余API，详情参考路由鉴权部分\n\n\n### v1.14.0 @2021-3-12\n- 新增：新增`SaLoginModel`登录参数Model，适配 [记住我] 模式\t **[重要]**\n- 新增：新增 `StpUtil.login()` 时指定token有效期，可灵活控制用户的一次登录免验证时长 \n- 新增：新增Cookie时间判断，在`timeout`设置为-1时，`Cookie`有效期将为`Integer.MAX_VALUE`\t **[重要]**\n- 新增：新增密码加密工具类，可快速MD5、SHA1、SHA256、AES、RSA加密 \t**[重要]**\n- 新增：新增 OAuth2.0 模块  \t**[重要]** \n- 新增：`SaTokenConfig`配置类所有set方法支持链式调用 \n- 新增：`SaOAuth2Config` sa-token oauth2 配置类所有set方法新增支持链式调用 \n- 优化：`StpLogic`类所有`getKey`方法重名为`splicingKey`，更语义化的函数名称 \n- 新增：`IsRunFunction`新增`noExe`函数，用于指定当`isRun`值为`false`时执行的函数 \n- 新增：`SaSession`新增数据存取值操作API \n- 优化：优化`SaTokenDao`接口，增加Object操作API\n- 优化：jwt示例`createToken`方法去除默认秘钥判断，只在启动项目时打印警告 \n- 文档：常见问题新增示例(修改密码后如何立即掉线)\n- 文档：权限认证文档新增[如何把权限精确搭到按钮级]示例说明 \n- 文档：优化文档，部分模块添加图片说明 \n\n\n### v1.13.0 @2021-2-9\n- 优化：优化源码注释与文档\n- 新增：文档集成Gitalk评论系统 \n- 优化：源码包`Maven`版本号更改为变量形式 \n- 修复：文档处方法名`getPermissionList`错误的bug \n- 修复：修复`StpUtil.getTokenInfo()`会触发自动续签的bug \n- 修复：修复接口 `SaTokenDao` 的 `searchData` 函数注释错误 \n- 新增：`SaSession`的创建抽象到`SaTokenAction`接口，方便按需重写 \n- 新建：框架内异常统一继承 `SaTokenException` 方便在异常处理时分辨处理 \n- 新增：`SaSession`新增`setId()`与`setCreateTime()`方法，方便部分框架的序列化 \n- 新增：新增`autoRenew`配置，用于控制是否打开自动续签模式\n- 新增：同域模式下的单点登录  **[重要]**\n- 新增：完善分布式会话的文档说明\n\n\n### v1.12.0 @2021-1-12\n- 新增：提供JWT集成示例 **[重要]**\n- 新增：新增路由式鉴权，可方便的根据路由匹配鉴权  **[重要]**\n- 新增：新增身份临时切换功能，可在一个代码段内将会话临时切换为其它账号  **[重要]**\n- 优化：将`SaCheckInterceptor.java`更名为`SaAnnotationInterceptor.java`，更语义化的名称 \n- 优化：优化文档\n- 升级：v1.12.1，新增`SaRouterUtil`工具类，更方便的路由鉴权   **[重要]**\n\n\n### v1.11.0 @2021-1-10\n- 新增：提供AOP注解鉴权方案 **[重要]**\n- 优化自动生成token的算法\n\n\n### v1.10.0 @2021-1-9\n- 新增：提供查询所有会话方案  **[重要]**\n- 修复：修复token设置为永不过期时无法正常被顶下线的bug，感谢github用户 @zjh599245299 提出的bug\n\n\n### v1.9.0 @2021-1-6\n- 优化：`spring-boot-starter-data-redis` 由 `2.3.7.RELEASE` 改为 `2.3.3.RELEASE` \n- 修复：补上注解拦截器里漏掉验证`@SaCheckRole`的bug\n- 新增：新增同端互斥登录，像QQ一样手机电脑同时在线，但是两个手机上互斥登录  **[重要]**\n\n\n### v1.8.0 @2021-1-2\n- 优化：优化源码注释\n- 修复：修复部分文档错别字 \n- 修复：修复项目文件夹名称错误\n- 优化：优化文档配色，更舒服的代码展示\n- 新增：提供`sa-token`集成 `redis` 的 `spring-boot-starter` 方案  **[重要]**\n- 新增：新增集成 `redis` 时，以`jackson`作为序列化方案  **[重要]**\n- 新增：dao层默认实现增加定时清理过期数据功能  **[重要]**\n- 新增：新增`token专属session`, 更灵活的会话管理  **[重要]**\n- 新增：增加配置，指定在获取`token专属session`时是否必须登录\n- 新增：在无token时自动创建会话，完美兼容`token-session`会话模型!   **[重要]**\n- 修改：权限码限定必须为String类型 \n- 优化：注解验证模式由boolean属性改为枚举方式\n- 删除：`StpUtil`删除部分冗长API，保持API清爽性\n- 新增：新增角色验证 (角色验证与权限验证已完全分离)  **[重要]**\n- 优化：移除`StpUtil.kickoutByLoginId()`API，由`logoutByLoginId`代替\n- 升级：开源协议修改为`Apache-2.0`\n\n\n### v1.7.0 @2020-12-24\n- 优化：项目架构改为maven多模块形式，方便增加新模块 **[重要]**\n- 优化：与`springboot`的集成改为`springboot-starter`模式，无需`@SaTokenSetup`注解即可完成自动装配 **[重要]**\n- 新增：新增`activity-timeout`配置，可控制token临时过期与续签功能 **[重要]**\n- 新增：`timeout`过期时间新增-1值，代表永不过期 \n- 新增：`StpUtil.getTokenInfo()`改为对象形式，新增部分常用字段 \n- 优化：解决在无cookie模式下，不集成redis时会话无法主动过期的问题 \n- 修复：修复文档首页样式问题 \n\n\n### v1.6.0 @2020-12-17\n- 新增：花式token生成方案 **[重要]** \n- 优化：优化`readme.md` \n- 修复：修复`SaCookieOper`与`SaTokenAction`无法自动注入的问题 \n\n\n### v1.5.1 @2020-12-16\n- 新增：细化未登录异常类型，提供五种场景值：未提供token、token无效、token已过期 、token已被顶下线、token已被踢下线 **[重要]**\n- 修复：修复`StpUtil.getSessionByLoginId(String loginId)`方法转换key出错的bug，感谢群友 @(＃°Д°)、@一米阳光 发现的bug \n- 优化：修改方法`StpUtil.getSessionByLoginId(Object loginId)`的isCreate值默认为true \n- 修改：`方法delSaSession`修改为`deleteSaSession`，更加语义化的函数名称 \n- 新增：新增`StpUtil.getTokenName()`方法，更语义化的获取tokenName \n- 新增：新增`SaTokenAction`框架行为Bean，方便重写逻辑 \n- 优化：`Cookie操作`改为接口代理模式，使其可以被重写 \n- 优化：文档里集成redis部分增加redis的pom依赖示例\n- 修复：登录验证-> `StpUtil.getLoginId_defaultNull()` 修复方法名错误的问题 \n- 优化：优化`readme.md` \n- 升级：开源协议修改为`MIT`\n\n\n### v1.4.0 @2020-9-7\n- 优化：修改一些函数、变量名称，使其更符合阿里java代码规范\n- 优化：`tokenValue`的读取优先级改为：`request` > `body` > `header` > `cookie`  **[重要]**\n- 新增：新增`isReadCookie`配置，决定是否从`cookie`里读取`token`信息 \n- 优化：如果`isReadCookie`配置为`false`，那么在登录时也不会把`cookie`写入`cookie` \n- 新增：新增`getSessionByLoginId(Object loginId, boolean isCreate)`方法\n- 修复：修复文档部分错误，修正群号码\n\n\n### v1.3.0 @2020-5-2\n- 新增：新增 `StpUtil.checkLogin()` 方法，更符合语义化的鉴权方法\n- 新增：注册拦截器时可设置 `StpLogic` ，方便不同模块不同鉴权方式\n- 新增：抛出异常时增加 `loginType` 区分，方便多账号体系鉴权处理 \n- 修复：修复启动时的版本字符画版本号打印不对的bug  \n- 修复：修复文档部分不正确之处\n- 新增：新增文档的友情链接\n\n\n### v1.2.0 @2020-3-7\n- 新增：新增注解式验证，可在路由方法中使用注解进行权限验证  **[重要]**\n- 参考：[注解式验证](use/at-check)\n\n\n### v1.1.0 @2020-2-12\n- 修复：修复`StpUtil.getLoginId(T defaultValue)`取值转换错误的bug\n\n\n### v1.0.0 @2020-2-4\n- 第一个版本出炉 \n"
  },
  {
    "path": "sa-token-doc/more/wenjuan.md",
    "content": "# 问卷调查\n\n我们想以运营一款产品的心态来运营一个开源框架，所以我们迫切希望您能够填写这份问卷，这有6道选择题，应该只会略微占用您1-3分钟的时间，Sa-Token将会非常重视每一位粉丝的宝贵意见！\n\n[https://wj.qq.com/s2/14587150/b5b4/](https://wj.qq.com/s2/14587150/b5b4/)\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-apidoc.md",
    "content": "# Sa-Token-OAuth2 Server端 API列表\n基于官方仓库的搭建示例，`OAuth2-Server`端会暴露出以下API，`OAuth2-Client`端可据此文档进行对接  \n\n--- \n\n## 1、模式一：授权码（Authorization Code）\n\n### 1.1、获取授权码\n\n根据以下格式构建URL，引导用户访问 （复制时请注意删减掉相应空格和换行符）\n``` url\nhttp://{host}:{port}/oauth2/authorize\n\t?response_type=code\n\t&client_id={client_id}\n\t&redirect_uri={redirect_uri}\n\t&scope={scope}\n\t&state={state}\n```\n\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| response_type\t| 是\t\t| 返回类型，这里请填写：`code`\t\t\t\t\t\t\t|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t\t\t\t\t|\n| redirect_uri\t| 是\t\t| 用户确认授权后，重定向的 url 地址\t\t\t\t\t\t\t|\n| scope\t\t\t| 否\t\t| 具体请求的权限，多个用逗号(或空格)隔开\t\t\t\t\t\t|\n| state\t\t\t| 否\t\t| 随机值，此参数会在重定向时追加到url末尾，不填不追加，如果填写则每次填写的值不可以重复\t|\n\n注意点：\n1. 如果用户在 `OAuth-Server` 端尚未登录：会被转发到登录视图，你可以参照文档或官方示例自定义登录页面。\n2. 如果 `scope` 参数为空，或者请求的 `scope` 用户近期已确认授权过，则无需用户再次确认，达到静默授权的效果，否则需要用户手动确认，服务器才可以下放 `code` 授权码。\n\n用户确认授权之后，会被重定向至`redirect_uri`，并追加 `code` 参数与 `state` 参数，形如：\n``` url\nredirect_uri?code={code}&state={state}\n```\n\n`Code` 授权码具有以下特点：\n1. 每次授权产生的 `Code` 码都不一样。\n2. `Code` 码用完即废，不能二次使用。\n3. 一个 `Code` 的有效期默认为五分钟，超时自动作废。\n4. 每次授权产生新 `Code` 码，会导致旧 `Code` 码立即作废，即使旧 `Code` 码尚未使用。\n\n\n<details>\n<summary>RestAPI 登录接口：/oauth2/doLogin</summary>\n\n如果用户在 OAuth-Server 端尚未登录，则会被阻塞在登录界面，开始登录，需要在页面上调用`/oauth2/doLogin`完成登录（此接口非 OAuth2 标准协议接口）\n\n``` url\nhttp://{host}:{port}/oauth2/doLogin\n\t?name={name}\n\t&pwd={pwd}\n```\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| name\t\t\t| 否\t\t| 账号\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| pwd\t\t\t| 否\t\t| 密码\t\t\t\t\t\t\t\t\t\t\t\t\t|\n\n访问此接口将进入自定义的 `cfg.doLoginHandle` 函数开始登录，你只要在此函数内调用 `StpUtil.login(xxx)` 即代表登录成功。\n\n另外需要注意：此接口并非只能携带 `name`、`pwd` 参数，因为你可以在方法里通过 `SaHolder.getRequest().getParam(\"xxx\")` 来获取前端提交的其它参数。\n\n</details>\n\n\n<details>\n<summary>RestAPI 确认授权接口：/oauth2/doConfirm</summary>\n\n如果 oauth-client 端申请的 scope 在 OAuth-Server 端需要用户手动确认授权，则会被阻塞在授权界面，\n需要在页面上调用`/oauth2/doConfirm`完成授权（此接口非 OAuth2 标准协议接口）\n\n``` url\nhttp://{host}:{port}/oauth2/doConfirm\n    ?client_id={value}\n    &scope={value}\n    &build_redirect_uri={true|false}\n    &response_type={value}\n    &redirect_uri={value}\n    &state={value}\n```\n参数详解：\n\n| 参数\t\t\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| client_id\t\t\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t\t\t\t\t|\n| scope\t\t\t\t\t| 是\t\t| 具体确认的权限，多个用逗号(或空格)隔开\t\t\t\t\t|\n| build_redirect_uri\t| 否\t\t| 是否立即构建 `redirect_uri` 授权地址，取值：true | false\t\t|\n| response_type\t\t\t| 否\t\t| 取 url 上的 `response_type` 参数来提交\t\t\t\t\t|\n| redirect_uri\t\t\t| 否\t\t| 取 url 上的 `redirect_uri` 参数来提交\t\t\t\t\t|\n| state\t\t\t\t\t| 否\t\t| 取 url 上的 `state` 参数来提交\t\t\t\t\t\t\t|\n\n此接口有两种调用方式，一种只提供 `client_id`、`scope` 两个参数，此时返回结果代表是否确认授权成功：\n``` js\n{\n    code: 200, \n    msg: 'ok', \n    data: null,\n}\n```\n\n一种是指定 `build_redirect_uri: true`，并同时提供 `client_id`、`scope`、`response_type`、`redirect_uri`、`state` 全部参数，\n此时返回结果包括最终的 code 授权地址：\n``` js\n{\n    code: 200, \n    msg: 'ok', \n    data: null,\n\tredirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC'\n}\n```\n\n前端在 ajax 回调函数中直接使用 `location.href=res.redirect_uri` 跳转即可，无需再重复访问 `/oauth2/authorize` 接口。\n\n</details>\n\n\n\n### 1.2、根据授权码获取 Access-Token\n获得 `Code` 码后，我们可以通过以下接口，获取到用户的 `Access-Token`、`Refresh-Token` 等信息。\n\n``` url\nhttp://{host}:{port}/oauth2/token\n\t?grant_type=authorization_code\n\t&client_id={client_id}\n\t&client_secret={client_secret}\n\t&code={code}\n```\n\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| grant_type\t| 是\t\t| 授权类型，这里请填写：`authorization_code`\t\t\t\t|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t\t\t\t\t|\n| client_secret\t| 是\t\t| 应用秘钥\t\t\t\t\t\t\t\t\t\t\t\t|\n| code\t\t\t| 是\t\t| 步骤 1.1 中获取到的授权码\t\t\t\t\t\t\t\t|\n\n也可以通过 `Basic Authorization` 方式提交 `client` 信息，格式为在请求 `header` 头添加 `Authorization` 参数：\n``` js\nheader['Authorization'] = base64(`${client_id}:${client_secret}`);\n```\n\n接口返回示例：\n\n``` js\n{\n    \"code\": 200,    // 200表示请求成功，非200标识请求失败, 以下不再赘述 \n    \"msg\": \"ok\",\n    \"data\": null,\n    \"token_type\": \"bearer\",\n    \"access_token\": \"Gly7mnnXSdCxkOqmOwcA5SbG6ZtPmJVX7ZgSn1pidhRmnenBEgxbWJS8VWxA\",     // Access-Token值\n    \"refresh_token\": \"EuYNwpxdc18MpaZLPyhFeyAyzr2IOWEr4q3QUGgPWqdJujQqvohjQEDJpwOm\",    // Refresh-Token值\n    \"expires_in\": 7199,                  // Access-Token剩余有效期，单位秒  \n    \"refresh_expires_in\": 2591999,       // Refresh-Token剩余有效期，单位秒  \n    \"client_id\": \"1001\",                 // 应用 id\n    \"scope\": \"userinfo\"                  // 此令牌包含的权限\n}\n```\n\n\n### 1.3、根据 Refresh-Token 刷新 Access-Token （如果需要的话）\nAccess-Token的有效期较短，如果每次过期都需要重新授权的话，会比较影响用户体验，因此我们可以在后台通过`Refresh-Token` 刷新 `Access-Token`\n\n``` url\nhttp://{host}:{port}/oauth2/refresh\n\t?grant_type=refresh_token\n\t&client_id={client_id}\n\t&client_secret={client_secret}\n\t&refresh_token={refresh_token}\n```\n\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| grant_type\t| 是\t\t| 授权类型，这里请填写：`refresh_token`\t\t\t\t|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t\t\t\t\t|\n| client_secret\t| 是\t\t| 应用秘钥\t\t\t\t\t\t\t\t\t\t\t\t|\n| refresh_token | 是\t\t| 步骤1.2中获取到的 `Refresh-Token` 值\t\t\t\t\t\t\t|\n\n接口返回值同章节1.2，此处不再赘述 \n\n\n### 1.4、回收 Access-Token （如果需要的话）\n在A ccess-Token 过期之前主动将其回收 \n\n``` url\nhttp://{host}:{port}/oauth2/revoke\n\t?client_id={client_id}\n\t&client_secret={client_secret}\n\t&access_token={access_token}\n```\n\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t\t\t\t\t|\n| client_secret\t| 是\t\t| 应用秘钥\t\t\t\t\t\t\t\t\t\t\t\t|\n| access_token  | 是\t\t| 步骤1.2中获取到的`Access-Token`值\t\t\t\t\t\t|\n\n返回值样例：\n``` js\n{\n    \"code\": 200,\n    \"msg\": \"ok\",\n    \"data\": null\n}\n```\n\n\n### 1.5、根据 Access-Token 获取相应用户的账号信息  \n注：此接口非 OAuth2 标准协议接口，为官方仓库 demo 模拟接口，正式项目中大家可以根据此样例，自定义需要的接口及参数 \n\n``` url\nhttp://{host}:{port}/oauth2/userinfo?access_token={access_token}\n```\n\n返回值样例：\n``` js\n{\n    \"code\": 200,\n    \"msg\": \"ok\",\n\t\"nickname\": \"shengzhang_\",         // 账号昵称\n\t\"avatar\": \"http://xxx.com/1.jpg\",  // 头像地址\n\t\"age\": \"18\",                       // 年龄\n\t\"sex\": \"男\",                       // 性别\n\t\"address\": \"山东省 青岛市 城阳区\"   // 所在城市 \n}\n```\n\n除了直接在 url 中以 query 参数方式提交 `access_token`，你也可以在 `Authorization` 请求头以 `Bearer Token` 方式提交：\n``` js\nheader['Authorization'] = 'Bearer access_token';\n```\n\n\n## 2、模式二：隐藏式（Implicit）\n\n根据以下格式构建URL，引导用户访问：\n``` url\nhttp://{host}:{port}/oauth2/authorize\n\t?response_type=token\n\t&client_id={client_id}\n\t&redirect_uri={redirect_uri}\n\t&scope={scope}\n\t&state={state}\n```\n\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| response_type\t| 是\t\t| 返回类型，这里请填写：`token`\t\t\t\t\t\t\t|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t\t\t\t\t|\n| redirect_uri\t| 是\t\t| 用户确认授权后，重定向的url地址\t\t\t\t\t\t\t|\n| scope\t\t\t| 否\t\t| 具体请求的权限，多个用逗号(或空格)隔开\t\t\t\t\t\t\t|\n| state\t\t\t| 否\t\t| 随机值，此参数会在重定向时追加到url末尾，不填不追加，如果填写则每次填写的值不可以重复\t\t\t|\n\n此模式会越过授权码的步骤，直接返回 `Access-Token` 到前端页面，形如：\n``` url\nredirect_uri#token=xxxx-xxxx-xxxx-xxxx\n```\n注意 token 是以 `#` 锚参数的形式拼接到 url 上的。\n\n\n## 3、模式三：密码式（Password）\n首先在Client端构建表单，让用户输入 Server 端的账号和密码，然后在 Client 端访问接口 \n``` url\nhttp://{host}:{port}/oauth2/token\n\t?grant_type=password\n\t&client_id={client_id}\n\t&client_secret={client_secret}\n\t&username={username}\n\t&password={password}\n\t&scope={scope}\n```\n\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t|\n| grant_type\t| 是\t\t| 返回类型，这里请填写：`password`|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t|\n| client_secret\t| 是\t\t| 应用秘钥\t\t\t\t\t\t\t\t\t\t\t\t|\n| username\t\t| 是\t\t| 用户的 `OAuth2-Server` 端账号\t\t\t|\n| password\t\t| 是\t\t| 用户的 `OAuth2-Server` 端密码\t\t\t|\n| scope\t\t\t| 否\t\t| 具体请求的权限，多个用逗号(或空格)隔开\t\t\t\t\t\t|\n\n接口返回示例：\n\n``` js\n{\n    \"code\": 200,\t// 200表示请求成功，非200标识请求失败, 以下不再赘述 \n    \"msg\": \"ok\",\n\t\"access_token\": \"7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ\",     // Access-Token 值\n\t\"refresh_token\": \"ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66\",    // Refresh-Token 值\n\t\"expires_in\": 7199,                 // Access-Token 剩余有效期，单位秒  \n\t\"refresh_expires_in\": 2591999,      // Refresh-Token 剩余有效期，单位秒  \n\t\"client_id\": \"1001\",                // 应用 id\n\t\"scope\": \"\",                        // 此令牌包含的权限\n}\n```\n\n> [!WARNING| label:重写认证处理器] \n> 在正式项目中，password 认证模式需要重写 `PasswordGrantTypeHandler` 处理器，在后面的 [自定义 grant_type](/oauth2/oauth2-custom-grant_type) 章节我们会详细介绍\n\n\n## 4、模式四：凭证式（Client Credentials）\n以上三种模式获取的都是用户的 `Access-Token`，代表用户对第三方应用的授权，\n在OAuth2.0中还有一种针对 Client级别的授权， 即：`Client-Token`，代表应用自身的资源授权\n\n在 Client 端的后台访问以下接口：\n\n``` url\nhttp://{host}:{port}/oauth2/client_token\n\t?grant_type=client_credentials\n\t&client_id={client_id}\n\t&client_secret={client_secret}\n\t&scope={scope}\n```\n\n参数详解：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t|\n| grant_type\t| 是\t\t| 返回类型，这里请填写：`client_credentials`|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t|\n| client_secret\t| 是\t\t| 应用秘钥\t\t\t\t\t\t\t\t|\n| scope\t\t\t| 否\t\t| 具体请求的权限，多个用逗号(或空格)隔开\t|\n\n接口返回值样例：\n``` js\n{\n    \"code\": 200,\n    \"msg\": \"ok\",\n\t\"client_token\": \"HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO\",\t// Client-Token 值\n\t\"expires_in\": 7199,     // Token剩余有效时间，单位秒 \n\t\"client_id\": \"1001\",    // 应用 id\n\t\"scope\": null           // 包含权限 \n}\n```\n\n注：`Client-Token`具有延迟作废特性，即：在每次获取最新`Client-Token`的时候，旧`Client-Token`不会立即过期，而是作为`Lower-Client-Token`再次储存起来，\n资源请求方只要携带其中之一便可通过Token校验，这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”， 保证了服务的高可用。\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-at-check.md",
    "content": "# Sa-Token OAuth2 模块相关注解\n\nsa-token-oauth2 模块扩展了三个注解用于相关数据校验：\n- `@SaCheckAccessToken`：指定请求中必须包含有效的 `access_token` ，并且包含指定的 `scope`。\n- `@SaCheckClientToken`：指定请求中必须包含有效的 `client_token` ，并且包含指定的 `scope`。\n- `@SaCheckClientIdSecret`：指定请求中必须包含有效的 `client_id` 和 `client_secret` 信息。\n\n和 Sa-Token-Code 模块的注解一样，你必须先注册框架的内置拦截器，才可以使用这些注解，详细参考：[注解鉴权](/use/at-check) 。\n\n--- \n\n\n### 1、@SaCheckAccessToken 示例\n\n``` java\n@RestController\n@RequestMapping(\"/test\")\npublic class TestController {\n\n    // 测试：携带有效的 access_token 才可以进入请求\n    // 你可以在请求参数中携带 access_token 参数，或者从请求头以 Authorization: bearer xxx 的形式携带 \n    @SaCheckAccessToken\n    @RequestMapping(\"/checkAccessToken\")\n    public SaResult checkAccessToken() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 access_token ，并且具备指定 scope 才可以进入请求\n    @SaCheckAccessToken(scope = \"userinfo\")\n    @RequestMapping(\"/checkAccessTokenScope\")\n    public SaResult checkAccessTokenScope() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 access_token ，并且具备指定 scope 列表才可以进入请求\n    @SaCheckAccessToken(scope = {\"openid\", \"userinfo\"})\n    @RequestMapping(\"/checkAccessTokenScopeList\")\n    public SaResult checkAccessTokenScopeList() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n}\n```\n\n\n### 2、@SaCheckClientToken 示例\n\n``` java\n@RestController\n@RequestMapping(\"/test\")\npublic class TestController {\n\n    // 测试：携带有效的 client_token 才可以进入请求\n    // 你可以在请求参数中携带 client_token 参数，或者从请求头以 Authorization: bearer xxx 的形式携带\n    @SaCheckClientToken\n    @RequestMapping(\"/checkClientToken\")\n    public SaResult checkClientToken() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 client_token ，并且具备指定 scope 才可以进入请求\n    @SaCheckClientToken(scope = \"userinfo\")\n    @RequestMapping(\"/checkClientTokenScope\")\n    public SaResult checkClientTokenScope() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n    // 测试：携带有效的 client_token ，并且具备指定 scope 列表才可以进入请求\n    @SaCheckClientToken(scope = {\"openid\", \"userinfo\"})\n    @RequestMapping(\"/checkClientTokenScopeList\")\n    public SaResult checkClientTokenScopeList() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n}\n```\n\n\n### 3、@SaCheckClientIdSecret 示例\n``` java\n@RestController\n@RequestMapping(\"/test\")\npublic class TestController {\n\n    // 测试：携带有效的 client_id 和 client_secret 信息，才可以进入请求\n    // 你可以在请求参数中携带 client_id 和 client_secret 参数，或者从请求头以 Authorization: Basic base64(client_id:client_secret) 的形式携带\n    @SaCheckClientIdSecret\n    @RequestMapping(\"/checkClientIdSecret\")\n    public SaResult checkClientIdSecret() {\n        return SaResult.ok(\"访问成功\");\n    }\n\n}\n```\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-check-domain.md",
    "content": "# OAuth2 整合-配置域名校验\n\n--- \n\n### 1、code 劫持攻击\n在前面章节的 OAuth-Server 搭建示例中：\n\n``` java\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t// 根据 clientId 获取 Client 信息\n\t@Override\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\tif(\"1001\".equals(clientId)) {\n\t\t\treturn new SaClientModel()\n\t\t\t\t\t// ...\n\t\t\t\t\t.addAllowRedirectUris(\"*\")    // 所有允许授权的 url\n\t\t\t\t\t// ...\n\t\t}\n\t\treturn null;\n\t}\n\t// 其它代码 ... \n}\n```\n\n配置项 `AllowRedirectUris` 意为配置此 `Client` 端所有允许的授权地址，不在此配置项中的 URL 将无法下发 `code` 授权码。\n\n为了方便测试，上述代码将其配置为`*`，但是，<font color=\"#FF0000\" >在生产环境中，此配置项绝对不能配置为 * </font>，否则会有被 `code` 劫持的风险。\n\n假设攻击者根据模仿我们的授权地址，巧妙的构造一个URL：\n\n> [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)\n\n当不知情的小红被诱导访问了这个 URL 时，它将被重定向至百度首页。\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/oauth2-ticket-jc.png\" alt=\"oauth2-ticket-jc\" />\n\n可以看到，代表着用户身份的 code 授权码也显现到了URL之中，借此漏洞，攻击者完全可以构建一个 URL 将小红的 code 授权码自动提交到攻击者自己的服务器，伪造小红身份登录网站。\n\n\n### 2、防范方法\n\n造成此漏洞的直接原因就是我们对此 client 配置了过于宽泛的 `AllowRedirectUris` 允许授权地址，防范的方法也很简单，就是缩小 `AllowRedirectUris` 授权范围。\n\n我们将其配置为一个具体的URL：\n\n``` java\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t// 根据 clientId 获取 Client 信息\n\t@Override\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\tif(\"1001\".equals(clientId)) {\n\t\t\treturn new SaClientModel()\n\t\t\t\t\t// ...\n\t\t\t\t\t.addAllowRedirectUris(\"http://sa-oauth-client.com:8002/\")    // 所有允许授权的 url\n\t\t\t\t\t// ...\n\t\t}\n\t\treturn null;\n\t}\n\t// 其它代码 ... \n}\n```\n\n再次访问上述链接：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/oauth2-feifa-rf.png\" alt=\"oauth2-feifa-rf\" />\n\nURL 没有通过校验，拒绝授权！\n\n\n### 3、配置安全性参考表\n\n| 配置方式\t\t| 举例\t\t\t\t\t\t\t\t\t\t\t| 安全性\t\t\t\t\t\t\t\t|  建议\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t\t\t\t\t\t\t\t\t\t| :--------\t\t\t\t\t\t\t| :--------\t\t\t\t\t\t\t\t|\n| 配置为*\t\t| `*`\t\t\t\t\t\t\t\t\t\t\t| <font color=\"#F00\" >低</font>\t\t| **<font color=\"#F00\" >禁止在生产环境下使用</font>**\t|\n| 配置到域名\t\t| `http://sa-oauth-client.com:8002/*`\t\t\t| <font color=\"#F70\" >中</font>\t\t| <font color=\"#F70\" >不建议在生产环境下使用</font>\t|\n| 配置到详细地址\t| `http://sa-oauth-client.com:8002/xxx/xxx`\t\t| <font color=\"#080\" >高</font>\t\t| <font color=\"#080\" >可以在生产环境下使用</font>\t|\n\n\n### 4、其它规则\n\n1、AllowRedirectUris 配置的地址不允许出现 `@` 字符。\n\n- 反例：`http://user@sa-token.cc`\n- 反例：`http://sa-oauth-client.com@sa-token.cc`\n\n*详见源码：[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) \n`checkRedirectUri` 方法。*\n\n2、AllowRedirectUris 配置的地址 `*` 通配符只允许出现在字符串末尾，不允许出现在字符串中间位置。\n\n- 反例：`http*://sa-oauth-client.com/`\n- 反例：`http://*.sa-oauth-client.com/`\n\n*详见源码： [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) \n`checkRedirectUriListNormalStaticMethod` 方法。*\n\n参考：[github/issue/529](https://github.com/dromara/Sa-Token/issues/529)\n感谢这位 `@m4ra7h0n` 用户反馈的漏洞。\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-custom-api.md",
    "content": "# OAuth2-自定义 API 路由 \n\n---\n\n### 方式一：修改全局变量\n\n在之前的章节中，我们演示了如何搭建一个 OAuth2 认证中心：\n``` java\n/**\n * Sa-Token-OAuth2 Server端 Controller \n */\n@RestController\npublic class SaOAuth2ServerController {\n\n\t// OAuth2-Server 端：处理所有 OAuth2 相关请求 \n\t@RequestMapping(\"/oauth2/*\")\n\tpublic Object request() {\n\t\treturn SaOAuth2ServerProcessor.instance.dister();\n\t}\n\t\n\t// ... 其它代码\n\t\n}\n```\n\n这种写法集成简单但却不够灵活。例如获取 code 授权码地址只能是：`http://{host}:{port}/oauth2/authorize`，如果我们想要自定义其API地址，应该怎么做呢？\n\n打开 OAuth2 模块相关源码，有关 API 的设计都定义在：\n[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)\n中，我们可以对其进行二次修改。\n\n例如，我们可以在 Main 方法启动类或者 OAuth2 配置方法中修改变量值：\n``` java\n// 配置 OAuth2 相关参数 \n@Autowired\nprivate void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t// 自定义API地址\n\tSaOAuth2Consts.Api.authorize = \"/oauth2/authorize2\";\n\t// ... \n}\n```\n\n启动项目，统一认证地址就被我们修改成了：`http://{host}:{port}/oauth2/authorize2`\n\n\n### 方式二：拆分路由入口\n根据上述路由入口：`@RequestMapping(\"/oauth2/*\")`，我们给它起一个合适的名字 —— 聚合式路由。\n\n与之对应的，我们可以将其修改为拆分式路由：\n\n``` java\n/**\n * Sa-Token-OAuth2 Server端 Controller \n */\n@RestController\npublic class SaOAuth2ServerController {\n\n\t// 模式一：Code授权码 || 模式二：隐藏式\n\t@RequestMapping(\"/oauth2/authorize\")\n\tpublic Object authorize() {\n\t\treturn SaOAuth2ServerProcessor.instance.authorize();\n\t}\n\n\t// 用户登录\n\t@RequestMapping(\"/oauth2/doLogin\")\n\tpublic Object doLogin() {\n\t\treturn SaOAuth2ServerProcessor.instance.doLogin();\n\t}\n\n\t// 用户确认授权\n\t@RequestMapping(\"/oauth2/doConfirm\")\n\tpublic Object doConfirm() {\n\t\treturn SaOAuth2ServerProcessor.instance.doConfirm();\n\t}\n\n\t// Code 换 Access-Token || 模式三：密码式\n\t@RequestMapping(\"/oauth2/token\")\n\tpublic Object token() {\n\t\treturn SaOAuth2ServerProcessor.instance.token();\n\t}\n\n\t// Refresh-Token 刷新 Access-Token\n\t@RequestMapping(\"/oauth2/refresh\")\n\tpublic Object refresh() {\n\t\treturn SaOAuth2ServerProcessor.instance.refresh();\n\t}\n\n\t// 回收 Access-Token\n\t@RequestMapping(\"/oauth2/revoke\")\n\tpublic Object revoke() {\n\t\treturn SaOAuth2ServerProcessor.instance.revoke();\n\t}\n\n\t// 模式四：凭证式\n\t@RequestMapping(\"/oauth2/client_token\")\n\tpublic Object clientToken() {\n\t\treturn SaOAuth2ServerProcessor.instance.clientToken();\n\t}\n\n}\n```\n\n拆分式路由 与 聚合式路由 在功能上完全等价，且提供了更为细致的路由管控。\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-custom-grant_type.md",
    "content": "# OAuth2-自定义权限处理器 \n\n\n### 1、需求场景\n\nOAuth2 协议的 `/oauth2/token` 接口定义了两种获取 `access_token` 的 `grant_type`，分别是：\n- `authorization_code`：使用用户授权的授权码获取 access_token。\n- `password`：使用用户提交的账号、密码来获取 access_token。\n\n你可以重写内置 `grant_type` 处理器，或添加自定义 `grant_type` 处理器，来支持更多的场景。\n\n\n\n--- \n\n\n### 2、重写 password 认证模式处理器\n\n当我们按照文档搭建的代码直接测试 password 认证模式时，控制台会得到警告：\n``` txt\n警告信息：当前 password 认证模式，使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle)，仅供开发测试\n正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法\n```\n\n这是因为为方便测试，框架内部直接将 password 认证请求转发到了 `SaOAuth2Strategy.instance.doLoginHandle` 来处理，\n在真正的项目中需要大家重写 password 认证模式处理器：\n\n``` java\n/**\n * 自定义 Password Grant_Type 授权模式处理器认证过程\n */\n@Component\npublic class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler {\n\n    @Override\n    public PasswordAuthResult loginByUsernamePassword(String username, String password) {\n        if(\"sa\".equals(username) && \"123456\".equals(password)) {\n            long userId = 10001;\n            return new PasswordAuthResult(userId);\n        } else {\n            throw new SaOAuth2Exception(\"无效账号密码\");\n        }\n    }\n\n}\n```\n\n\n### 3、添加自定义 grant_type 处理器\n\n假设有以下需求：通过 手机号+验证码 登录，返回 `access_token`。\n\n#### 3.1、新增验证码发送接口\n\n首先在 oauth2-server 端开放一个接口，为指定手机号发送验证码。\n\n``` java\n/**\n * 自定义手机登录接口\n */\n@RestController\npublic class PhoneLoginController {\n\n    @RequestMapping(\"/oauth2/sendPhoneCode\")\n    public SaResult sendCode(String phone) {\n        String code = SaFoxUtil.getRandomNumber(100000, 999999) + \"\";\n        SaManager.getSaTokenDao().set(\"phone_code:\" + phone, code, 60 * 5);\n        System.out.println(\"手机号：\" + phone + \"，验证码：\" + code + \"，已发送成功\");\n        return SaResult.ok(\"验证码发送成功\");\n    }\n\n}\n```\n\n真实项目肯定是要对接短信服务商的，此处我们仅做模拟代码，将发送的验证码打印在控制台上。\n\n\n#### 3.2、自定义 grant_type 处理器\n\n在 oauth2-server 新建 `PhoneCodeGrantTypeHandler` 实现 `SaOAuth2GrantTypeHandlerInterface` 接口：\n\n``` java\n/**\n * 自定义 phone_code 授权模式处理器 \n */\n@Component\npublic class PhoneCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {\n\n    @Override\n    public String getHandlerGrantType() {\n        return \"phone_code\";\n    }\n\n    @Override\n    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {\n\n        // 获取前端提交的参数 \n        String phone = req.getParamNotNull(\"phone\");\n        String code = req.getParamNotNull(\"code\");\n        String realCode = SaManager.getSaTokenDao().get(\"phone_code:\" + phone);\n\n        // 1、校验验证码是否正确\n        if(!code.equals(realCode)) {\n            throw new SaOAuth2Exception(\"验证码错误\");\n        }\n\n        // 2、校验通过，删除验证码\n        SaManager.getSaTokenDao().delete(\"phone_code:\" + phone);\n\n        // 3、登录\n        long userId = 10001; // 模拟 userId，真实项目应该根据手机号从数据库查询\n\n        // 4、构建 ra 对象\n        RequestAuthModel ra = new RequestAuthModel();\n        ra.clientId = clientId;\n        ra.loginId = userId;\n        ra.scopes = scopes;\n\n        // 5、生成 Access-Token\n        AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = \"phone_code\");\n        return at;\n    }\n}\n```\n\n#### 3.3、为应用添加允许的授权类型\n\n在 `SaOAuth2DataLoader` 实现类中，为 client 的允许授权类型增加自定义的 `phone_code` \n\n``` java\n// Sa-Token OAuth2：自定义数据加载器 \n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t@Override\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\tif(\"1001\".equals(clientId)) {\n\t\t\treturn new SaClientModel()\n\t\t\t\t\t.setClientId(\"1001\")  \n\t\t\t\t\t.setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")  \n\t\t\t\t\t.addAllowRedirectUris(\"*\")    // 所有允许授权的 url\n\t\t\t\t\t.addContractScopes(\"openid\", \"userid\", \"userinfo\")  \n\t\t\t\t\t.addAllowGrantTypes( \n\t\t\t\t\t\t\tGrantType.authorization_code, \n\t\t\t\t\t\t\tGrantType.implicit, \n\t\t\t\t\t\t\tGrantType.refresh_token, \n\t\t\t\t\t\t\tGrantType.password, \n\t\t\t\t\t\t\tGrantType.client_credentials, \n\t\t\t\t\t\t\t\"phone_code\"  // 重要代码：自定义授权模式 手机号验证码登录\n\t\t\t\t\t)\n\t\t\t;\n\t\t}\n\t\treturn null;\n\t}\n\t// 其它代码 ... \n}\n```\n\n完工，开始测试。\n\n\n### 4、测试步骤\n\n#### 1、先发送验证码 \n\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/sendPhoneCode?phone=13144556677\n```\n\n#### 2、请求 token  \n\n注意 `grant_type` 要填写我们自定义的 `phone_code`，code 的具体值可以在后端的控制台上看到 \n\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/token\n    ?grant_type=phone_code\n    &client_id=1001\n    &client_secret=aaaa-bbbb-cccc-dddd-eeee\n\t&scope=openid\n    &phone=13144556677\n\t&code={value}\n```\n\n返回结果参考如下：\n\n``` js\n{\n    \"code\": 200,\n    \"msg\": \"ok\",\n    \"data\": null,\n    \"token_type\": \"bearer\",\n    \"access_token\": \"pfxRz6KVacwvKNu4IHmDsCJs33kvvARs2z1lTch7stog8nRt6rfVLowtAZ0E\",\n    \"refresh_token\": \"qcFD6Wo2qZidofXQtWF5oK5ML6ljHKufQ5SbouBxzGnHhnMjUG4VV0iXZhdE\",\n    \"expires_in\": 7199,\n    \"refresh_expires_in\": 2591999,\n    \"client_id\": \"1001\",\n    \"scope\": \"openid\",\n    \"openid\": \"ded91dc189a437dd1bac2274be167d50\"\n}\n```\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-custom-login.md",
    "content": "# OAuth2 定制化登录页面\n\n---\n\n\n\n### 1、如何自定义 OAuth-Server 端的登录视图？\n\n重写 `SaOAuth2Strategy.instance.notLoginView` 策略：\n\n``` java\n@Autowired\npublic void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t// 配置：未登录时返回的View \n\tSaOAuth2Strategy.instance.notLoginView = ()->{\n\t\treturn new ModelAndView(\"xxx.html\");\n\t};\n}\n```\n\n在以上返回的视图中 ajax 方式调用 `/oauth2/doLogin` 接口，该接口接受以下参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t|\n| name\t\t\t| 否\t\t| 账号\t\t\t\t\t\t\t\t\t|\n| pwd\t\t\t| 否\t\t| 密码\t\t\t\t\t\t\t\t\t|\n\n接口返回值根据你重写的 `cfg.doLoginHandle` 策略进行自由决定。\n\n\n\n### 2、如何自定义登录API的接口地址？\n根据需求点选择解决方案：\n\n#### 2.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数？\n``` java\n// 在任意代码处获取前端提交的参数 \nString xxx = SaHolder.getRequest().getParam(\"xxx\");\n```\n\n#### 2.2、想完全自定义一个接口来接受前端登录请求？\n``` java\n// 直接定义一个拦截路由为 `/oauth2/doLogin` 的接口即可 \n@RequestMapping(\"/oauth2/doLogin\")\npublic SaResult ss(String name, String pwd) {\n\tSystem.out.println(\"------ 请求进入了自定义的API接口 ---------- \");\n\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\tStpUtil.login(10001);\n\t\treturn SaResult.ok(\"登录成功！\");\n\t}\n\treturn SaResult.error(\"登录失败！\");\n}\n```\n\n#### 2.3、不想使用`/oauth2/doLogin`这个接口，想自定义一个API地址？\n\n答：直接在前端更改点击按钮时 Ajax 的请求地址即可 \n\n\n\n### 3、如何自定义 OAuth-Server 端的确认授权视图？\n\n重写 `SaOAuth2Strategy.instance.confirmView` 策略：\n\n``` java\n@Autowired\npublic void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t// 配置：授权确认视图 \n\tSaOAuth2Strategy.instance.confirmView = (clientId, scopes)->{\n\t\tMap<String, Object> map = new HashMap<>();\n\t\tmap.put(\"clientId\", clientId);\n\t\tmap.put(\"scope\", scopes);\n\t\treturn new ModelAndView(\"confirm.html\", map);\n\t};\n}\n```\n\n在以上返回的视图中 ajax 方式调用 `/oauth2/doConfirm` 接口，即可完成授权，该接口接受以下参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t|\n| client_id\t\t| 是\t\t| 应用 id\t\t\t\t\t\t\t\t|\n| scope\t\t\t| 是\t\t| 具体授予的权限，多个用逗号(或空格)隔开\t|\n\n接口返回值样例：\n``` js\n{\n    \"code\": 200,\n    \"msg\": \"ok\",\n    \"data\": null,\n}\n```\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-custom-scope.md",
    "content": "# OAuth2-自定义 Scope 权限及处理器\n\n--- \n\n### 1、需求场景\n一般情况下，对于第三方 oauth2-client 来讲，仅仅拿到用户的 access_token 是不够的，还需要拿到更多的信息，比如用户昵称、头像等资料。\n\nsa-token-oauth2 提供两种模式，让 access_token 可以得到更多信息。\n\n- 自定义接口模式：在 oauth2-server 端开放一个资料查询接口，在 oauth2-client 得到 `access_token` 后，再次调用这个接口来获取 `userinfo` 信息。\n- 自定义权限处理器模式：自定义一个 `ScopeHandler`，直接在返回 `access_token` 时追加字段，将 `userinfo` 信息和 `access_token` 一并返回到 oauth2-client。\n\n\n### 2、自定义接口模式\n\n#### 1、新建查询接口\n\n在 oauth2-server 新建接口，查询指定 `access_token` 代表的 `userId` 其 `userinfo`：\n\n``` java\n// 获取 userinfo 信息：昵称、头像、性别等等\n@RequestMapping(\"/oauth2/userinfo\")\npublic SaResult userinfo() {\n\t// 获取 Access-Token 对应的账号id\n\tString accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());\n\tObject loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);\n\tSystem.out.println(\"-------- 此Access-Token对应的账号id: \" + loginId);\n\t\n\t// 校验 Access-Token 是否具有权限: userinfo\n\tSaOAuth2Util.checkAccessTokenScope(accessToken, \"userinfo\");\n\t\n\t// 模拟账号信息 （真实环境需要查询数据库获取信息）\n\tMap<String, Object> map = new LinkedHashMap<>();\n\t// map.put(\"userId\", loginId);  一般原则下，oauth2-server 不能把 userId 返回给 oauth2-client\n\tmap.put(\"nickname\", \"林小林\");\n\tmap.put(\"avatar\", \"http://xxx.com/1.jpg\");\n\tmap.put(\"age\", \"18\");\n\tmap.put(\"sex\", \"男\");\n\tmap.put(\"address\", \"山东省 青岛市 城阳区\");\n\treturn SaResult.ok().setMap(map);\n}\n```\n\n\n#### 2、申请 code 时指定权限\noauth2-client 申请 `code` 时，一定需要加上 `userinfo` 权限\n\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/authorize\n\t?response_type=code\n\t&client_id=1001\n\t&redirect_uri=http://sa-oauth-client.com:8002/\n\t&scope=userinfo\n```\n\n\n#### 3、code 换 access_token\n访问上述链接后，得到 `code` 授权码，然后我们拿着 `code` 换 `access_token`\n\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/token\n    ?grant_type=authorization_code\n    &client_id=1001\n    &client_secret=aaaa-bbbb-cccc-dddd-eeee\n    &code=${code}\n```\n\n#### 4、access_token 取 userinfo \n使用返回的 `access_token` 再次访问接口 `/oauth2/userinfo`\n\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/userinfo?access_token=${access_token}\n```\n\n返回以下结果：\n``` js\n{\n  \"code\": 200,\n  \"msg\": \"ok\",\n  \"data\": null,\n  \"nickname\": \"林小林\",\n  \"avatar\": \"http://xxx.com/1.jpg\",\n  \"age\": \"18\",\n  \"sex\": \"男\",\n  \"address\": \"山东省 青岛市 城阳区\"\n}\n```\n\n拿到 userinfo。\n\n\n\n### 3、自定义权限处理器模式\n\n#### 1、新建权限处理器\n在 oauth2-server 新建 `UserinfoScopeHandler.java` 实现 `SaOAuth2ScopeHandlerInterface` 接口：\n\n``` java\n/**\n *  自定义 userinfo scope 处理器 \n */\n@Component\npublic class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface {\n\n    // 指示当前处理器所要处理的 scope \n    @Override\n    public String getHandlerScope() {\n        return \"userinfo\";\n    }\n\n    // 当构建的 AccessToken 具有此权限时，所需要执行的方法\n    @Override\n    public void workAccessToken(AccessTokenModel at) {\n        System.out.println(\"--------- userinfo 权限，加工 AccessTokenModel --------- \");\n        // 模拟账号信息 （真实环境需要查询数据库获取信息）\n        Map<String, Object> map = new LinkedHashMap<String, Object>();\n        map.put(\"userId\", \"10008\");\n        map.put(\"nickname\", \"shengzhang_\");\n        map.put(\"avatar\", \"http://xxx.com/1.jpg\");\n        map.put(\"age\", \"18\");\n        map.put(\"sex\", \"男\");\n        map.put(\"address\", \"山东省 青岛市 城阳区\");\n        at.extraData.putAll(map);\n    }\n\n    // 当构建的 ClientToken 具有此权限时，所需要执行的方法\n    @Override\n    public void workClientToken(ClientTokenModel ct) {\n    }\n\n    // 当使用 RefreshToken 刷新 AccessToken 时，是否重新执行 workAccessToken 构建方法\n\t// 在一些实时性较高的数据中需要指定为 true \n    @Override\n    public boolean refreshAccessTokenIsWork() {\n        return true;\n    }\n    \n}\n```\n\n如上所述，所有写入到 `extraData` 中的数据，都将追加返回到 oauth2-client 端。\n\n\n#### 2、申请 code 时指定权限\noauth2-client 申请 `code` 时，一定需要加上 `userinfo` 权限\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/authorize\n\t?response_type=code\n\t&client_id=1001\n\t&redirect_uri=http://sa-oauth-client.com:8002/\n\t&scope=userinfo\n```\n\n\n#### 3、code 换 access_token\n访问上述链接后，得到 `code` 授权码，然后我们拿着 `code` 换 `access_token`\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/token\n    ?grant_type=authorization_code\n    &client_id=1001\n    &client_secret=aaaa-bbbb-cccc-dddd-eeee\n    &code=${code}\n```\n\n返回结果如下\n``` js\n{\n  \"code\": 200,\n  \"msg\": \"ok\",\n  \"data\": null,\n  \"token_type\": \"bearer\",\n  \"access_token\": \"LQ24xI0hX25vIzvciHPA0PNsnGCweSFM1Bzl8783li07VAXpw8sEfn9xsta2\",\n  \"refresh_token\": \"rKB8mby1Mw8yZXHbWzliHx6lmatcLcULLw5C5cUMBhMMRx72DFg5u0owZgrA\",\n  \"expires_in\": 7199,\n  \"refresh_expires_in\": 2591999,\n  \"client_id\": \"1001\",\n  \"scope\": \"openid,userid,userinfo\",\n  \"userinfo\": {\n    \"userId\": \"10008\",\n    \"nickname\": \"shengzhang_\",\n    \"avatar\": \"http://xxx.com/1.jpg\",\n    \"age\": \"18\",\n    \"sex\": \"男\",\n    \"address\": \"山东省 青岛市 城阳区\"\n  }\n}\n```\n\n拿到 userinfo。\n\n#### 总结\n相比于自定义接口模式，自定义权限处理器模式可以少一次网络请求，让 oauth2-client 端提前拿到 `userinfo` 信息。\n\n\n\n### 4、最终权限处理器\n当一个自定义权限处理器，监听的 scope 字符串为 `_FINALLY_WORK_SCOPE` 时，则代表这个权限处理器为“最终权限处理器”。\n\n最终权限处理器会永远在所有权限处理器工作完成之后执行一次，即使 oauth2-client 端没有申请任何 scope，最终权限处理器也会固定执行。\n\n示例：\n``` java\n/**\n * 最终权限处理器：在所有权限处理器工作完成之后，执行此权限处理器\n */\n@Component\npublic class FinallyWorkScopeHandler implements SaOAuth2ScopeHandlerInterface {\n\n    @Override\n    public String getHandlerScope() {\n        return SaOAuth2Consts._FINALLY_WORK_SCOPE;\n    }\n\n    @Override\n    public void workAccessToken(AccessTokenModel at) {\n        // 在所有权限处理器工作完成之后，执行此处方法加工 AccessToken\n        // System.out.println(123);\n    }\n\n    @Override\n    public void workClientToken(ClientTokenModel ct) {\n        // System.out.println(456);\n    }\n}\n```\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-data-loader.md",
    "content": "# OAuth2-自定义数据加载器\n\n\n\n### 1、基于内存的数据加载\n\n在之前搭建 OAuth2-Server 示例中，我们演示了 client 信息配置方案：\n``` java\n// Sa-Token OAuth2 定制化配置\n@Autowired\npublic void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t\n\t// 添加 client \n\toauth2Server.addClient(\n\t\tnew SaClientModel()\n\t\t\t.setClientId(\"1001\")    // client id\n\t\t\t.setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")    // client 秘钥\n\t\t\t.addAllowRedirectUris(\"*\")    // 所有允许授权的 url\n\t\t\t.addContractScopes(\"openid\", \"userid\", \"userinfo\")    // 所有签约的权限\n\t\t\t.addAllowGrantTypes(\t // 所有允许的授权模式\n\t\t\t\t\tGrantType.authorization_code, // 授权码式\n\t\t\t\t\tGrantType.implicit,  // 隐式式\n\t\t\t\t\tGrantType.refresh_token,  // 刷新令牌\n\t\t\t\t\tGrantType.password,  // 密码式\n\t\t\t\t\tGrantType.client_credentials,  // 客户端模式\n\t\t\t)\n\t)\n\t\n\t// 可以添加更多 client 信息，只要保持 clientId 唯一就行了\n\t// oauth2Server.addClient(...)\n\n}\n```\n\n你也可以在 `application.yml` 配置中 `client` 信息：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# sa-token配置\nsa-token: \n    # OAuth2.0 配置 \n    oauth2-server:\n\t\t# client 列表 \n        clients:\n            # 客户端1\n            1001:\n                # 客户端id\n                client-id: 1001\n                # 客户端秘钥\n                client-secret: aaaa-bbbb-cccc-dddd-eeee\n                # 所有允许授权的 url\n                allow-redirect-uris:\n                  - http://sa-oauth-client.com:8002\n                  - http://sa-oauth-client.com:8002/*\n                # 所有签约的权限\n                contract-scopes:\n                  - openid\n                  - userid\n                  - userinfo\n                # 所有允许的授权模式\n                allow-grant-types:\n                  - authorization_code\n                  - implicit\n                  - refresh_token\n                  - password\n                  - client_credentials\n            # 客户端2\n            1002:\n                # 客户端id\n                client-id: 1002\n\t\t\t\t# 更多配置 ...\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n########### 客户端1\n# 客户端id\nsa-token.oauth2-server.clients.1001.client-id=1001\n# 客户端秘钥\nsa-token.oauth2-server.clients.1001.client-secret=aaaa-bbbb-cccc-dddd-eeee\n# 所有允许授权的 url\nsa-token.oauth2-server.clients.1001.allow-redirect-uris[0]=http://sa-oauth-client.com:8002\nsa-token.oauth2-server.clients.1001.allow-redirect-uris[1]=http://sa-oauth-client.com:8002/*\n# 所有签约的权限\nsa-token.oauth2-server.clients.1001.contract-scopes[0]=openid\nsa-token.oauth2-server.clients.1001.contract-scopes[1]=userid\nsa-token.oauth2-server.clients.1001.contract-scopes[2]=userinfo\n# 所有允许的授权模式\nsa-token.oauth2-server.clients.1001.allow-grant-types[0]=authorization_code\nsa-token.oauth2-server.clients.1001.allow-grant-types[1]=implicit\nsa-token.oauth2-server.clients.1001.allow-grant-types[2]=refresh_token\nsa-token.oauth2-server.clients.1001.allow-grant-types[3]=password\nsa-token.oauth2-server.clients.1001.allow-grant-types[4]=client_credentials\n\n########### 客户端2\nsa-token.oauth2-server.clients.1002.client-id=1002\nsa-token.oauth2-server.clients.1002.client-secret=...\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n这两种方案都是基于内存形式的 client 信息配置，只适合简单的测试，一般真实项目的 client 信息都是保存在数据库中的，下面演示一下如何在数据库中动态获取 client 信息\n\n\n### 2、基于数据库的数据加载\n\n你只需要自定义数据加载器：新建 `SaOAuth2DataLoaderImpl` 实现 `SaOAuth2DataLoader` 接口。\n\n\n``` java\n/**\n * Sa-Token OAuth2：自定义数据加载器\n */\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t\n\t// 根据 clientId 获取 Client 信息\n\t@Override\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\t// 此为模拟数据，真实环境需要从数据库查询 \n\t\tif(\"1001\".equals(clientId)) {\n\t\t\treturn new SaClientModel()\n\t\t\t\t\t.setClientId(\"1001\")    // client id\n\t\t\t\t\t.setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")    // client 秘钥\n\t\t\t\t\t.addAllowRedirectUris(\"*\")    // 所有允许授权的 url\n\t\t\t\t\t.addContractScopes(\"openid\", \"userid\", \"userinfo\")    // 所有签约的权限\n\t\t\t\t\t.addAllowGrantTypes(\t // 所有允许的授权模式\n\t\t\t\t\t\t\tGrantType.authorization_code, // 授权码式\n\t\t\t\t\t\t\tGrantType.implicit,  // 隐式式\n\t\t\t\t\t\t\tGrantType.refresh_token,  // 刷新令牌\n\t\t\t\t\t\t\tGrantType.password,  // 密码式\n\t\t\t\t\t\t\tGrantType.client_credentials  // 客户端模式\n\t\t\t\t\t)\n\t\t\t;\n\t\t}\n\t\treturn null;\n\t}\n\t\n\t// 根据 clientId 和 loginId 获取 openid\n\t@Override\n\tpublic String getOpenid(String clientId, Object loginId) {\n\t\t// 此处使用框架默认算法生成 openid，真实环境建议改为从数据库查询\n\t\treturn SaOAuth2DataLoader.super.getOpenid(clientId, loginId);\n\t}\n\n}\n``` \n\n此种形式更加灵活，后续文档将默认按照此种形式来展示示例。\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-dev.md",
    "content": "# Sa-Token-OAuth2 Server端 二次开发用到的所有函数说明 \n\n官方示例只提供了基本的授权流程，以及 userinfo 资源的开放，如果您需要开放更多的接口，则二次开发时可能用到以下相关 API 方法 \n\n--- \n\n### Client 信息相关\n\n``` java\n// 获取 ClientModel，根据 clientId\nSaOAuth2Util.getClientModel(clientId);\n\n// 校验 clientId 信息并返回 ClientModel，如果找不到对应 Client 信息则抛出异常\nSaOAuth2Util.checkClientModel(clientId);\n\n// 校验：clientId 与 clientSecret 是否正确\nSaOAuth2Util.checkClientSecret(clientId, clientSecret);\n\n// 校验：clientId 与 clientSecret 是否正确，并且是否签约了指定 scopes\nSaOAuth2Util.checkClientSecretAndScope(clientId, clientSecret, scopes);\n\n// 判断：该 Client 是否签约了指定的 Scope，返回 true 或 false\nSaOAuth2Util.isContractScope(clientId, scopes);\n\n// 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常\nSaOAuth2Util.checkContractScope(clientId, scopes);\n\n// 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常\nSaOAuth2Util.checkContractScope(clientModel, scopes);\n\n// 校验：该 Client 使用指定 url 作为回调地址，是否合法\nSaOAuth2Util.checkRedirectUri(clientId, url);\n\n// 判断：指定 loginId 是否对一个 Client 授权给了指定 Scope\nSaOAuth2Util.isGrantScope(loginId, clientId, scopes);\n\n// 删除：指定 loginId 针对指定 Client 的授权信息\nSaOAuth2Util.deleteGrantScope(loginId, clientId);\n```\n\n\n### Code 相关\n``` java\n// 获取 CodeModel，无效的 code 会返回 null\nSaOAuth2Util.getCode(code);\n\n// 校验 Code，成功返回 CodeModel，失败则抛出异常\nSaOAuth2Util.checkCode(code);\n\n// 获取 Code，根据索引： clientId、loginId\nSaOAuth2Util.getCodeValue(clientId, loginId);\n```\n\n\n### Access-Token 相关\n``` java\n// 获取 AccessTokenModel，无效的 AccessToken 会返回 null\nSaOAuth2Util.getAccessToken(accessToken);\n\n// 校验 Access-Token，成功返回 AccessTokenModel，失败则抛出异常\nSaOAuth2Util.checkAccessToken(accessToken);\n\n// 获取 Access-Token 列表：此应用下 对 某个用户 签发的所有 Access-token\nSaOAuth2Util.getAccessTokenValueList(clientId, loginId);\n\n// 判断：指定 Access-Token 是否具有指定 Scope 列表，返回 true 或 false\nSaOAuth2Util.hasAccessTokenScope(accessToken, ...scopes);\n\n// 校验：指定 Access-Token 是否具有指定 Scope 列表，如果不具备则抛出异常\nSaOAuth2Util.checkAccessTokenScope(accessToken, ...scopes);\n\n// 获取 Access-Token 所代表的LoginId\nSaOAuth2Util.getLoginIdByAccessToken(accessToken);\n\n// 获取 Access-Token 所代表的 clientId\nSaOAuth2Util.getClientIdByAccessToken(accessToken);\n\n// 回收一个 Access-Token\nSaOAuth2Util.revokeAccessToken(accessToken);\n\n// 回收全部 Access-Token：指定应用下 指定用户 的全部 Access-Token\nSaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId);\n```\n\n\n### Refresh-Token 相关\n``` java\n// 获取 RefreshTokenModel，无效的 RefreshToken 会返回 null\nSaOAuth2Util.getRefreshToken(refreshToken);\n\n// 校验 Refresh-Token，成功返回 RefreshTokenModel，失败则抛出异常\nSaOAuth2Util.checkRefreshToken(refreshToken);\n\n// 获取 Refresh-Token 列表：此应用下 对 某个用户 签发的所有 Refresh-Token\nSaOAuth2Util.getRefreshTokenValueList(clientId, loginId);\n\n// 回收一个 Refresh-Token\nSaOAuth2Util.revokeRefreshToken(refreshToken);\n\n// 回收全部 Refresh-Token：指定应用下 指定用户 的全部 Refresh-Token\nSaOAuth2Util.revokeRefreshTokenByIndex(clientId, loginId);\n\n// 根据 RefreshToken 刷新出一个 AccessToken\nSaOAuth2Util.refreshAccessToken(refreshToken);\n```\n\n\n### Client-Token 相关\n\n``` java\n// 获取 ClientTokenModel，无效的 ClientToken 会返回 null\nSaOAuth2Util.getClientToken(clientToken);\n\n// 校验 Client-Token，成功返回 ClientTokenModel，失败则抛出异常\nSaOAuth2Util.checkClientToken(clientToken);\n\n// 获取 Client-Token 列表：此应用下 对 某个用户 签发的所有 Client-token\nSaOAuth2Util.getClientTokenValueList(clientId);\n\n// 判断：指定 Client-Token 是否具有指定 Scope 列表，返回 true 或 false\nSaOAuth2Util.hasClientTokenScope(clientToken, ...scopes);\n\n// 校验：指定 Client-Token 是否具有指定 Scope 列表，如果不具备则抛出异常\nSaOAuth2Util.checkClientTokenScope(clientToken, ...scopes);\n\n// 回收一个 ClientToken\nSaOAuth2Util.revokeClientToken(clientToken);\n\n// 回收全部 Client-Token：指定应用下的全部 Client-Token\nSaOAuth2Util.revokeClientTokenByIndex(clientId);\n```\n\n\n### 请求查询\n\n``` java\n// 数据读取：从当前请求对象中读取 access_token，并查询到 AccessTokenModel 信息，无效 access_token 抛出异常\n// 1、请求参数 access_token，2、请求头 Authorization Bearer access_token\nSaOAuth2Util.currentAccessToken();\n\n// 数据读取：从当前请求对象中读取 client_token，并查询到 ClientTokenModel 信息，无效 client_token 抛出异常\n// 1、请求参数 client_token，2、请求头 Authorization Bearer client_token\nSaOAuth2Util.currentClientToken();\n```\n\n\n\n详情请参考源码：[码云：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)\n\n\n### OAuth2-Server 所有可重写策略\n\n\n#### 权限处理器\n``` java\n// 根据 scope 信息对一个 AccessTokenModel 进行加工处理\nSaOAuth2Strategy.instance.workAccessTokenByScope = at -> {\n\t// ... \n}\n\n// 当使用 RefreshToken 刷新 AccessToken 时，根据 scope 信息对一个 AccessTokenModel 进行加工处理\nSaOAuth2Strategy.instance.refreshAccessTokenWorkByScope = at -> {\n\t// ... \n}\n\n// 根据 scope 信息对一个 ClientTokenModel 进行加工处理\nSaOAuth2Strategy.instance.workClientTokenByScope = at -> {\n\t// ... \n}\n```\n\n\n#### grant_type 处理器\n``` java\n// 根据 grantType 构造一个 AccessTokenModel\nSaOAuth2Strategy.instance.grantTypeAuth = req -> {\n\t// ... \n}\n```\n\n\n#### 凭证创建\n``` java\n// 创建一个 code value\nSaOAuth2Strategy.instance.createCodeValue = (clientId, loginId, scopes) -> {\n\t// ... \n}\n\n// 创建一个 AccessToken value\nSaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> {\n\t// ... \n}\n\n// 创建一个 RefreshToken value\nSaOAuth2Strategy.instance.createRefreshToken = (clientId, loginId, scopes) -> {\n\t// ... \n}\n\n// 创建一个 ClientToken value\nSaOAuth2Strategy.instance.createClientToken = (clientId, scopes) -> {\n\t// ... \n}\n```\n\n\n#### 认证流程回调\n``` java\n// OAuth-Server端：未登录时返回的View\nSaOAuth2Strategy.instance.notLoginView = () -> {\n\t// ... \n}\n\n// OAuth-Server端：确认授权时返回的View\nSaOAuth2Strategy.instance.confirmView = (clientId, scopes) -> {\n\t// ... \n}\n\n// OAuth-Server端：登录函数\nSaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> {\n\t// ... \n}\n\n// OAuth-Server端：用户在授权指定 client 前的检查，如果检查不通过，请直接抛出异常\nSaOAuth2Strategy.instance.userAuthorizeClientCheck = (loginId, clientId) -> {\n\t// ... \n}\n```\n\n\n#### 其它\n``` java\n// 在创建 SaClientModel 时，设置其默认字段\nSaOAuth2Strategy.instance.setSaClientModelDefaultFields = (clientModel) -> {\n\t// ... \n}\n```\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-h5.md",
    "content": "# OAuth2-Server 端前后台分离\n\n### 1、设计分析\n\n要使 OAuth2-Server 端做到前后台分离，则需要对接口进行一部分改造：\n\n- 改造前的接口列表：\n\t- `http:{后端主机}/oauth2/authorize`\n\t- `http:{后端主机}/oauth2/token`\n\t- `http:{后端主机}/oauth2/refresh`\n\t- 更多...\n- 改造后的接口列表：\n\t- `http:{前端主机}/oauth2/authorize`\n\t- `http:{后端主机}/oauth2/token`\n\t- `http:{后端主机}/oauth2/refresh`\n\t- 更多...\n\n也就是，只需要重点改造 `/oauth2/authorize` 一个接口即可，`/oauth2/authorize` 接口主要做了三件事：\n1. 判断用户在 oauth2-server 端是否登录，未登录会进入 [登录页面]，已登录则进入下一步。\n2. 判断应用请求的 scope 是否需要用户手动确认授权，需要会进入 [确认授权页面]，不需要则进入下一步。\n3. 重定向至 `redirect_uri` 指定的 url 地址，并携带 code 授权码参数。\n\n我们只需要把上述逻辑从 oauth2-server 的后端搬到 oauth2-server 的前端即可。\n\n\n### 2、OAuth2-Server 后端添加接口\n\n首先在 `oauth2-server` 的后端添加一个接口，用于获取最终授权重定向地址：\n\n``` java\n/**\n * Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口)\n */\n@RestController\npublic class SaOAuth2ServerH5Controller {\n\n    /**\n     * 获取最终授权重定向地址，形如：http://xxx.com/xxx?code=xxxxx\n     *\n     * <p> 情况1：客户端未登录，返回 code=401，提示用户登录 <p/>\n     * <p> 情况2：请求的 scope 需要客户端手动确认授权，返回 code=411，提示用户手动确认 <p/>\n     * <p> 情况3：已登录且请求的 scope 已确认授权，返回 code=200，redirect_uri=最终重定向 url 地址(携带code码参数) <p/>\n     *\n     * @return /\n     */\n    @PostMapping(\"/oauth2/getRedirectUri\")\n    public Object getRedirectUri() {\n\n        // 获取变量\n        SaRequest req = SaHolder.getRequest();\n        SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();\n        SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();\n        SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n        String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type);\n\n        // 1、先判断是否开启了指定的授权模式\n        SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg);\n\n        // 2、如果尚未登录, 则先去登录\n        long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L);\n        if(loginId == 0L) {\n            return SaResult.get(401, \"need login\", null);\n        }\n\n        // 3、构建请求 Model\n        RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);\n\n        // 4、开发者自定义的授权前置检查\n        SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId);\n\n        // 5、校验：重定向域名是否合法\n        oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri);\n\n        // 6、校验：此次申请的Scope，该Client是否已经签约\n        oauth2Template.checkContractScope(ra.clientId, ra.scopes);\n\n        // 7、判断：如果此次申请的Scope，该用户尚未授权，则转到授权页面\n        boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);\n        if(isNeedCarefulConfirm) {\n            SaClientModel cm = oauth2Template.checkClientModel(ra.clientId);\n            if( ! cm.getIsAutoConfirm()) {\n                // code=411，需要用户手动确认授权\n                return SaResult.get(411, \"need confirm\", null);\n            }\n        }\n\n        // 8、判断授权类型，重定向到不同地址\n        // \t\t如果是 授权码式，则：开始重定向授权，下放code\n        if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) {\n            CodeModel codeModel = dataGenerate.generateCode(ra);\n            String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);\n            return SaResult.ok().set(\"redirect_uri\", redirectUri);\n        }\n\n        // \t\t如果是 隐藏式，则：开始重定向授权，下放 token\n        if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) {\n            AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null);\n            String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);\n            return SaResult.ok().set(\"redirect_uri\", redirectUri);\n        }\n\n        // 默认返回\n        throw new SaOAuth2Exception(\"无效 response_type: \" + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);\n    }\n\n}\n```\n\n### 3、新建前端项目\n\n既然是前后台分离，那肯定要有一个独立的前端项目，所需代码比较冗长，不便于在文档处直接展示，大家可以参考在线仓库示例：\n\n[sa-token-demo-oauth2-server-h5/](https://gitee.com/dromara/sa-token/blob/dev/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/)\n\n\n### 4、运行测试\n\n在前端 ide 中导入 demo 案例的 `sa-token-demo-oauth2-server-h5` 项目，然后直接预览 `oauth2-authorize.html` 页面，如图所示：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/sa-oauth2-server-authorize-h5.png\" alt=\"sa-oauth2-server-authorize-h5.png\" />\n\n复制上述地址，然后将其配置到 “OAuth2前端测试页” 的 “OAuth2 Server 授权页地址” 选项中，其它选项保持默认不变：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/sa-oauth2-client-test-h5-page-setting.png\" alt=\"sa-oauth2-client-test-h5-page-setting.png\" />\n\n然后根据 “OAuth2前端测试页” 的页面提示进行测试即可，此处不再赘述。\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-interworking.md",
    "content": "# OAuth2 与登录会话实现数据互通\n\n--- \n\n### 前提\n\n前提，我们：\n- 把 OAuth2 模块生成的令牌称作资源令牌（access_token），\n- 把 StpUtil 登录会话生成的令牌称作会话令牌（satoken）。\n\n正常情况下，资源令牌 与 会话令牌 的数据是不互通的，具体表现就是：当我们拿着 access_token 去访问 satoken 令牌的接口，会被抛出异常：`无效Token：xxxxx`\n\n那么，有什么办法可以做到这两个模块的数据互通呢？\n\n\n\n### OAuth2-Server 端数据互通\n\n很简单，你只需要在 `configOAuth2Server` 中重写 Access-Token 的生成策略：\n\n``` java\n// Sa-Token OAuth2 定制化配置 \n@Autowired\npublic void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t// 其它配置 ...\n\t\n\t// 重写 AccessToken 创建策略，返回会话令牌\n\tSaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> {\n\t\tSystem.out.println(\"----返回会话令牌\");\n\t\treturn StpUtil.getOrCreateLoginSession(loginId);\n\t};\n\n}\n```\n\n重启项目，然后在 OAuth2 模块授权登录，现在生成的 `access_token` ，可以用来访问 `satoken` 的会话接口了。\n\n\n> [!WARNING| label:注意点] \n> 数据互通，让前端与后端的交互更加方便，一个 token 即可访问所有接口，但也一定程度上失去了OAuth2的 “不同 Client 不同权限” 的设计意义，\n> 同时也默认每个 Client 都拥有了账号的会话权限（access_token 与 satoken 为同一个）。\n> \n> 应该根据自己的架构合理分析是否应该整合数据互通。\n\n\n\n### OAuth2-Client 数据互通\n除了Server端，Client端也可以打通 `access_token` 与 `satoken` 会话。做法是在 Client 端拿到 `access_token` 后进行登录时，使用 `SaLoginParameter` 预定登录生成的 Token 值 \n\n``` java\n// 1. 获取到access_token\nString access_token = ...\n\n// 2. 登录时预定生成的token \nStpUtil.login(uid, new SaLoginParameter().setToken(access_token));\n\n// 3. 其它代码...\n```\n\n\n\n**疑问：数据互通后，两个 token 的过期策略是什么？**\n\n会话 token 由 `sa-token.timeout` 决定，`access_token` 由 `sa-token.oauth2-server.access-token-timeout` 决定。\n\n数据互通只是将 token 拷贝一份进行复用，动作完成之后两者不再有任何联系。\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-oidc.md",
    "content": "# OAuth2 开启 OIDC 协议 （OpenID Connect）\n\n--- \n\n### 1、开启步骤\n\n1、引入 `sa-token-jwt` 依赖，用来签发 `id_token` \n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml\n<!-- sa-token-jwt 签发 OIDC id_token 令牌 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-jwt</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// sa-token-jwt 签发 OIDC id_token 令牌 \nimplementation 'cn.dev33:sa-token-jwt:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n2、在 `SaOAuth2DataLoader` 实现类中，返回的 `SaClientModel` 中添加 `oidc` 的签约权限。\n\n``` java\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t@Override\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\t// 此为模拟数据，真实环境需要从数据库查询 \n\t\tif(\"1001\".equals(clientId)) {\n\t\t\treturn new SaClientModel()\n\t\t\t\t\t// .... \n\t\t\t\t\t.addContractScopes(\"openid\", \"userid\", \"userinfo\", \"oidc\")    // 此处添加上签约权限：oidc \n\t\t\t\t\t.addAllowGrantTypes(\t\n\t\t\t\t\t\t\t// ... \n\t\t\t\t\t)\n\t\t\t;\n\t\t}\n\t\treturn null;\n\t}\n\t// 其它代码 ... \n}\n```\n\n3、在 `application.yml` 配置文件中配置 jwt 生成秘钥：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token:\n\t# jwt秘钥 \n\tjwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# jwt秘钥 \nsa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n```\n<!---------------------------- tabs:end ---------------------------->\n\n注：为了安全起见请不要直接复制官网示例这个字符串（随便按几个字符就好了）\n\n\n\n### 2、测试\n\n1、在 OAuth2-Client 端申请授权码时，添加上 `oidc` 权限：\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/authorize\n\t?response_type=code\n\t&client_id=1001\n\t&redirect_uri=http://sa-oauth-client.com:8002/\n\t&scope=oidc\n```\n\n2、得到授权码后，然后拿着 `code` 换 `access_token`\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/token\n    ?grant_type=authorization_code\n    &client_id=1001\n    &client_secret=aaaa-bbbb-cccc-dddd-eeee\n    &code=${code}\n```\n\n3、返回的结果中将包含 `id_token` 字段：\n``` js\n{\n\t\"code\": 200,\n\t\"msg\": \"ok\",\n\t\"data\": null,\n\t\"token_type\": \"bearer\",\n\t\"access_token\": \"WdpjZdGlXdOzsAcr7gqPwmLVInHrhpznQa2pDOVqZmLXQynBflkcWqE6f5o2\",\n\t\"refresh_token\": \"hKHwBm3eH6iqSHlXRGWQaziV8OoyHvzmUb97lKEEZnZJLt3NunBFx7rVZWbT\",\n\t\"expires_in\": 7199,\n\t\"refresh_expires_in\": 2591999,\n\t\"client_id\": \"1001\",\n\t\"scope\": \"oidc\",\n\t\"id_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzI0NDI1OTg5LCJpYXQiOjE3MjQ0MjUzODksImF1dGhfdGltZSI6MTcyNDQwMDUyNiwibm9uY2UiOiJLTHlNR08zZ1R0YVdhMEFRcHF0RUNpTk9SWkY1QkhvRCIsImF6cCI6IjEwMDEifQ.gP3UYMexaQ9v0huKUuqhV9-dPxPpaEuFPIlPb2UZaOI\"\n}\n```\n\n4、解析 `id_token` 将得到以下载荷\n``` js\n{\n  \"iss\": \"http://sa-oauth-server.com:8000\",  // 签发人\n  \"sub\": \"10001\",   // userId \n  \"aud\": \"1001\",   // clientId\n  \"exp\": 1724425989,   // 令牌到期时间，10位时间戳 \n  \"iat\": 1724425389,   // 签发此令牌的时间，10位时间戳 \n  \"auth_time\": 1724400526,   // 用户认证时间，10位时间戳 \n  \"nonce\": \"KLyMGO3gTtaWa0AQpqtECiNORZF5BHoD\",  // 随机数，防止重放攻击 \n  \"azp\": \"1001\"   // clientId\n}\n```\n\n如果默认携带的载荷无法满足你的业务需求，你还可以自定义追加扩展字段，让 `id_token` 返回更多信息 \n\n\n### 3、扩展 id_token 载荷\n\n新建 `CustomOidcScopeHandler` 集成 `OidcScopeHandler`，扩展 OIDC 权限处理器，返回更多字段：\n``` java\n/**\n * 扩展 OIDC 权限处理器，返回更多字段\n */\n@Component\npublic class CustomOidcScopeHandler extends OidcScopeHandler {\n\n    @Override\n    public IdTokenModel workExtraData(IdTokenModel idToken) {\n        Object userId = idToken.sub;\n        System.out.println(\"----- 为 idToken 追加扩展字段 ----- \");\n\n        idToken.extraData.put(\"uid\", userId); // 用户id\n        idToken.extraData.put(\"nickname\", \"lin_xiao_lin\"); // 昵称\n        idToken.extraData.put(\"picture\", \"https://sa-token.cc/logo.png\"); // 头像\n        idToken.extraData.put(\"email\", \"456456@xx.com\"); // 邮箱\n        idToken.extraData.put(\"phone_number\", \"13144556677\"); // 手机号\n        // 更多字段 ...\n        // 可参考：https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\n\n        return idToken;\n    }\n\n}\n```\n\n重启项目，再次请求授权，返回的 `id_token` 载荷将包含更多字段：\n\n``` js\n{\n  \"iss\": \"http://sa-oauth-server.com:8000\",\n  \"sub\": \"10001\",\n  \"aud\": \"1001\",\n  \"exp\": 1724430149,\n  \"iat\": 1724429549,\n  \"auth_time\": 1724400526,\n  \"nonce\": \"SBRLOcfeo9FFmLTB8OINvuulam5FMOre\",\n  \"azp\": \"1001\",\n  \"uid\": \"10001\",\n  \"nickname\": \"lin_xiao_lin\",\n  \"picture\": \"https://sa-token.cc/logo.png\",\n  \"email\": \"456456@xx.com\",\n  \"phone_number\": \"13144556677\"\n}\n```\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-openid.md",
    "content": "# OpenId 与 UnionId\n\n<p><a class=\"case-btn case-btn-video\" href=\"https://www.bilibili.com/video/BV1oz6AY5ERJ/\" target=\"_blank\">\n\t参考视频：OAuth2 授权流程中的 clientId、openId、unionId、userId 都是干嘛的？\n</a></p>\n\n\n### 1、OpenId \n\nopenid 是用户在某一 client 下的唯一标识，其有如下特点：\n\n- 一个用户在同一个 client 下，openid 是固定的，每次请求都会返回相同的值。\n- 一个用户在不同的 client 下，openid 是不同的，会返回不同的值。\n\noauth2-client 在每次授权时可根据返回的 openid 值来确定用户身份。\n\n框架默认的 openid 生成算法为：\n``` java\nmd5(prefix + \"_\" + clientId + \"_\" + loginId);\n```\n\n其中的 prefix 前缀默认值为：`openid_default_digest_prefix`，你可以通过以下方式配置：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# sa-token配置\nsa-token:\n\toauth2-server:\n\t\t# 默认 openid 生成算法中使用的摘要前缀\n\t\topenid-digest-prefix: xxxxxx\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 默认 openid 生成算法中使用的摘要前缀\nsa-token.oauth2-server.openid-digest-prefix=xxxxxx\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 OpenId 生成算法：\n\n``` java\n/**\n * Sa-Token OAuth2：自定义数据加载器\n */\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t\n\t// 自定义 openid 生成算法 \n\t@Override\n\tpublic String getOpenid(String clientId, Object loginId) {\n\t\t// 此种写法代表使用框架默认算法生成 openid，真实环境建议改为从数据库查询\n\t\treturn SaOAuth2DataLoader.super.getOpenid(clientId, loginId);\n\t}\n\n}\n``` \n\n\n#### openid 算法要求\n\n\n正常来讲，openid 算法需要保证：\n\n1. 单个 clientId 下同一 loginId 生成的 `openid` 一致。[必须]\n2. 多个 clientId 下同一 loginId 生成的 `openid` 不一致。[非常建议]\n3. 客户端无法通过 clientId + loginId 推测 `openid` 值。[建议]\n4. 客户端无法通过 clientId + loginId + openid 推测该 loginId 在其它 clientId 下的 `openid` 值。[建议]\n5. oauth2-server 自身由 `openid` 可以反查出对应的 clientId 和 loginId。[根据业务需求而定是否满足]\n\n框架内置的算法，可以满足 1和2，如果自定义了 `sa-token.oauth2-server.openid-digest-prefix` 配置，可以满足3。\n\n如果自定义配置的 prefix 长度较短，或比较简单呈现规律性，则有客户端根据 clientId + loginId + openid 穷举爆破出 `prefix` 的风险，\n从而获得提前计算彩虹表来推测出其它 clientId、loginId 对应 openid 值的能力。\n\n如果自定义的 prefix 前缀比较复杂，让客户端无法爆破，则可以满足4。但依然无法满足5。\n\n所以 openid 算法的最优解，应该是 oauth2-server 采用随机字符串作为 openid，然后自建数据库表来维护其映射关系，这样可以同时满足12345。\n\n表结构参考如下：\n\n- id：数据id，主键。\n- client_id：应用id。\n- user_id：用户账号id。\n- openid：对应的 openid 值，随机字符串。\n- create_time：数据创建时间。\n- xxx：其它需要扩展的字段。\n\n\n\n### 2、UnionId \n\n`UnionId` 的特点与 `OpenId` 几乎一致：同一用户在不同 client 里的 UnionId 值是不同的，除非这些应用属于同一主体。\n\n例如：甲公司申请了`应用A`、`应用B`、`应用C`，乙公司申请了`应用D`、`应用F`，那么用户张三：\n- 在应用 A、B、C 里的 UnionId 值一致。\n- 在应用 D、F 里的 UnionId 值一致。\n- 在应用 A 和 应用 D 之间，UnionId 值不一致。\n\n那么 Sa-Token 框架是如何识别到某两个应用是否为同一主体的呢？这就需要你在注册应用时指定 `subjectId` 属性了：\n\n``` java\n/**\n * Sa-Token OAuth2：自定义数据加载器\n */\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n    \n    // 根据 clientId 获取 Client 信息\n    @Override\n    public SaClientModel getClientModel(String clientId) {\n        // 此为模拟数据，真实环境需要从数据库查询 \n        if(\"1001\".equals(clientId)) {\n            return new SaClientModel()\n\t\t\t\t\t.setClientId(\"xxxx\")  \n\t\t\t\t\t.setClientSecret(\"xxxx\")   \n\t\t\t\t\t.setSubjectId(\"1000001\")   // ⚠️ 关键代码：主体 id (可选)\n\t\t\t\t\t// ....\n            ;\n        }\n        return null;\n    }\n    \n}\n```\n\n`subjectId` 代表此应用的拥有者，相同 `subjectId` 值的应用将被识别为同一主体，在授权中返回的 `unionid` 值也将一致。\n\n框架默认的 `unionid` 生成算法为：\n\n``` java\nmd5(prefix + \"_\" + subjectId + \"_\" + loginId);\n```\n\n其中的 prefix 前缀默认值为：`unionid_default_digest_prefix`，你可以通过以下方式配置：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# sa-token配置\nsa-token:\n\toauth2-server:\n\t\t# 默认 unionid 生成算法中使用的摘要前缀\n\t\tunionid-digest-prefix: xxxxxx\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 默认 unionid 生成算法中使用的摘要前缀\nsa-token.oauth2-server.unionid-digest-prefix=xxxxxx\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 UnionId 生成算法：\n\n``` java\n/**\n * Sa-Token OAuth2：自定义数据加载器\n */\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t\n\t// 自定义 unionid 生成算法 \n\t@Override\n\tpublic String getUnionid(String subjectId, Object loginId) {\n\t\t// 此种写法代表使用框架默认算法生成 unionid，真实环境建议改为从数据库查询\n\t\treturn SaOAuth2DataLoader.super.getUnionid(subjectId, loginId);\n\t}\n\n}\n``` \n\nunionid 算法要求与 openid 基本一致，可参考上述 openid 算法要求介绍，此处暂不赘述。\n\n\n\n\n### 3、总结\n\n| 类型\t\t\t| 概念\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| userid       \t| 在 oauth2-server 端的用户，其唯一标识\t\t\t\t\t\t\t|\n| clientid     \t| 第三方公司在 oauth2-server 开放平台申请的应用，其唯一标识\t\t\t|\n| openid       \t| 用户在某个应用下的唯一标识\t\t\t\t\t\t\t\t\t\t|\n| unionid      \t| 用户在某一组应用下的唯一标识\t（按照主体id分组）\t\t\t\t\t|\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-questions.md",
    "content": "# Sa-Token-OAuth2整合-常见问题总结\n\nOAuth2 集成常见问题整理\n\n[[toc]]\n\n--- \n\n\n### 问：搭建好 oauth2-server 服务后，访问返回：`{\"msg\": \"not handle\"}`。\n\n返回这个信息，代表你访问的路由有错误，比如说：\n\n- 统一认证登录地址是：`http://{host}:{port}/oauth2/authorize`。\n- 而你访问的却是：`http://{host}:{port}/oauth2/authorize2`。\n\n地址写错了，框架就不会处理这个请求，会直接返回 `{\"msg\": \"not handle\"}`，所有开放地址可参考：[OAuth2 开放接口](/oauth2/oauth2-apidoc)\n\n如果仔细检查地址后没有写错，却依然返回了这个信息，那有可能是对应的接口没有打开，比如说：\n\n- sso-server 端的单点注销地址：`http://{host}:{port}/sso/signout`；\n- sso-client 端的注销地址：`http://{host}:{port}/sso/logout`；\n\n都需要在配置文件配置：`sa-token.sso.is-slo=true`后，才会打开。\n\n\n\n### 问：我参照文档搭建 oauth2-server，一直提示：code 无效，请问怎么回事？\n一个 code 码只能使用一次，多次使用就会报这个错。\n\n\n\n### 问：Sa-Token-OAuth2 怎么集成多账号模式？\n\n在 `configOAuth2Server` 里指定 oauth2 模块使用的 `StpLogic` 对象即可： \n\n``` java\n// Sa-Token OAuth2 定制化配置\n@Autowired\npublic void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t// 其它配置 ... \n\t\n\t// 指定 oauth2 模块使用的 `StpLogic` 对象 \n\tSaOAuth2Manager.setStpLogic(StpUserUtil.stpLogic);\n}\n```\n\n\n\n\n### 问：授权码流程中 state 参数是干吗用的？\n\nstate 参数用于验证授权码流程的发起端和接受端是否为同一个客户端，以防止OAuth-Server账号伪装攻击。\n\n授权流程发起端必须保证：\n- state 参数必须足够随机，不可被预测。\n- state 参数与授权码流程发起客户端 一 一 对 应，授权流程发起时创建的 state 必须与接受时返回的 state 值一致。\n- 安全起见，一个 state 参数只允许使用一次。\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-scope-level.md",
    "content": "# OAuth2 - 为 Scope 划分等级\n\n\n### 1、划分等级 \n\n我们可以通过配置文件来为 scope 划分等级\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# sa-token 配置\nsa-token: \n    # OAuth2.0 配置 \n    oauth2-server:\n        # 定义哪些 scope 是高级权限，多个用逗号隔开\n        higher-scope: openid,userid\n        # 定义哪些 scope 是低级权限，多个用逗号隔开\n        lower-scope: userinfo\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 定义哪些 scope 是高级权限，多个用逗号隔开\nsa-token.oauth2-server.higher-scope=openid,userid\n# 定义哪些 scope 是低级权限，多个用逗号隔开\nsa-token.oauth2-server.lower-scope=userinfo\n```\n<!---------------------------- tabs:end ---------------------------->\n\n如上所示：\n- 通过 `sa-token.oauth2-server.higher-scope` 配置项指定的 `scope` 将变成 **高级权限**。\n- 通过 `sa-token.oauth2-server.lower-scope` 配置项指定的 `scope` 将变成 **低级权限**。\n- 其它未指定的 `scope` 将默认为 **一般权限**。\n\n不同的权限等级其差异主要表现在：oauth2-client 授权时是否需要用户手动确认授权。\n\n| 权限等级\t\t| 申请授权时表现\t\t\t\t\t\t| \n| :--------\t\t| :--------\t\t\t\t\t\t\t| \n| 高级权限\t\t| 申请授权时：每次都需要用户手动点击确认授权按钮，才会下放 code 授权码\t| \n| 一般权限\t\t| 申请授权时：如果申请的 scope 用户近期授权过，则静默授权，如果近期未授权过，则需要手动点击确认授权按钮\t\t| \n| 低级权限\t\t| 申请授权时：不需要用户手动点击确认授权，程序自动完成静默授权\t\t\t\t\t\t| \n\n\n### 2、详细举例\n\n1、如下例子，oauth2-client 申请的 `openid` 权限为**高级权限**，每次都需要用户手动点击确认授权按钮，才会下放 code 授权码。\n\n``` url\nhttp://{host}:{port}/oauth2/authorize\n\t?response_type=code\n\t&client_id=1001\n\t&redirect_uri=http://sa-oauth-client.com:8002/\n\t&scope=openid\n```\n\n2、如下例子，oauth2-client 申请的 `userinfo` 权限为**低级权限**，此时不需要用户手动点击确认授权，程序自动完成静默授权。\n\n``` url\nhttp://{host}:{port}/oauth2/authorize\n\t?response_type=code\n\t&client_id=1001\n\t&redirect_uri=http://sa-oauth-client.com:8002/\n\t&scope=userinfo\n```\n\n3、如下例子，oauth2-client 申请的 `fans_list` 权限为**一般权限**，首次申请时，需要用户手动点击确认授权，第二次再申请则是静默授权。\n\n``` url\nhttp://{host}:{port}/oauth2/authorize\n\t?response_type=code\n\t&client_id=1001\n\t&redirect_uri=http://sa-oauth-client.com:8002/\n\t&scope=fans_list\n```\n\n4、如下例子，oauth2-client 申请的 `openid,userid,userinfo,fans_list` 权限同时包括 **高级权限**、**低级权限**、**一般权限**：\n\n``` url\nhttp://{host}:{port}/oauth2/authorize\n\t?response_type=code\n\t&client_id=1001\n\t&redirect_uri=http://sa-oauth-client.com:8002/\n\t&scope=openid,userid,userinfo,fans_list\n```\n\n此时是否需要用户手动点击确认授权按钮？具体规则表现为：\n- 如果请求的 scope 列表包括高级权限，则必须用户手动点击确认授权。\n- 如果 scope 列表不包括高级权限，则将 scope 列表中的所有低级权限剔除。\n- 剔除后的 list 大小如果为零，则直接静默授权通过。\n- 剔除后的 list 大小如果不为零，则判断剩余的这些 scope 是否全部已近期授权过：\n\t- 如果是，则静默授权。\n\t- 如果否，则需要用户手动点击确认授权。\n\n\n### 3、申请高级权限时 `/oauth2/authorize` 无法通过验证\n\n由于申请高级权限时，每次都必须用户手动点击确认授权，`/oauth2/authorize` 路由接口是无法完成权限验证操作的。\n\n此时需要将构建 `redirect_uri` 的动作提前，在 `/oauth2/doConfirm` 确认授权接口时额外追加 `build_redirect_uri: true` 等参数：\n``` url\nhttp://{host}:{port}/oauth2/doConfirm\n    ?client={value}\n    &scope={value}\n    &build_redirect_uri=true\n    &response_type={value}\n    &redirect_uri={value}\n    &state={value}\n```\n\n返回结果示例：\n``` js\n{\n\tcode: 200, \n\tmsg: 'ok', \n\tdata: null,\n\tredirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC'\n}\n```\n\n其中 `redirect_uri` 参数为授权挂载code地址，直接在 ajax 回调函数中使用 `location.href=res.redirect_uri` 跳转即可。\n\n自定义确认授权视图修改参考：\n``` java\n// 授权确认视图\ncfg.confirmView = (clientId, scopes)->{\n\tString scopeStr = SaFoxUtil.convertListToString(scopes);\n\tString yesCode =\n\t\t\t\"fetch('/oauth2/doConfirm' + location.search + '&build_redirect_uri=true', {method: 'POST'})\" +\n\t\t\t\t\t\".then(res => res.json())\" +\n\t\t\t\t\t\".then(res => location.href=res.redirect_uri)\";\n\tString res = \"<p>应用 \" + clientId + \" 请求授权：\" + scopeStr + \"，是否同意？</p>\"\n\t\t\t+ \"<p>\" +\n\t\t\t\"\t\t<button onclick=\\\"\" + yesCode + \"\\\">同意</button>\" +\n\t\t\t\"\t\t<button onclick='history.back()'>拒绝</button>\" +\n\t\t\t\"</p>\";\n\treturn res;\n};\n```"
  },
  {
    "path": "sa-token-doc/oauth2/oauth2-server.md",
    "content": "# 搭建OAuth2-Server\n\n--- \n\n### 1、准备工作 \n首先修改hosts文件`(C:\\windows\\system32\\drivers\\etc\\hosts)`，添加以下IP映射，方便我们进行测试：\n``` url\n127.0.0.1 sa-oauth-server.com\n127.0.0.1 sa-oauth-client.com\n```\n\n\n### 2、引入依赖 \n创建SpringBoot项目 `sa-token-demo-oauth2-server`（不会的同学自行百度或参考仓库示例），引入 `pom.xml` 依赖：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml\n<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token OAuth2.0 模块 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-oauth2</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 整合 RedisTemplate (可选) -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n\t<groupId>org.apache.commons</groupId>\n\t<artifactId>commons-pool2</artifactId>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n\n// Sa-Token OAuth2.0 模块\nimplementation 'cn.dev33:sa-token-oauth2:${sa.top.version}'\n\n// Sa-Token 整合 RedisTemplate (可选)\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n注：Redis 相关依赖是非必须的，如果集成了 redis，可以让你更细致的观察到 sa-token-oauth2 的底层数据格式。\n\n\n### 3、开放服务 \n<!-- \n1、自定义数据加载器：新建 `SaOAuth2DataLoaderImpl` 实现 `SaOAuth2DataLoader` 接口。\n\n``` java\n/**\n * Sa-Token OAuth2：自定义数据加载器\n */\n@Component\npublic class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {\n\t\n\t// 根据 clientId 获取 Client 信息\n\t@Override\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\t// 此为模拟数据，真实环境需要从数据库查询 \n\t\tif(\"1001\".equals(clientId)) {\n\t\t\treturn new SaClientModel()\n\t\t\t\t\t.setClientId(\"1001\")    // client id\n\t\t\t\t\t.setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")    // client 秘钥\n\t\t\t\t\t.addAllowRedirectUris(\"*\")    // 所有允许授权的 url\n\t\t\t\t\t.addContractScopes(\"openid\", \"userid\", \"userinfo\")    // 所有签约的权限\n\t\t\t\t\t.addAllowGrantTypes(\t // 所有允许的授权模式\n\t\t\t\t\t\t\tGrantType.authorization_code, // 授权码式\n\t\t\t\t\t\t\tGrantType.implicit,  // 隐式式\n\t\t\t\t\t\t\tGrantType.refresh_token,  // 刷新令牌\n\t\t\t\t\t\t\tGrantType.password,  // 密码式\n\t\t\t\t\t\t\tGrantType.client_credentials  // 客户端模式\n\t\t\t\t\t)\n\t\t\t;\n\t\t}\n\t\treturn null;\n\t}\n\t\n\t// 根据 clientId 和 loginId 获取 openid\n\t@Override\n\tpublic String getOpenid(String clientId, Object loginId) {\n\t\t// 此处使用框架默认算法生成 openid，真实环境建议改为从数据库查询\n\t\treturn SaOAuth2DataLoader.super.getOpenid(clientId, loginId);\n\t}\n\n}\n``` \n\n你可以在 [框架配置](/use/config?id=SaClientModel属性定义) 了解有关 `SaClientModel` 对象所有属性的详细定义\n-->\n\n1、新建`SaOAuth2ServerController`\n``` java\n/**\n * Sa-Token OAuth2 Server端 控制器 \n */\n@RestController\npublic class SaOAuth2ServerController {\n\n\t// OAuth2-Server 端：处理所有 OAuth2 相关请求\n\t@RequestMapping(\"/oauth2/*\")\n\tpublic Object request() {\n\t\tSystem.out.println(\"------- 进入请求: \" + SaHolder.getRequest().getUrl());\n\t\treturn SaOAuth2ServerProcessor.instance.dister();\n\t}\n\n\t// Sa-Token OAuth2 定制化配置 \n\t@Autowired\n\tpublic void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {\n\t\t\n\t\t// 添加 client 信息 \n\t\toauth2Server.addClient(\n\t\t\tnew SaClientModel()\n\t\t\t\t.setClientId(\"1001\")    // client id\n\t\t\t\t.setClientSecret(\"aaaa-bbbb-cccc-dddd-eeee\")    // client 秘钥\n\t\t\t\t.addAllowRedirectUris(\"*\")    // 所有允许授权的 url\n\t\t\t\t.addContractScopes(\"openid\", \"userid\", \"userinfo\")    // 所有签约的权限\n\t\t\t\t.addAllowGrantTypes(\t // 所有允许的授权模式\n\t\t\t\t\t\tGrantType.authorization_code, // 授权码式\n\t\t\t\t\t\tGrantType.implicit,  // 隐式式\n\t\t\t\t\t\tGrantType.refresh_token,  // 刷新令牌\n\t\t\t\t\t\tGrantType.password,  // 密码式\n\t\t\t\t\t\tGrantType.client_credentials  // 客户端模式\n\t\t\t\t)\n\t\t);\n\t\t\n\t\t// 可以添加更多 client 信息，只要保持 clientId 唯一就行了\n\t\t// oauth2Server.addClient(...)\n\t\t\n\t\t// 配置：未登录时返回的View \n\t\tSaOAuth2Strategy.instance.notLoginView = () -> {\n\t\t\t// 简化模拟表单\n\t\t\tString doLoginCode =\n\t\t\t\t\t\"fetch(`/oauth2/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) \" +\n\t\t\t\t\t\t\t\" .then(res => res.json()) \" +\n\t\t\t\t\t\t\t\" .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )\";\n\t\t\tString res =\n\t\t\t\t\t\"<h2>当前客户端在 OAuth-Server 认证中心尚未登录，请先登录</h2>\" +\n\t\t\t\t\t\t\t\"用户：<input id='name' /> <br> \" +\n\t\t\t\t\t\t\t\"密码：<input id='pwd' /> <br>\" +\n\t\t\t\t\t\t\t\"<button onclick=\\\"\" + doLoginCode + \"\\\">登录</button>\";\n\t\t\treturn res;\n\t\t};\n\t\t\n\t\t// 配置：登录处理函数 \n\t\tSaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> {\n\t\t\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\tStpUtil.login(10001);\n\t\t\t\treturn SaResult.ok();\n\t\t\t}\n\t\t\treturn SaResult.error(\"账号名或密码错误\");\n\t\t};\n\t\t\n\t\t// 配置：确认授权时返回的 view \n\t\tSaOAuth2Strategy.instance.confirmView = (clientId, scopes) -> {\n\t\t\tString scopeStr = SaFoxUtil.convertListToString(scopes);\n\t\t\tString yesCode =\n\t\t\t\t\t\"fetch('/oauth2/doConfirm?client_id=\" + clientId + \"&scope=\" + scopeStr + \"', {method: 'POST'})\" +\n\t\t\t\t\t\".then(res => res.json())\" +\n\t\t\t\t\t\".then(res => location.reload())\";\n\t\t\tString res = \"<p>应用 \" + clientId + \" 请求授权：\" + scopeStr + \"，是否同意？</p>\"\n\t\t\t\t\t+ \"<p>\" +\n\t\t\t\t\t\"\t\t<button onclick=\\\"\" + yesCode + \"\\\">同意</button>\" +\n\t\t\t\t\t\"\t\t<button onclick='history.back()'>拒绝</button>\" +\n\t\t\t\t\t\"</p>\";\n\t\t\treturn res;\n\t\t};\n\t}\n\t\n}\n```\n注意：\n- 在 `doLoginHandle` 函数里如果要获取 name, pwd 以外的参数，可通过 `SaHolder.getRequest().getParam(\"xxx\")` 来获取。\n- 你可以在 [框架配置](/use/config?id=SaClientModel属性定义) 了解有关 `SaClientModel` 对象所有属性的详细定义。\n\n\n2、全局异常处理\n``` java\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n}\n```\n\n3、创建启动类：\n``` java\n/**\n * 启动：Sa-OAuth2 Server端 \n */\n@SpringBootApplication \npublic class SaOAuth2ServerApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaOAuth2ServerApplication.class, args);\n\t\tSystem.out.println(\"\\nSa-Token-OAuth2 Server端启动成功，配置如下：\");\n\t\tSystem.out.println(SaOAuth2Manager.getServerConfig());\n\t}\n}\n```\n启动项目\n\n\n### 4、访问测试 \n\n1、由于暂未搭建Client端，我们可以使用 Sa-Token 官网作为重定向URL进行测试：\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://sa-token.cc&scope=openid\n```\n\n2、由于首次访问，我们在OAuth-Server端暂未登录，会被转发到登录视图 \n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/sa-oauth2-server-login-view--v43.png\" alt=\"sa-oauth2-server-login-view\" />\n\n3、输入 `sa/123456` 进行登录之后，会提示我们确认授权\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/sa-oauth2-server-scope.png\" alt=\"sa-oauth2-server-scope\" />\n\n4、点击同意授权之后，我们会被重定向至 redirect_uri 页面，并携带了code参数 \n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/sa-oauth2-server-code.png\" alt=\"sa-oauth2-server-code\" />\n\n4、我们拿着code参数，访问以下地址：\n``` url\nhttp://sa-oauth-server.com:8000/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code}\n```\n\n将得到 `Access-Token`、`Refresh-Token`、`openid`等授权信息：\n\n``` js\n{\n  \"code\": 200,\n  \"msg\": \"ok\",\n  \"data\": null,\n  \"token_type\": \"bearer\",\n  \"access_token\": \"cAls8jnBLmeo5yuCUMwb8zxaSsQPPzGINXF3NOCjCqFHplr6hagdT6A5HeR2\",\n  \"refresh_token\": \"L2rPbJ3aaOXwaB4Zu0EGWNz5EjVNpw5u2oMP9CS2IEap7rR3Hb76ZqqHS07J\",\n  \"expires_in\": 7199,\n  \"refresh_expires_in\": 2591999,\n  \"client_id\": \"1001\",\n  \"scope\": \"openid\",\n  \"openid\": \"ded91dc189a437dd1bac2274be167d50\"\n}\n```\n\n\n测试完毕\n\n\n### 5、运行官方示例\n以上代码只是简单模拟了一下OAuth2.0的授权流程，现在，我们运行一下官方示例，里面有制作好的UI界面\n\n- 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) <br/>\n- 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) <br/>\n\n依次启动`OAuth2-Server` 与 `OAuth2-Client`，然后从浏览器访问：[http://sa-oauth-client.com:8002](http://sa-oauth-client.com:8002)\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/sa-oauth2-client-index.png\" alt=\"sa-oauth2-client-index\" />\n\n如图，可以针对OAuth2.0四种模式进行详细测试 \n\n\n\n### 6、OAuth2 前端测试页\n\nOAuth2 前端测试页： \n`/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/` \n[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5) <br/>\n\n此示例允许你在前端自由配置 OAuth-Client 端所需的各个参数，方便对 OAuth2 四种模式的测试。\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/oauth2-new/sa-oauth2-client-test-h5-page.png\" alt=\"sa-oauth2-client-index\" />\n\n<p><a class=\"case-btn case-btn-video\" href=\"https://www.bilibili.com/video/BV13LSMYzEmE/\" target=\"_blank\">\n\t参考视频：OAuth2 四种模式 前端测试页\n</a></p>\n\n\n"
  },
  {
    "path": "sa-token-doc/oauth2/readme.md",
    "content": "# Sa-Token-OAuth2.0 模块 \n\n--- \n\n### 什么是 OAuth2.0 ？解决什么问题？\n\nOAuth2.0 与 SSO 相比，增加了对应用授权范围的控制，减弱了应用之间数据同步的能力。\n\n有关 OAuth2.0 的设计思想网上教程较多，此处不再重复赘述，详细可参考博客：\n[OAuth2.0 简单解释](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html)\n<!-- 、[OAuth2.0 的四种方式](http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html) -->\n\n如果你还不知道你的项目应该选择 SSO 还是 OAuth2.0，可以参考这篇：[技术选型：[ 单点登录 ] VS [ OAuth2.0 ]](/fun/sso-vs-oauth2)\n\n\n\n### OAuth2.0 四种模式 \n\n基于不同的使用场景，OAuth2.0设计了四种模式：\n\n1. 授权码（Authorization Code）：OAuth2.0 标准授权步骤，Server 端向 Client 端下放 `Code` 码，Client 端再用 `Code` 码换取授权 `Access-Token`。\n2. 隐藏式（Implicit）：无法使用授权码模式时的备用选择，Server 端使用 URL 重定向方式直接将 `Access-Token` 下放到 Client 端页面。\n3. 密码式（Password）：Client 端直接拿着用户的账号密码换取授权 `Access-Token`。\n4. 客户端凭证（Client Credentials）：Server 端针对 Client 级别的 Token，代表应用自身的资源授权。\n\n<img src=\"/big-file/doc/oauth2-new/sa-oauth2-setup.png\" alt=\"sa-oauth2-setup.png\" />\n\n接下来我们将通过简单示例演示如何在 Sa-Token-OAuth2 中完成这四种模式的对接: [搭建OAuth2-Server](/oauth2/oauth2-server)\n\n\n### OAuth2.0 第三方开放平台完整开发流程参考\n\n1. oauth2-server 平台端 \n\t1. 搭建 oauth2-server 数据后台管理端，也称：后台管理。（后台人员对底层数据增删改查维护的地方）。\n\t2. 搭建 oauth2-server 数据前台申请端，也称：开放平台。（给第三方公司提供一个申请注册 client 的地方）\n\t3. 搭建 oauth2-server 授权端 以及其接口文档，也称：认证中心。（让第三方公司拿到 access_token）\n\t4. 搭建 oauth2-server 资源端 以及其接口文档，也称：资源中心。（让第三方公司通过 access_token 拿到对应的资源数据）\n\t5. 以上四端可以是一个项目，也可以是四个独立的项目，也可以是一个后端 + 多个前端的形式。\n\n2. oauth2-client 第三方公司端\n\t1. 第三方公司登录 oauth-server 数据前台申请端，申请注册应用，拿到 `clientId`、`clientSecret` 等数据。\n\t2. 根据自己的业务选择对应的 scope 申请签约，等待平台端审核通过。\n\t3. 在自己系统通过 `clientId`、`clientSecret` 等参数对接 oauth2-server 授权端，拿到 `access_token`。\n\t4. 通过 `access_token` 调用 oauth2-server 资源端接口，拿到对应资源数据。\n\n3. 用户端操作\n\t1. 打开第三方公司开发的网站或APP等程序。\n\t2. 一般有个“通过xx第三方登录”的按钮，点它。\n\t3. 跳转到了 oauth2-server 端的网站，在此网站用 oauth2-server 的账号开始登录。\n\t4. 登录完成，继续跳转到授权页，点击确认授权。\n\t5. 授权完成，oauth2-server 端生成一个 code 码，重定向回 oauth2-client 的网站，把 code 参数挂到对应的 url 上。\n\t6. oauth2-client 从 url 中读取 code 参数，提交到 oauth2-client 的后端。\n\t7. oauth2-client 后端拿着 `code`、`clientId`、`clientSecret` 等信息调用 oauth2-server 授权端 的接口，得到 `access_token`。\n\t8. 继续拿着 `access_token` 调用 oauth2-server 资源端获取此用户对应的数据。\n\t9. 一般最终目的拿到一个 openid 值，oauth2-client 根据 openid 进行登录。生成自己的会话 token ，返回到数据到前端。\n\t10. 前端拿到自己 oauth2-client 生成的会话 token ，完成登录。开始进行业务操作。\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/alone-redis.md",
    "content": "# Sa-Token-Alone-Redis 独立Redis插件\n--- \n\nSa-Token默认的Redis集成方式会把权限数据和业务缓存放在一起，但在部分场景下我们需要将他们彻底分离开来，比如：\n\n> [!NOTE| label:业务场景] \n> 搭建两个Redis服务器，一个专门用来做业务缓存，另一台专门存放Sa-Token权限数据 \n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/plugin/g3--alone-redis.gif\">加载动态演示图</button>\n\n\n要将Sa-Token的数据单独抽离出来很简单，你只需要为Sa-Token单独配置一个Redis连接信息即可 \n\n--- \n\n\n### 1、首先引入Alone-Redis依赖 \n\n\n> [!WARNING| label:Spring Boot 4 用户]\n> 若使用 Spring Boot 4.x，请引入 `sa-token-alone-redis-by-spring-boot4` 替代 `sa-token-alone-redis`。\n> 注：当前版本下(v1.45.0)，此包尚未发布到 Maven 中央仓库，如需使用请下载源码手动自行打包或直接将源码复制到你的项目中进行使用。\n\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-alone-redis</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 Redis （使用 jackson 序列化方式）\nimplementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n### 2、然后在application.yml中增加配置\n\n<!---------------------------- tabs:start ---------------------------->\n\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token: \n\t# Token名称\n\ttoken-name: satoken\n\t# Token有效期\n\ttimeout: 2592000\n\t# Token风格\n\ttoken-style: uuid\n\t\n\t# 配置 Sa-Token 单独使用的 Redis 连接 \n\talone-redis: \n\t\t# Redis数据库索引（默认为0）\n\t\tdatabase: 2\n\t\t# Redis服务器地址\n\t\thost: 127.0.0.1\n\t\t# Redis服务器连接端口\n\t\tport: 6379\n\t\t# Redis服务器连接密码（默认为空）\n\t\tpassword: \n\t\t# 连接超时时间\n\t\ttimeout: 10s\n\nspring: \n\t# 配置业务使用的 Redis 连接 \n\tredis: \n\t\t# Redis数据库索引（默认为0）\n\t\tdatabase: 0\n\t\t# Redis服务器地址\n\t\thost: 127.0.0.1\n\t\t# Redis服务器连接端口\n\t\tport: 6379\n\t\t# Redis服务器连接密码（默认为空）\n\t\tpassword: \n\t\t# 连接超时时间\n\t\ttimeout: 10s\n```\n\n<!------------- tab:properties 风格  ------------->\n``` properties\n############## Sa-Token 配置 ############## \n# Token名称\nsa-token.token-name=satoken\n# Token有效期\nsa-token.timeout=2592000\n# Token风格\nsa-token.token-style=uuid\n\n############## 配置 Sa-Token 单独使用的 Redis 连接  ############## \n# Redis数据库索引（默认为0）\nsa-token.alone-redis.database=2\n# Redis服务器地址\nsa-token.alone-redis.host=127.0.0.1\n# Redis服务器连接端口\nsa-token.alone-redis.port=6379\n# Redis服务器连接密码（默认为空）\nsa-token.alone-redis.password=\n# 连接超时时间\nsa-token.alone-redis.timeout=10s\n\n############## 配置业务使用的 Redis 连接 ############## \n# Redis数据库索引（默认为0）\nspring.redis.database=0\n# Redis服务器地址\nspring.redis.host=127.0.0.1\n# Redis服务器连接端口\nspring.redis.port=6379\n# Redis服务器连接密码（默认为空）\nspring.redis.password=\n# 连接超时时间\nspring.redis.timeout=10s\n\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n具体可参考示例：[码云：application.yml](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml)\n\n集群配置说明: alone-redis同样可以配置集群(cluster模式和sentinel模式), 且基础配置参数和spring redis集群配置别无二致\n\n集群配置示例可参考demo项目sa-token-demo-alone-redis-cluster\n\n\n### 3、测试\n新建Controller测试一下 \n``` java\n@RestController\n@RequestMapping(\"/test/\")\npublic class TestController {\n\n\t@Autowired\n\tStringRedisTemplate stringRedisTemplate;\n\t\n\t// 测试Sa-Token缓存\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(@RequestParam(defaultValue=\"10001\") String id) {\n\t\tSystem.out.println(\"--------------- 测试Sa-Token缓存\");\n\t\tStpUtil.login(id);\t\n\t\treturn SaResult.ok();\n\t}\n\t\n\t// 测试业务缓存\n\t@RequestMapping(\"test\")\n\tpublic SaResult test() {\n\t\tSystem.out.println(\"--------------- 测试业务缓存\");\n\t\tstringRedisTemplate.opsForValue().set(\"hello\", \"Hello World\");\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n```\n\n分别访问两个接口，观察Redis中增加的数据 \n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/alone-redis.png\" alt=\"alone-redis\" />\n\n测试完毕！\n\n### 4、注意点\n目前 Sa-Token-Alone-Redis 仅对以下插件有 Redis 分离效果：\n- sa-token-redis-template\n- sa-token-redis-template-jdk-serializer"
  },
  {
    "path": "sa-token-doc/plugin/aop-at.md",
    "content": "# AOP注解鉴权\n--- \n\n在 [注解式鉴权](/use/at-check) 章节，我们非常轻松的实现了注解鉴权，\n但是默认的拦截器模式却有一个缺点，那就是无法在`Controller层`以外的代码使用进行校验\n\n因此Sa-Token提供AOP插件，你只需在`pom.xml`里添加如下依赖，便可以在任意层级使用注解鉴权\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 整合 SpringAOP 实现注解鉴权 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-aop</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 SpringAOP 实现注解鉴权\nimplementation 'cn.dev33:sa-token-spring-aop:${sa.top.version}'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n\n#### 注意点：\n- 使用拦截器模式，只能把注解写在`Controller层`，使用AOP模式，可以将注解写在任意层级 <br>\n- **拦截器模式和AOP模式不可同时集成**，否则会在`Controller层`发生一个注解校验两次的bug\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/api-key.md",
    "content": "# API Key 接口调用秘钥\n\nAPI Key（应用程序编程接口密钥） 是一种用于身份验证和授权的字符串代码，通常由服务提供商生成并分配给开发者或用户。它的主要作用是标识调用 API（应用程序编程接口）的请求来源，确保请求的合法性，并控制访问权限。\n\n以上是官话，简单理解：API Key 是一种接口调用密钥，类似于会话 token ，但比会话 token 具有更灵活的权限控制。\n\n示例仓库地址：[sa-token-demo-apikey](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-apikey) 🔗\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/plugin/sa-api-key.png\" alt=\"sa-api-key\" />\n\n\n### 1、需求场景\n\n为了帮助大家更好的理解 API Key 的应用场景，我们假设具有以下业务场景：\n\n> [!NOTE| label:业务场景] \n> 你们公司开发了一款论坛网站，非常火爆。\n> \n> 某日，你发现一位用户的头像可以随着日期而变化，Ta 的头像总是显示当前最新日期。\n> \n> 这并未引起你的警觉，因为你是一个程序员，在你看来，写一个任务脚本，每天定时调用 API 更新自己的头像是一件非常简单的事情。\n> \n> 一个月后，越来越多的账号“具有了此功能”，仿佛发生了人传人，Ta 们的头像都可以随着日期而变化，而且颜色各不相同，DIY 的不亦乐乎。\n> \n> 这引起了你的怀疑，如此大批账号的自动化更新行为，显然不是 “某个程序员利用定时脚本更新账号信息” 可以解释的。\n> \n> 一番调查之后，你发现了事情的真相，没有灰产公司捣乱，这批账号也不是机器账号，只是有一个公司为你们的网站开发了一款插件。\n> \n> 这款插件的作用是：用户把自己的 账号+密码 保存在插件中，插件便可以定时更新该账号的头像、昵称、资料等信息。\n> \n> 你觉得插件很有意思，但是插件“要求用户提交账号密码”的行为，让你感到很不爽。\n> \n> 总有一些用户为了得到“些许便利”，而出卖自己的账号密码给插件。\n> \n> 随着时间推移，越来越多的第三方公司或个人为你的网站开发插件：有的可以自动更新账号资料、有的可以自动发帖，有的检测到新粉丝就发送消息通知...\n> \n> 最终，不守规矩的插件出现了：一款插件在提供功能的同时，大量收集用户密码等隐私信息，作为不法用途。\n> \n> 为了遏制这种现象，你们公司升级了系统，增加了 IP 校验等风控判断，阻断了这些插件的 API 调用。\n> \n> 似乎……解决了问题？用户再也不会把账号密码交给第三方插件了。\n> \n> 但是插件的需求总是存在的呀，有些用户确实很需要这些插件的能力来提高网站使用体验。\n> \n> 俗话说的好，堵不如疏，既然用户有需求，第三方公司愿意免费打工开发插件，我们何不设计一套授权架构，\n> 既不需要让用户把账号密码交给第三方插件，又能让插件得到一些权限来调用特定 API 为用户服务。\n> \n> API Key 就是为了完成这种“可控式部分授权” 而设计的一种身份凭证。\n\n\n为了让第三方插件为用户工作，用户必定是要为插件提供一个“凭证”信息的，然后插件利用“凭证”信息，代替用户调用特定 API 完成一些功能。\n\n不同的凭证信息将会带来不同的后果：\n\n\n| 提供的凭证\t\t\t\t\t| 后果\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t\t| :--------\t\t\t\t\t\t\t\t\t|\n| 账号密码\t\t\t\t\t| 插件可以得到账号所有权限，安全风险极高\t\t|\n| 会话 token\t\t\t\t\t| 插件可以调用几乎所有 API，安全风险极高，且容易受到用户退出登录导致 token 失效的影响\t\t|\n| API Key\t\t\t\t\t| 在可控的范围内进行部分授权，且可以方便的随时取消授权，只要设计得当，不会造成安全问题\t\t|\n\nAPI Key 具有以下特点：\n- 1、格式类似于会话 token，是一个随机字符串。\n- 2、每个 API Key 都会和具体的用户 id 发生绑定，后端可以查询到此 API Key 的授权人是谁。\n- 3、一个用户可以创建多个 API Key，用作不同的插件中。\n- 4、每个 API Key 都可以赋予不同的 scope 权限，以做到最小化授权。\n- 5、API Key 可以设置有效期，并且随时删除回收，做到灵活控制。\n\n\n\n### 2、引入依赖\n在使用 API Key 模块之前，你必须先引入依赖：\n``` xml\n<!-- Sa-Token 整合 API Key -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-apikey</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n\n\n### 3、创建 API Key\n\n理解了应用场景后，让我们看看 Sa-Token 为 API Key 提供了哪些方法：\n\n\n``` java\n// 为指定用户创建一个新的 API Key \nApiKeyModel akModel = SaApiKeyUtil.createApiKeyModel(10001).setTitle(\"test\");\nSystem.out.println(\"API Key 值：\" + akModel.getApiKey());\n\n// 保存 API Key \nSaApiKeyUtil.saveApiKey(akModel);\n\n// 删除 API Key \nSaApiKeyUtil.deleteApiKey(apiKey);\n```\n\n一个 ApiKeyModel 可设置以下属性：\n``` java\nApiKeyModel akModel = new ApiKeyModel();\nakModel.setLoginId(10001);  // 设置绑定的用户 id\nakModel.setApiKey(\"AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp\");  // 设置 API Key 值\nakModel.setTitle(\"commit\");\t  // 设置名称\nakModel.setIntro(\"提交代码专用\");   // 设置描述\nakModel.addScope(\"commit\", \"pull\");  // 设置权限范围\nakModel.setExpiresTime(System.currentTimeMillis() + 2592000);  // 设置失效时间，13位时间戳，-1=永不失效\nakModel.setIsValid(true);   // 设置是否有效\nakModel.addExtra(\"name\", \"张三\");   // 设置扩展信息\n// 保存 \nSaApiKeyUtil.saveApiKey(akModel);  \n```\n\n查询：\n\n``` java\n// 获取 API Key 详细信息 \nApiKeyModel akModel = SaApiKeyUtil.getApiKey(\"AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp\");\n\n// 直接获取 ApiKey 所代表的 loginId\nObject loginId = SaApiKeyUtil.getLoginIdByApiKey(\"AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp\");\n\n// 获取指定 loginId 的 ApiKey 列表记录\nList<ApiKeyModel> apiKeyList = SaApiKeyUtil.getApiKeyList(10001);\n```\n\n\n### 4、校验 API Key\n\n``` java\n// 校验指定 API Key 是否有效，无效会抛出异常 ApiKeyException\nSaApiKeyUtil.checkApiKey(\"AK-XxxXxxXxx\");\n\n// 校验指定 API Key 是否具有指定 Scope 权限，不具有会抛出异常 ApiKeyScopeException\nSaApiKeyUtil.checkApiKeyScope(\"AK-XxxXxxXxx\", \"userinfo\");\n\n// 校验指定 API Key 是否具有指定 Scope 权限，返回 true 或 false\nSaApiKeyUtil.hasApiKeyScope(\"AK-XxxXxxXxx\", \"userinfo\");\n\n// 校验指定 API Key 是否属于指定账号 id \nSaApiKeyUtil.checkApiKeyLoginId(\"AK-XxxXxxXxx\", 10001);\n```\n\n注解鉴权示例：\n``` java\n/**\n * API Key 资源 相关接口\n */\n@RestController\npublic class ApiKeyResourcesController {\n\n\t// 必须携带有效的 ApiKey 才能访问\n\t@SaCheckApiKey\n\t@RequestMapping(\"/akRes1\")\n\tpublic SaResult akRes1() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n\t// 必须携带有效的 ApiKey ，且具有 userinfo 权限\n\t@SaCheckApiKey(scope = \"userinfo\")\n\t@RequestMapping(\"/akRes2\")\n\tpublic SaResult akRes2() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n\t// 必须携带有效的 ApiKey ，且同时具有 userinfo、chat 权限\n\t@SaCheckApiKey(scope = {\"userinfo\", \"chat\"})\n\t@RequestMapping(\"/akRes3\")\n\tpublic SaResult akRes3() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n\t// 必须携带有效的 ApiKey ，且具有 userinfo、chat 其中之一权限\n\t@SaCheckApiKey(scope = {\"userinfo\", \"chat\"}, mode = SaMode.OR)\n\t@RequestMapping(\"/akRes4\")\n\tpublic SaResult akRes4() {\n\t\tApiKeyModel akModel = SaApiKeyUtil.currentApiKey();\n\t\tSystem.out.println(\"当前 ApiKey: \" + akModel);\n\t\treturn SaResult.ok(\"调用成功\");\n\t}\n\n}\n```\n\n\n### 5、前端如何提交 API Key？\n默认情况下，前端可以从任意途径提交 API Key 字符串，只要后端能接受到。\n\n但是如果后端是通过 `SaApiKeyUtil.currentApiKey()` 方法获取，或者 `@SaCheckApiKey` 注解校验，则需要前端按照一定的格式来提交了：\n\n方式一：通过请求参数或请求头，参数名为 `apikey`（全小写）\n\n``` url\n/user/getInfo?apikey=AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp\n```\n\n\n方式二：通过 Basic 参数提交\n\n``` url\nhttp://AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp@localhost:8081/user/getInfo\n```\n\n\n\n\n\n### 6、打开数据库模式\n\n框架默认将所有 API Key 信息保存在缓存中，这可以称之为“缓存模式”，这种模式下，重启缓存库后，数据将丢失。\n\n如果你想改为“数据库模式”，可以通过 `implements SaApiKeyDataLoader` 实现从数据库加载的逻辑。\n\n``` java\n/**\n * API Key 数据加载器实现类 （从数据库查询）\n */\n@Component \npublic class SaApiKeyDataLoaderImpl implements SaApiKeyDataLoader {\n\n    @Autowired\n    SaApiKeyMapper apiKeyMapper;\n\n    // 指定框架不再维护 API Key 索引信息，而是由我们手动从数据库维护\n    @Override\n    public Boolean getIsRecordIndex() {\n        return false;\n    }\n\n    // 根据 apiKey 从数据库获取 ApiKeyModel 信息 （实现此方法无需为数据做缓存处理，框架内部已包含缓存逻辑）\n    @Override\n    public ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) {\n        return apiKeyMapper.getApiKeyModel(apiKey);\n    }\n\n}\n```\n\n参考上述代码实现后，框架内部逻辑将会做出一些改变，请注意以下事项：\n\n- 1、调用 `SaApiKeyUtil.getApiKey(\"ApiKey\")` 时，会先从缓存中查询，查询不到时调用 `getApiKeyModelFromDatabase` 从数据库加载。\n- 2、框架不再维护 API Key 索引数据，这意味着无法再调用 `SaApiKeyUtil.getApiKeyList(10001)` 来获取一个用户的所有的 API Key 数据，请自行从数据库查询。\n- 3、调用 `SaApiKeyUtil.saveApiKey(akModel)` 保存时，只会把 API Key 数据保存到缓存中，请自行补充额外代码向数据库保存数据。\n- 4、调用 `SaApiKeyUtil.deleteApiKey(\"ApiKey\")` 时，只会删除这个 API Key 在缓存中的数据，不会删除数据库的数据，请自行补充相关代码保证数据双删。\n- 5、其它诸如查询 `SaApiKeyUtil.getApiKey(\"ApiKey\")` 或校验 `SaApiKeyUtil.checkApiKeyScope(\"ApiKey\", \"userinfo\")` 等方法，依旧可以正常调用。\n\n\n\n### 7、多账号模式使用 \n\n如果系统有多套账号表，比如 Admin 和 User，只需要指定不同的命名空间即可：\n\n例如 User 账号的 API Key，我们使用原生 `SaApiKeyUtil` 进行创建与校验。\n\n对于 Admin 账号的 API Key，我们则新建一个 `SaApiKeyTemplate` 实例\n\n``` java\n// 新建 Admin 账号的 apiKeyTemplate 对象，命名空间为 \"admin-apikey\"\npublic static SaApiKeyTemplate adminApiKeyTemplate = new SaApiKeyTemplate(\"admin-apikey\");\n\n// 创建一个新的 ApiKey，并返回\n@RequestMapping(\"/createApiKey\")\npublic SaResult createApiKey() {\n\tApiKeyModel akModel = adminApiKeyTemplate.createApiKeyModel(StpUtil.getLoginId()).setTitle(\"test\");\n\tadminApiKeyTemplate.saveApiKey(akModel);\n\treturn SaResult.data(akModel);\n}\n\n// ...校验、查询等操作，均使用新创建的 adminApiKeyTemplate，而非原生 `SaApiKeyUtil`\n```\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/api-sign.md",
    "content": "# API 接口参数签名\n\n<p><a class=\"case-btn case-btn-video\" href=\"https://www.bilibili.com/video/BV17oeKeZEHo/\" target=\"_blank\">\n\t观看本节视频讲解（B站：抓蛙师）\n</a></p>\n\n在涉及跨系统接口调用时，我们容易碰到以下安全问题：\n\n- 请求身份被伪造。\n- 请求参数被篡改。\n- 请求被抓包，然后重放攻击。\n\nsa-token-sign 模块将帮你轻松解决以上难题。\n\n本篇将根据假设的需求场景，循序渐进讲明白跨系统接口调用时必做的几个步骤，以及为什么要有这些步骤的原因。\n\n\n### 1、需求场景\n\n假设我们有如下业务需求：\n\n> [!NOTE| label:业务场景] \n> 用户在 A 系统参与活动成功后，活动奖励以余额的形式下发到 B 系统。\n\n\n### 2、初始方案：直接裸奔\n\n在不考虑安全问题的情况下，我们很容易完成这个需求：\n\n1、在 B 系统开放一个接口。\n\n``` java\n/**\n * 为指定用户添加指定余额\n * \n * @param userId 用户 id\n * @param money 要添加的余额，单位：分\n * @return / \n */\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money) {\n\t// 处理业务 \n\t// ...\n\t\n\t// 返回 \n\treturn SaResult.ok();\n}\n```\n\n2、在 A 系统使用 http 工具类调用这个接口。\n\n``` java\nlong userId = 10001;\nlong money = 1000;\nString res = HttpUtil.request(\"http://b.com/api/addMoney?userId=\" + userId + \"&money=\" + money);\n```\n\n上述代码简单的完成了需求，但是很明显它有一个安全问题：\n\nB 系统开放的接口不仅可以被 A 系统调用，还可以被其它任何人调用，甚至别人可以本地跑一个 for 循环调用这个接口，为自己无限充值金额。\n\n\n### 3、方案升级：增加 secretKey 校验\n\n为防止 B 系统开放的接口被陌生人任意调用，我们增加一个 secretKey 参数\n\n``` java\n// 为指定用户添加指定余额\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money, String secretKey) {\n\t// 1、先校验 secretKey 参数是否正确，如果不正确直接拒绝响应请求\n\tif( ! check(secretKey) ) {\n\t\treturn SaResult.error(\"无效 secretKey，无法响应请求\");\n\t}\n\t\n\t// 2、业务代码 \n\t// ...\n\t\n\t// 3、返回\n\treturn SaResult.ok();\n}\n```\n\n由于 A 系统是我们 “自己人”，所以它可以拿着 `secretKey` 进行合法请求：\n\n``` java\nlong userId = 10001;\nlong money = 1000;\nString secretKey = \"xxxxxxxxxxxxxxxxxxxx\";\nString res = HttpUtil.request(\"http://b.com/api/addMoney?userId=\" + userId + \"&money=\" + money + \"&secretKey=\" + secretKey);\n```\n\n现在，即使 B 系统的接口被暴露了，也不会被陌生人任意调用了，安全性得到了一定的保证，但是仍然存在一些问题：\n\n- 如果请求被抓包，secretKey 就会泄露，因为每次请求都在 url 中明文传输了 secretKey 参数。\n- 如果请求被抓包，请求的其它参数就可以被任意修改，例如可以将 money 参数修改为 9999999，B系统无法确定参数是否被修改过。\n\n\n\n### 4、方案再升级：使用摘要算法生成参数签名\n\n首先，在 A 系统不要直接发起请求，而是先计算一个 sign 参数：\n\n``` java\n// 声明变量\nlong userId = 10001;\nlong money = 1000;\nString secretKey = \"xxxxxxxxxxxxxxxxxxxx\";\n\n// 计算 sign 参数\nString sign = md5(\"money=\" + money + \"&userId=\" + userId + \"&key=\" + secretKey);\n\n// 将 sign 拼接在请求地址后面\nString res = HttpUtil.request(\"http://b.com/api/addMoney?userId=\" + userId + \"&money=\" + money + \"&sign=\" + sign);\n```\n\n**注意此处计算签名时，需要将所有参数按照字典顺序依次排列（key除外，挂在最后面）。**以下所有计算签名时同理，不再赘述。\n\n然后在 B 系统接收请求时，使用同样的算法、同样的秘钥，生成 sign 字符串，与参数中 sign 值进行比较：\n\n\n``` java\n// 为指定用户添加指定余额\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money, String sign) {\n\n\t// 在 B 系统，使用同样的算法、同样的密钥，计算出 sign2，与传入的 sign 进行比对\n\tString sign2 = md5(\"money=\" + money + \"&userId=\" + userId + \"&key=\" + secretKey);\n\tif( ! sign2.equals(sign)) {\n\t\treturn SaResult.error(\"无效 sign，无法响应请求\");\n\t}\n\n\t// 2、业务代码 \n\t// ...\n\t\n\t// 3、返回\n\treturn SaResult.ok();\n}\n```\n\n因为 sign 的值是由 userId、money、secretKey 三个参数共同决定的，所以只要有一个参数不一致，就会造成最终生成 sign 也是不一致的，所以，根据比对结果：\n\n- 如果 sign 一致，说明这是个合法请求。\n- 如果 sign 不一致，说明发起请求的客户端秘钥不正确，或者请求参数被篡改过，是个不合法请求。\n\n此方案优点：\n- 不在 url 中直接传递 secretKey 参数了，避免了泄露风险。\n- 由于 sign 参数的限制，请求中的参数也不可被篡改，B 系统可放心的使用这些参数。\n\n此方案仍然存在以下缺陷：\n- 被抓包后，请求可以被无限重放，B 系统无法判断请求是真正来自于 A 系统发出的，还是被抓包后重放的。\n\n\n\n### 5、方案再再升级：追加 nonce 随机字符串\n\n首先，在 A 系统发起调用前，追加一个 nonce 参数，一起参与到签名中：\n\n``` java\n// 声明变量\nlong userId = 10001;\nlong money = 1000;\nString nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串\nString secretKey = \"xxxxxxxxxxxxxxxxxxxx\";\n\n// 计算 sign 参数\nString sign = md5(\"money=\" + money + \"&nonce=\" + nonce + \"&userId=\" + userId + \"&key=\" + secretKey);\n\n// 将 sign 拼接在请求地址后面\nString res = HttpUtil.request(\"http://b.com/api/addMoney?userId=\" + userId + \"&money=\" + money + \"nonce=\" + nonce + \"&sign=\" + sign);\n```\n\n然后在 B 系统接收请求时，也把 nonce 参数加进去生成 sign 字符串，进行比较：\n\n``` java\n// 为指定用户添加指定余额\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money, String nonce, String sign) {\n\n\t// 1、检查此 nonce 是否已被使用过了\n\tif(CacheUtil.get(\"nonce_\" + nonce) != null) {\n\t\treturn SaResult.error(\"此 nonce 已被使用过了，请求无效\");\n\t}\n\n\t// 2、验证签名\n\tString sign2 = md5(\"money=\" + money + \"&nonce=\" + nonce + \"&userId=\" + userId + \"&key=\" + secretKey);\n\tif( ! sign2.equals(sign)) {\n\t\treturn SaResult.error(\"无效 sign，无法响应请求\");\n\t}\n\n\t// 3、将 nonce 记入缓存，防止重复使用\n\tCacheUtil.set(\"nonce_\" + nonce, \"1\");\n\n\t// 4、业务代码 \n\t// ...\n\n\t// 5、返回\n\treturn SaResult.ok();\n}\n```\n\n代码分析：\n \n- 为方便理解，我们先看第 3 步：此处在校验签名成功后，将 nonce 随机字符串记入缓存中。\n- 再看第 1 步：每次请求进来，先查看一下缓存中是否已经记录了这个随机字符串，如果是，则立即返回：无效请求。\n\n这两步的组合，保证了一个 nonce 随机字符串只能被使用一次，如果请求被抓包后重放，是无法通过 nonce 校验的。\n\n至此，问题似乎已被解决了 …… 吗？\n\n别急，我们还有一个问题没有考虑：这个 nonce 在字符串在缓存应该被保存多久呢？\n\n- 保存 15 分钟？那抓包的人只需要等待 15 分钟，你的 nonce 记录在缓存中消失，请求就可以被重放了。\n- 那保存 24 小时？保存一周？保存半个月？好像无论保存多久，都无法从根本上解决这个问题。\n\n你可能会想到，那我永久保存吧。这样确实能解决问题，但显然服务器承载不了这么做，即使再微小的数据量，在时间的累加下，也总一天会超出服务器能够承载的上限。\n\n\n### 6、方案再再再升级：追加 timestamp 时间戳\n\n我们可以再追加一个 timestamp 时间戳参数，将请求的有效性限定在一个有限时间范围内，例如 15分钟。\n\n首先，在 A 系统追加 timestamp 参数：\n\n``` java\n// 声明变量\nlong userId = 10001;\nlong money = 1000;\nString nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串\nlong timestamp = System.currentTimeMillis(); // 系统当前时间戳 \nString secretKey = \"xxxxxxxxxxxxxxxxxxxx\";\n\n// 计算 sign 参数\nString sign = md5(\"money=\" + money + \"&nonce=\" + nonce + \"&timestamp=\" + timestamp + \"&userId=\" + userId + \"&key=\" + secretKey);\n\n// 将 sign 拼接在请求地址后面\nString res = HttpUtil.request(\"http://b.com/api/addMoney\" +\n\t\t\"?userId=\" + userId + \"&money=\" + money + \"&nonce=\" + nonce + \"&timestamp=\" + timestamp + \"&sign=\" + sign);\n```\n\n在 B 系统检测这个 timestamp 是否超出了允许的范围 \n\n``` java\n// 为指定用户添加指定余额\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) {\n\n\t// 1、检查 timestamp 是否超出允许的范围（此处假定最大允许15分钟差距）\n\tlong timestampDisparity = System.currentTimeMillis() - timestamp; // 实际的时间差\n\tif(timestampDisparity > 1000 * 60 * 15) {\n\t\treturn SaResult.error(\"timestamp 时间差超出允许的范围，请求无效\");\n\t}\n\n\t// 2、检查此 nonce 是否已被使用过了\n\t// 代码同上，不再赘述\n\n\t// 3、验证签名\n\t// 代码同上，不再赘述\n\n\t// 4、将 nonce 记入缓存，ttl 有效期和 allowDisparity 允许时间差一致 \n\tCacheUtil.set(\"nonce_\" + nonce, \"1\", 1000 * 60 * 15);\n\n\t// 5、业务代码 ...\n\n\t// 6、返回\n\treturn SaResult.ok();\n}\n```\n\n至此，抓包者：\n\n- 如果在 15 分钟内重放攻击，nonce 参数不答应：缓存中可以查出 nonce 值，直接拒绝响应请求。\n- 如果在 15 分钟后重放攻击，timestamp 参数不答应：超出了允许的 timestamp 时间差，直接拒绝响应请求。\n\n\n### 7、服务器的时钟差异造成安全问题\n\n以上的代码，均假设 A 系统服务器与 B 系统服务器的时钟一致，才可以正常完成安全校验，但在实际的开发场景中，有些服务器会存在时钟不准确的问题。\n\n假设 A 服务器与 B 服务器的时钟差异为 10 分钟，即：在 A 服务器为 8:00 的时候，B 服务器为 7:50。\n\n1. A 系统发起请求，其生成的时间戳也是代表 8:00。\n2. B 系统接受到请求后，完成业务处理，此时 nonce 的 ttl 为 15分钟，到期时间为 7:50 + 15分 = 8:05。\n3. 8.05 后，nonce 缓存消失，抓包者重放请求攻击：\n\t- timestamp 校验通过：因为时间戳差距仅有 8.05 - 8.00 = 5分钟，小于 15 分钟，校验通过。\n\t- nonce 校验通过：因为此时 nonce 缓存已经消失，可以通过校验。\n\t- sign 校验通过：因为这本来就是由 A 系统构建的一个合法签名。\n\t- 攻击完成。\n\n要解决上述问题，有两种方案：\n- 方案一：修改服务器时钟，使两个服务器时钟保持一致。\n- 方案二：在代码层面兼容时钟不一致的场景。\n\n要采用方案一的同学可自行搜索一下同步时钟的方法，在此暂不赘述，此处详细阐述一下方案二。\n\n我们只需简单修改一下，B 系统校验参数的代码即可：\n\n``` java\n// 为指定用户添加指定余额\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) {\n\n\t// 1、检查 timestamp 是否超出允许的范围 （⚠️ 重点一：此处需要取绝对值）\n\tlong timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp);\n\tif(timestampDisparity > 1000 * 60 * 15) {\n\t\treturn SaResult.error(\"timestamp 时间差超出允许的范围，请求无效\");\n\t}\n\n\t// 2、检查此 nonce 是否已被使用过了\n\t// 代码同上，不再赘述 \n\n\t// 3、验证签名\n\t// 代码同上，不再赘述 \n\n\t// 4、将 nonce 记入缓存，防止重复使用（⚠️ 重点二：此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ）\n\tCacheUtil.set(\"nonce_\" + nonce, \"1\", (1000 * 60 * 15) * 2);\n\n\t// 5、业务代码 ...\n\n\t// 6、返回\n\treturn SaResult.ok();\n}\n```\n\n\n### 8、最终版方案\n\n此处再贴一下完整的代码。\n\nA 系统（发起请求端）：\n\n``` java\n// 声明变量\nlong userId = 10001;\nlong money = 1000;\nString nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串\nlong timestamp = System.currentTimeMillis(); // 当前时间戳\nString secretKey = \"xxxxxxxxxxxxxxxxxxxx\";\n\n// 计算 sign 参数\nString sign = md5(\"money=\" + money + \"&nonce=\" + nonce + \"&timestamp=\" + timestamp + \"&userId=\" + userId + \"&key=\" + secretKey);\n\n// 将 sign 拼接在请求地址后面\nString res = HttpUtil.request(\"http://b.com/api/addMoney\" +\n\t\t\"?userId=\" + userId + \"&money=\" + money + \"&nonce=\" + nonce + \"&timestamp=\" + timestamp + \"&sign=\" + sign);\n```\n\nB 系统（接收请求端）：\n\n``` java\n// 为指定用户添加指定余额\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) {\n\n\t// 1、检查 timestamp 是否超出允许的范围\n\tlong allowDisparity = 1000 * 60 * 15;\t// 允许的时间差：15分钟\n\tlong timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp); // 实际的时间差\n\tif(timestampDisparity > allowDisparity) {\n\t\treturn SaResult.error(\"timestamp 时间差超出允许的范围，请求无效\");\n\t}\n\n\t// 2、检查此 nonce 是否已被使用过了\n\tif(CacheUtil.get(\"nonce_\" + nonce) != null) {\n\t\treturn SaResult.error(\"此 nonce 已被使用过了，请求无效\");\n\t}\n\n\t// 3、验证签名\n\tString sign2 = md5(\"money=\" + money + \"&nonce=\" + nonce + \"&timestamp=\" + timestamp + \"&userId=\" + userId + \"&key=\" + secretKey);\n\tif( ! sign2.equals(sign)) {\n\t\treturn SaResult.error(\"无效 sign，无法响应请求\");\n\t}\n\n\t// 4、将 nonce 记入缓存，防止重复使用，注意此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2  \n\tCacheUtil.set(\"nonce_\" + nonce, \"1\", allowDisparity * 2);\n\n\t// 5、业务代码 ...\n\n\t// 6、返回\n\treturn SaResult.ok();\n}\n```\n\n\n### 9、使用 Sa-Token 框架完成 API 参数签名\n\n接下来步入正题，使用 sa-token-sign 模块，方便的完成 API 签名创建、校验等步骤：\n- 不限制请求的参数数量，方便组织业务需求代码。\n- 自动补全 nonce、timestamp 参数，省时省力。\n- 自动构建签名，并序列化参数为字符串。\n- 一句代码完成 nonce、timestamp、sign 的校验，防伪造请求调用、防参数篡改、防重放攻击。\n\n\n#### 9.1、引入依赖\n请求发起端和接收端都需要引入：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n\n``` xml \n<!-- Sa-Token 整合 API 参数签名校验 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-sign</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n\n<!-------- tab:Gradle 方式 -------->\n\n``` gradle\n// Sa-Token 整合 API 参数签名校验\nimplementation 'cn.dev33:sa-token-sign:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 9.2、配置秘钥\n请求发起端和接收端需要配置一个相同的秘钥，在 `application.yml` 中配置：\n\n``` yml\nsa-token: \n    sign:\n        # API 接口签名秘钥 （随便乱摁几个字母即可）\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n\n#### 9.3、请求发起端构建签名\n\n``` java\n// 请求地址\nString url = \"http://b.com/api/addMoney\";\n\n// 请求参数\nMap<String, Object> paramMap = new LinkedHashMap<>();\nparamMap.put(\"userId\", 10001);\nparamMap.put(\"money\", 1000);\n// 更多参数，不限制数量...\n\n// 补全 timestamp、nonce、sign 参数，并序列化为 kv 字符串\nString paramStr = SaSignUtil.addSignParamsAndJoin(paramMap);\n\n// 将参数字符串拼接在请求地址后面\nurl += \"?\" + paramStr;\n\n// 发送请求\nString res = HttpUtil.request(url);\n\n// 根据返回值做后续处理\nSystem.out.println(\"server 端返回信息：\" + res);\n```\n\n\n#### 9.4、请求接受端校验签名\n``` java\n// 为指定用户添加指定余额\n@RequestMapping(\"addMoney\")\npublic SaResult addMoney(long userId, long money) {\n\n\t// 1、校验请求中的签名\n\tSaSignUtil.checkRequest(SaHolder.getRequest());\n\t\n\t// 2、校验通过，处理业务\n\tSystem.out.println(\"userId=\" + userId);\n\tSystem.out.println(\"money=\" + money);\n\t\n\t// 3、返回\n\treturn SaResult.ok();\n}\n```\n\n如上代码便可简单方便的完成 API 接口参数签名校验，当请求端的秘钥不对，或者请求参数被篡改、请求被重放时，均无法通过 `SaSignUtil.checkRequest` 校验。\n\n``` js\n{\n  \"code\": 500,\n  \"msg\": \"无效签名：9c3e3e98c7d543fb599766c9d3f3b5ff\",\n  \"data\": null\n}\n```\n\n\n### 10、使用注解校验签名\n\n`@SaCheckSign` 注解用于为一个接口提供签名校验，用于替代 `SaSignUtil.checkRequest(SaHolder.getRequest())`，示例如下：\n\n``` java\n// 校验全部参数：效果等同于  SaSignUtil.checkRequest(SaHolder.getRequest())\n@SaCheckSign\n@RequestMapping(\"test1\")\npublic SaResult test1() {\n\t// code ...\n\treturn SaResult.ok();\n}\n\n// 指定参与签名的参数有哪些：效果等同于 SaSignUtil.checkRequest(SaHolder.getRequest(), \"id\", \"name\");\n@SaCheckSign(verifyParams = {\"id\", \"name\"})\n@RequestMapping(\"test2\")\npublic SaResult test2() {\n\t// code ...\n\treturn SaResult.ok();\n}\n\n// 指定: 在多应用模式下，使用的 appid，详情见下\n@SaCheckSign(appid = \"xm-shop\")\n@RequestMapping(\"test3\")\npublic SaResult test3() {\n\t// code ...\n\treturn SaResult.ok();\n}\n```\n\n\n### 11、多应用模式\n\n有时候我们可能需要同时与多个应用对接，每个应用都需要使用不同的秘钥：\n\n首先在配置文件配置多个应用信息：\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n    # API 签名配置 多应用模式\n    sign-many:\n        # 应用1\n        xm-shop:\n            secret-key: 0123456789abcdefg\n            digest-algo: md5\n        # 应用2\n        xm-forum:\n            secret-key: 0123456789hijklmnopq\n            digest-algo: sha256\n        # 应用3\n        xm-video:\n            secret-key: 12341234aaaaccccdddd\n            digest-algo: sha512\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# API 签名配置 多应用模式\n# 应用1\nsa-token.sign-many.xm-shop.secret-key=0123456789abcdefg\nsa-token.sign-many.xm-shop.digest-algo=md5\n# 应用2\nsa-token.sign-many.xm-forum.secret-key=0123456789hijklmnopq\nsa-token.sign-many.xm-forum.digest-algo=sha256\n# 应用3\nsa-token.sign-many.xm-video.secret-key=12341234aaaaccccdddd\nsa-token.sign-many.xm-video.digest-algo=sha512\n```\n<!------------- tab:代码风格示例  ------------->\n``` java\n@Autowired\npublic void configSaToken(SaTokenConfig config) {\n    // API 签名配置 多应用模式\n\t// 应用1\n\tconfig.getSignMany().put(\"xm-shop\", new SaSignConfig()\n\t\t\t.setSecretKey(\"0123456789abcdefg\")   // 秘钥\n\t\t\t.setDigestAlgo(\"md5\")   // 签名算法\n\t);\n\t// 应用2\n\tconfig.getSignMany().put(\"xm-forum\", new SaSignConfig()\n\t\t\t.setSecretKey(\"0123456789hijklmnopq\")\n\t\t\t.setDigestAlgo(\"sha256\")\n\t);\n\t// 应用3\n\tconfig.getSignMany().put(\"xm-video\", new SaSignConfig()\n\t\t\t.setSecretKey(\"12341234aaaaccccdddd\")\n\t\t\t// 自定义签名算法示例\n\t\t\t.setDigestMethod(fullStr -> {\n\t\t\t\treturn SaSecureUtil.sha384(fullStr);\n\t\t\t})\n\t);\n}\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n然后在签名时通过指定 appid 的方式获取对应的 SignTemplate 进行操作：\n\n``` java\n// 创建签名示例\nString paramStr = SaSignMany.getSignTemplate(\"xm-shop\").addSignParamsAndJoin(paramMap);\n\n// 校验签名示例\nSaSignMany.getSignTemplate(\"xm-shop\").checkRequest(SaHolder.getRequest());\n```\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/custom-serializer.md",
    "content": "# 序列化插件扩展包\n--- \n\n引入此插件可以为 Sa-Token 提供一些有意思的序列化方案。（娱乐向，不建议上生产）\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 自定义 String 序列化方案合集 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-serializer-features</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 自定义 String 序列化方案合集\nimplementation 'cn.dev33:sa-token-serializer-features:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 1、SaSerializerForBase64UseTianGan\nbase64 编码，采用 十大天干、十二地支 等64个中文字符作为元字符集\n\n``` java\n// 设置序列化方案: base64 编码，采用 十大天干、十二地支 等64个中文字符作为元字符集\n@PostConstruct\npublic void rewriteComponent() {\n\tSaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan());\n}\n```\n\n效果图：\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-custom-serializer-tiangan.png\" alt=\"sa-custom-serializer-tiangan.png\" />\n\n\n#### 2、SaSerializerForBase64UsePeriodicTable\nbase64 编码，采用 元素周期表 前六十四位作为元字符集\n\n``` java\n// 设置序列化方案: base64 编码，采用 元素周期表 前六十四位作为元字符集\n@PostConstruct\npublic void rewriteComponent() {\n\tSaManager.setSaSerializerTemplate(new SaSerializerForBase64UsePeriodicTable());\n}\n```\n\n效果图：\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-custom-serializer-yszqb.png\" alt=\"sa-custom-serializer-yszqb.png\" />\n\n\n\n#### 3、SaSerializerForBase64UseSpecialSymbols\nbase64 编码，采用64个特殊符号作为元字符集\n\n``` java\n// 设置序列化方案: base64 编码，采用64个特殊符号作为元字符集\n@PostConstruct\npublic void rewriteComponent() {\n\tSaManager.setSaSerializerTemplate(new SaSerializerForBase64UseSpecialSymbols());\n}\n```\n\n效果图：\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-custom-serializer-tsfh.png\" alt=\"sa-custom-serializer-tsfh.png\" />\n\n\n#### 4、SaSerializerForBase64UseEmoji\nbase64 编码，采用 64 个 Emoji 小黄脸作为元字符集，无填充字符\n\n``` java\n// 设置序列化方案: base64 编码，采用 64 个 Emoji 小黄脸作为元字符集，无填充字符\n@PostConstruct\npublic void rewriteComponent() {\n\tSaManager.setSaSerializerTemplate(new SaSerializerForBase64UseEmoji());\n}\n```\n\n效果图：\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-custom-serializer-emoji.png\" alt=\"sa-custom-serializer-emoji.png\" />\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-custom-serializer-emoji2.png\" alt=\"sa-custom-serializer-emoji2.png\" />\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/dao-extend.md",
    "content": "# 缓存层扩展\n--- \n\n对于权限框架来讲，最容易碰到的扩展点便是数据存储方式，为了方便对接不同的缓存中间件，Sa-Token将所有数据持久化操作抽象到SaTokenDao接口，\n开发者要对接不同的平台只需要实现此接口即可，接口签名：[SaTokenDao.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java)\n \n框架已提供的集成包包括：\n\n- 默认方式：储存在内存中，位于core核心包。\n- sa-token-redis-template：Redis Template 集成包。\n- sa-token-redis-template-jdk-serializer：Redis 集成包，使用 jdk 默认序列化方式。\n- sa-token-hutool-timed-cache：集成 hutool 框架的 Timed-Cache 缓存方案（基于内存）。\n- sa-token-caffeine：集成 Caffeine 缓存方案（基于内存）。\n- sa-token-redisson：集成 Redisson 客户端。\n- sa-token-redisson-spring-boot-starter：集成 Redisson 客户端 - SpringBoot 自动配置包 。\n- sa-token-redisx：Redisx 集成包。 \n\n\n有关 Redis 集成，详细参考：[集成Redis](/up/integ-redis)，更多存储方式欢迎提交PR \n\n\n**扩展：集成 MongoDB**\n\n- [集成 MongoDB 参考一](/up/integ-spring-mongod-1)\n- [集成 MongoDB 参考二](/up/integ-spring-mongod-2)\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/dubbo-extend.md",
    "content": "# 和 Dubbo 集成 \n\n本插件的作用是让 Sa-Token 和 Dubbo 做一个整合。 \n\n--- \n\n### 先说说要解决的问题 \n\n在 Dubbo 的整个调用链中，代码被分为 Consumer 端和 Provider 端，为方便理解我们可以称其为 `[调用端]` 和 `[被调用端]`。 \n\nRPC 模式的调用，可以让我们像调用本地方法一样完成服务通信，然而这种便利下却隐藏着两个问题：\n\n- 上下文环境的丢失。\n- 上下文参数的丢失。 \n\n这种问题作用在 Sa-Token 框架上就是，在 [ 被调用端 ] 调用 Sa-Token 相关API会抛出异常： **`无效上下文`** 。\n\n所以本插件的目的也就是解决上述两个问题：\n\n- 在 [ 被调用端 ] 提供以 Dubbo 为基础的上下文环境 \n- 在 RPC 调用时将 Token 传递至 [ 被调用端 ]，同时在调用结束时将 Token 回传至 [ 调用端 ]。\n\n\n### 引入插件 \n\n在项目已经引入 Dubbo 的基础上，继续添加依赖（Consumer 端和 Provider 端都需要引入）：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 整合 Dubbo -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-dubbo</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 Dubbo\nimplementation 'cn.dev33:sa-token-dubbo:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n注：如果使用的是 dubbo3，只需要将 `sa-token-dubbo` 修改为 `sa-token-dubbo3` 即可。\n\n\n然后我们就可以愉快的做到以下事情：\n\n1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。\n2. 在 [ 调用端 ] 登录的会话，其登录状态可以自动传递到 [ 被调用端 ] 。\n3. 在 [ 被调用端 ] 登录的会话，其登录状态也会自动回传到 [ 调用端 ] 。\n\n但是我们仍具有以下限制：\n\n1. [ 调用端 ] 与 [ 被调用端 ] 的 `SaStorage` 数据无法互通。\n2. [ 被调用端 ] 执行的 `SaResponse.setHeader()`、`setStatus()` 等代码无效。\n\n应该合理避开以上 API 的使用。\n\n\n### RPC调用鉴权\n\n在之前的 [Same-Token](/micro/same-token) 章节，我们演示了基于 Feign 的 RPC 调用鉴权，下面我们演示一下在 Dubbo 中如何集成 Same-Token 模块。\n\n其实思路和 Feign 模式一致，在 [ 调用端 ] 追加 Same-Token 参数，在 [ 被调用端 ] 校验这个 Same-Token 参数：\n\n- 校验通过：调用成功。\n- 校验不通过：调用失败，抛出异常。\n\n我们有两种方式完成整合。\n\n##### 方式一、使用配置（推荐）\n\n直接在 `application.yml` 配置即可：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\t# 打开 RPC 调用鉴权 \n\tcheck-same-token: true\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 打开 RPC 调用鉴权 \nsa-token.check-same-token=true\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n##### 方式二、自建 Dubbo 过滤器校验\n此方式略显繁琐，好处是除了Same-Token，我们还可以添加其它自定义参数 (attachment)。\n\n1、在 [ 调用端 ] 的 `\\resources\\META-INF\\dubbo\\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件\n``` html\ndubboConsumerFilter=com.pj.DubboConsumerFilter\n```\n\n新建 `DubboConsumerFilter.java` 过滤器\n\n``` java\npackage com.pj;\n\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.*;\n\nimport cn.dev33.satoken.same.SaSameUtil;\n\n/**\n * Sa-Token 整合 Dubbo Consumer端过滤器 \n */\n@Activate(group = {CommonConstants.CONSUMER}, order = -10000)\npublic class DubboConsumerFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {\n\t\t\n\t\t// 追加 Same-Token 参数 \n\t\tRpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());\n\t\t\n\t\t// 如果有其他自定义附加数据，如租户\n\t\t// RpcContext.getContext().setAttachment(\"tenantContext\", tenantContext);\n\t\t\n\t\t// 开始调用\n\t\treturn invoker.invoke(invocation);\n\t}\n\n}\n```\n\n\n2、在 [ 被调用端 ] 的 `\\resources\\META-INF\\dubbo\\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件\n``` html\ndubboProviderFilter=com.pj.DubboProviderFilter\n```\n\n新建 `DubboProviderFilter.java` 过滤器\n\n``` java\npackage com.pj;\n\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.*;\n\nimport cn.dev33.satoken.same.SaSameUtil;\n\n/**\n * Sa-Token 整合 Dubbo Provider端过滤器 \n */\n@Activate(group = {CommonConstants.PROVIDER}, order = -10000)\npublic class DubboProviderFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {\n\t\t\n\t\t// 取出 Same-Token 进行校验 \n\t\tString sameToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN);\n\t\tSaSameUtil.checkToken(sameToken);\n\t\t\n\t\t// 取出其他自定义附加数据\n\t\t// TenantContext tenantContext = invocation.getAttachment(\"tenantContext\");\n\t\t\n\t\t// 开始调用\n\t\treturn invoker.invoke(invocation);\n\t}\n\n}\n```\n\n\n然后我们就可以进行安全的 RPC 调用了，不带有 Same-Token 参数的调用都会抛出异常，无法调用成功。\n\n"
  },
  {
    "path": "sa-token-doc/plugin/freemarker-extend.md",
    "content": "# Freemarker 自定义标签 \n\n本插件的作用是让我们可以在 Freemarker 页面中使用 Sa-Token 自定义标签以及相关API。\n\n--- \n\n### 1、引入依赖 \n首先我们确保项目已经引入 Freemarker 依赖，然后在此基础上继续添加：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- 在 Freemarker 页面中使用 Sa-Token 自定义标签 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-freemarker</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// 在 Freemarker 页面中使用 Sa-Token 自定义标签\nimplementation 'cn.dev33:sa-token-freemarker:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n### 2、注入 Sa-Token Freemarker 标签模板模型 对象 \n在 SaTokenConfigure 配置类中增加配置 \n``` java\n@Configuration\npublic class SaTokenConfigure {\n\n\t@Autowired\n\tFreeMarkerConfigurer configurer;\n\n\t/**\n\t * 注入 Sa-Token Freemarker 标签模板模型 对象\n\t */\n\t@PostConstruct\n\tpublic void setSaTokenTemplateModel() throws TemplateModelException {\n\n\t\t// 注入 Sa-Token Freemarker 标签模板模型，使之可以在 xxx.ftl 文件中使用 sa 标签，\n\t\t// 例如：<#if sa.login()>...</#if>\n\t\tconfigurer.getConfiguration().setSharedVariable(\"sa\", new SaTokenTemplateModel());\n\n\t\t// 注入 Sa-Token Freemarker 全局对象，使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法，\n\t\t// 例如：<span>${stp.getSession().get('name')}</span>\n\t\tconfigurer.getConfiguration().setSharedVariable(\"stp\", StpUtil.stpLogic);\n\t}\n\n}\n```\n\n\n### 3、使用自定义标签\n然后我们就可以愉快的使用在 Freemarker 页面中使用 Sa-Token 自定义标签了 \n\n##### 3.1、登录判断 \n``` html\n<h2>标签方言测试页面</h2>\n<p>\n\t登录之后才能显示：\n\t<@sa.login>value</@sa.login>\n</p>\n<p>\n\t不登录才能显示：\n\t<@sa.notLogin>value</@sa.notLogin>\n</p>\n```\n\n##### 3.2、角色判断\n``` html\n<p>\n\t具有角色 admin 才能显示：\n\t<@sa.hasRole value=\"admin\">value</@sa.hasRole>\n</p>\n<p>\n\t同时具备多个角色才能显示：\n\t<@sa.hasRoleAnd value=\"admin, ceo, cto\">value</@sa.hasRoleAnd>\n</p>\n<p>\n\t只要具有其中一个角色就能显示：\n\t<@sa.hasRoleOr value=\"admin, ceo, cto\">value</@sa.hasRoleOr>\n</p>\n<p>\n\t不具有角色 admin 才能显示：\n\t<@sa.notRole value=\"admin\">value</@sa.notRole>\n</p>\n```\n\n##### 3.3、权限判断\n``` html\n<p>\n\t具有权限 user-add 才能显示：\n\t<@sa.hasPermission value=\"user-add\">value</@sa.hasPermission>\n</p>\n<p>\n\t同时具备多个权限才能显示：\n\t<@sa.hasPermissionAnd value=\"user-add, user-delete, user-get\">value</@sa.hasPermissionAnd>\n</p>\n<p>\n\t只要具有其中一个权限就能显示：\n\t<@sa.hasPermissionOr value=\"user-add, user-delete, user-get\">value</@sa.hasPermissionOr>\n</p>\n<p>\n\t不具有权限 user-add 才能显示：\n\t<@sa.notPermission value=\"user-add\">value</@sa.notPermission>\n</p>\n```\n\n\n### 4、调用 Sa-Token 相关API  \n\n以上的自定义标签，可以满足我们大多数场景下的权限判断，然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API :\n\n \n``` html\n<p>\n\t从SaSession中取值：\n\t<#if stp.isLogin()>\n\t\t<span>${stp.getSession().get('name')}</span>\n\t</#if>\n</p>\n```\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/grpc-extend.md",
    "content": "# 和 grpc 集成\n\n本插件的作用是让 Sa-Token 和 grpc 做一个整合。\n\n--- \n\n和dubbo插件一样，解决了以下问题\n\n1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。\n2. 在 [ 调用端 ] 登录的会话，其登录状态可以自动传递到 [ 被调用端 ] ；在 [ 被调用端 ] 登录的会话，其登录状态可以自动回传到 [ 调用端 ]\n3. Same-Token 安全校验\n\n---\n和dubbo插件一样，具有以下限制：\n\n1. [ 调用端 ] 与 [ 被调用端 ] 的 `SaStorage` 数据无法互通。\n2. [ 被调用端 ] 执行的 `SaResponse.setHeader()`、`setStatus()` 等代码无效。\n\n### 引入插件\n需要springboot环境，添加依赖（调用端和被调用端都需要引入）：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 整合 grpc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-grpc</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 grpc\nimplementation 'cn.dev33:sa-token-grpc:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n---\n### 开启 Same-Token 校验：\n直接在 `application.yml` 配置即可：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\t# 打开 RPC 调用鉴权 \n\tcheck-same-token: true\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 打开 RPC 调用鉴权 \nsa-token.check-same-token=true\n```\n<!---------------------------- tabs:end ---------------------------->"
  },
  {
    "path": "sa-token-doc/plugin/json-extend.md",
    "content": "# JSON 序列化扩展\n\n--- \n\nSa-Token 在 Session 存储、Redis 缓存等场景下需要对对象进行 JSON 序列化与反序列化。框架将 JSON 转换逻辑抽象到 `SaJsonTemplate` 接口，\n开发者只需引入对应的 JSON 插件依赖，框架会通过 SPI 机制自动完成注入，接口签名：[SaJsonTemplate.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java)\n \n框架已提供的 JSON 序列化插件包括：\n\n- **sa-token-jackson**：集成 Jackson（com.fasterxml.jackson），适用于 SpringBoot2/3 等环境。\n- **sa-token-jackson3**：集成 Jackson 3（tools.jackson.core），适用于 SpringBoot4、Java 17+ 等环境。\n- **sa-token-fastjson**：集成 Fastjson。\n- **sa-token-fastjson2**：集成 Fastjson2。\n- **sa-token-snack3**：集成 Snack3。\n- **sa-token-snack4**：集成 Snack4。\n\n> 若使用 `sa-token-spring-boot-starter` 集成包（含 SpringBoot3），框架会自动引入 Jackson 作为默认 JSON 方案，一般无需额外配置。如需更换为其它 JSON 框架，引入对应插件依赖即可。\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:Jackson ------------->\n``` xml\n<!-- Sa-Token 整合 Jackson -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-jackson</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-jackson:${sa.top.version}'`\n\n<!------------- tab:Jackson3 ------------->\n``` xml\n<!-- Sa-Token 整合 Jackson3 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-jackson3</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-jackson3:${sa.top.version}'`\n\n<!------------- tab:Fastjson ------------->\n``` xml\n<!-- Sa-Token 整合 Fastjson -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-fastjson</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-fastjson:${sa.top.version}'`\n\n<!------------- tab:Fastjson2 ------------->\n``` xml\n<!-- Sa-Token 整合 Fastjson2 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-fastjson2</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-fastjson2:${sa.top.version}'`\n\n<!------------- tab:Snack3 ------------->\n``` xml\n<!-- Sa-Token 整合 Snack3 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-snack3</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-snack3:${sa.top.version}'`\n\n<!------------- tab:Snack4 ------------->\n``` xml\n<!-- Sa-Token 整合 Snack4 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-snack4</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-snack4:${sa.top.version}'`\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n有关 Redis 集成与序列化配置，详细参考：[集成 Redis](/up/integ-redis)\n\n更多自定义序列化方案（如 Base64、天干地支等），可参考：[序列化插件扩展包](/plugin/custom-serializer)\n"
  },
  {
    "path": "sa-token-doc/plugin/jwt-extend.md",
    "content": "# 和 jwt 集成 \n\n本插件的作用是让 Sa-Token 和 jwt 做一个整合。 \n\n--- \n\n### 1、引入依赖 \n首先在项目已经引入 Sa-Token 的基础上，继续添加：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 整合 jwt -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-jwt</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 jwt\nimplementation 'cn.dev33:sa-token-jwt:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n> [!WARNING| label:版本兼容性] \n> 1. 注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本，保险起见：你的项目中要么不引入 hutool，要么引入版本 >= 5.7.14 的 hutool 版本。\n> 2. hutool 5.8.13 和 5.8.14 版本下会出现类型转换问题，[关联issue](https://gitee.com/dromara/sa-token/issues/I6L429)。\n\n\n### 2、配置秘钥\n在 `application.yml` 配置文件中配置 jwt 生成秘钥：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token:\n\t# jwt秘钥 \n\tjwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# jwt秘钥 \nsa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n```\n<!---------------------------- tabs:end ---------------------------->\n\n注：为了安全起见请不要直接复制官网示例这个字符串（随便按几个字符就好了）\n\n\n### 3、注入jwt实现\n根据不同的整合规则，插件提供了三种不同的模式，你需要 **选择其中一种** 注入到你的项目中 \n\n<!------------------------------ tabs:start ------------------------------>\n\n<!-- tab: Simple 简单模式  -->\nSimple 模式：Token 风格替换\n``` java\n@Configuration\npublic class SaTokenConfigure {\n    // Sa-Token 整合 jwt (Simple 简单模式)\n\t@Bean\n    public StpLogic getStpLogicJwt() {\n    \treturn new StpLogicJwtForSimple();\n    }\n}\n```\n\n<!-- tab: Mixin 混入模式  -->\nMixin 模式：混入部分逻辑\n``` java\n@Configuration\npublic class SaTokenConfigure {\n    // Sa-Token 整合 jwt (Mixin 混入模式)\n\t@Bean\n    public StpLogic getStpLogicJwt() {\n    \treturn new StpLogicJwtForMixin();\n    }\n}\n```\n\n<!-- tab: Stateless 无状态模式  -->\nStateless 模式：服务器完全无状态\n``` java\n@Configuration\npublic class SaTokenConfigure {\n    // Sa-Token 整合 jwt (Stateless 无状态模式)\n\t@Bean\n    public StpLogic getStpLogicJwt() {\n    \treturn new StpLogicJwtForStateless();\n    }\n}\n```\n\n<!---------------------------- tabs:end ------------------------------>\n\n### 4、开始使用\n然后我们就可以像之前一样使用 Sa-Token 了 \n``` java\n/**\n * 登录测试 \n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n    // 测试登录\n    @RequestMapping(\"login\")\n    public SaResult login() {\n\t\tStpUtil.login(10001);\n        return SaResult.ok(\"登录成功\");\n    }\n\n    // 查询登录状态\n    @RequestMapping(\"isLogin\")\n    public SaResult isLogin() {\n        return SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n    }\n\n    // 测试注销\n    @RequestMapping(\"logout\")\n    public SaResult logout() {\n        StpUtil.logout();\n        return SaResult.ok();\n    }\n\n}\n```\n\n访问上述接口，观察Token生成的样式\n``` java\neyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbklkIjoiMTAwMDEiLCJybiI6IjZYYzgySzBHVWV3Uk5NTTl1dFdjbnpFZFZHTVNYd3JOIn0.F_7fbHsFsDZmckHlGDaBuwDotZwAjZ0HB14DRujQfOQ\n```\n\n\n### 5、不同模式策略对比\n\n注入不同模式会让框架具有不同的行为策略，以下是三种模式的差异点（为方便叙述，以下比较以同时引入 jwt 与 Redis 作为前提）：\n\n| 功能点\t\t\t\t\t\t| Simple 简单模式\t\t| Mixin 混入模式\t\t\t| Stateless 无状态模式\t|\n| :--------\t\t\t\t\t| :--------\t\t| :--------\t\t\t| :--------\t\t\t|\n| Token风格\t\t\t\t\t| jwt风格\t\t| jwt风格\t\t\t| jwt风格\t\t\t|\n| 登录数据存储\t\t\t\t| Redis中存储\t\t| Token中存储\t\t\t| Token中存储\t\t\t|\n| Session存储\t\t\t\t| Redis中存储\t\t| Redis中存储\t\t\t| 无Session\t\t\t|\n| 注销下线\t\t\t\t\t| 前后端双清数据\t| 前后端双清数据\t\t| 前端清除数据\t\t|\n| 踢人下线API\t\t\t\t| 支持\t\t\t| 不支持\t\t\t\t| 不支持\t\t\t\t|\n| 顶人下线API\t\t\t\t| 支持\t\t\t| 不支持\t\t\t\t| 不支持\t\t\t\t|\n| 登录认证\t\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| 角色认证\t\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| 权限认证\t\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| timeout 有效期\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| active-timeout 有效期\t\t| 支持\t\t\t| 支持\t\t\t\t| 不支持\t\t\t\t|\n| id反查Token\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 不支持\t\t\t\t|\n| 会话管理\t\t\t\t\t| 支持\t\t\t| 部分支持\t\t\t| 不支持\t\t\t\t|\n| 注解鉴权\t\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| 路由拦截鉴权\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| 账号封禁\t\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 不支持\t\t\t\t|\n| 身份切换\t\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| 二级认证\t\t\t\t\t| 支持\t\t\t| 支持\t\t\t\t| 支持\t\t\t\t|\n| 模式总结\t\t\t\t\t| Token风格替换\t| jwt 与 Redis 逻辑混合\t| 完全舍弃Redis，只用jwt\t\t|\n\n\n\n### 6、扩展参数\n你可以通过以下方式在登录时注入扩展参数：\n\n``` java\n// 登录10001账号，并为生成的 Token 追加扩展参数name\nStpUtil.login(10001, new SaLoginParameter().setExtra(\"name\", \"zhangsan\"));\n\n// 连缀写法追加多个\nStpUtil.login(10001, new SaLoginParameter()\n\t\t\t\t.setExtra(\"name\", \"zhangsan\")\n\t\t\t\t.setExtra(\"age\", 18)\n\t\t\t\t.setExtra(\"role\", \"超级管理员\"));\n\n// 获取扩展参数 \nString name = StpUtil.getExtra(\"name\");\n\n// 获取任意 Token 的扩展参数 \nString name = StpUtil.getExtra(\"tokenValue\", \"name\");\n```\n\n\n\n### 7、在多账户模式中集成 jwt\nsa-token-jwt 插件默认只为 `StpUtil` 注入 `StpLogicJwtFoxXxx` 实现，自定义的 `StpUserUtil` 是不会自动注入的，我们需要帮其手动注入：\n\n``` java\n/**\n * 为 StpUserUtil 注入 StpLogicJwt 实现 \n */\n@PostConstruct\npublic void setUserStpLogic() {\n\tStpUserUtil.setStpLogic(new StpLogicJwtForSimple(StpUserUtil.TYPE));\n}\n```\n\n\n\n### 8、自定义 SaJwtUtil 生成 token 的算法 \n\n如果需要自定义生成 token 的算法（例如更换sign方式），直接重写 SaJwtTemplate 对象即可：\n\n``` java\n/**\n * 自定义 SaJwtUtil 生成 token 的算法 \n */\n@PostConstruct\npublic void setSaJwtTemplate() {\n\tSaJwtUtil.setSaJwtTemplate(new SaJwtTemplate() {\n\t\t@Override\n\t\tpublic String generateToken(JWT jwt, String keyt) {\n\t\t\tSystem.out.println(\"------ 自定义了 token 生成算法\");\n\t\t\treturn super.generateToken(jwt, keyt);\n\t\t}\n\t});\n}\n```\n\n\n### 9、注意点\n\n##### 1、使用 jwt-simple 模式后，is-share=false 恒等于 false。\n\n`is-share=true` 的意思是每次登录都产生一样的 token，这种策略和 [ 为每个 token 单独设定 setExtra 数据 ] 不兼容的，\n为保证正确设定 Extra 数据，当使用 `jwt-simple` 模式后，`is-share` 配置项 恒等于 `false`。\n\n\n##### 2、使用 jwt-mixin 模式后，is-concurrent 必须为 true。\n\n`is-concurrent=false` 代表每次登录都把旧登录顶下线，但是 jwt-mixin 模式登录的 token 并不会记录在持久库数据中，\n技术上来讲无法将其踢下线，所以此时顶人下线和踢人下线等 API 都属于不可用状态，所以此时 `is-concurrent` 配置项必须配置为 `true`。\n\n\n##### 3、使用 jwt-mixin 模式后，max-try-times 恒等于 -1。\n\n为防止框架错误判断 token 唯一性，当使用 jwt-mixin 模式后，`max-try-times` 恒等于 -1。\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/plugin-dev.md",
    "content": "# Sa-Token 插件开发指南\n\n本插件的作用是让 Sa-Token 和 Dubbo 做一个整合。 \n\n--- \n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/quick-login.md",
    "content": "# Sa-Token-Quick-Login 快速登录认证\n--- \n\n\n### 解决什么问题\n\nSa-Token-Quick-Login 可以为一个系统快速的、零代码 注入一个登录页面 \n\n试想一下，假如我们开发了一个非常简单的小系统，比如说：服务器性能监控页面，\n我们将它部署在服务器上，通过访问这个页面，我们可以随时了解服务器性能信息，非常方便\n\n然而，这个页面方便我们的同时，也方便了一些不法的攻击者，由于这个页面毫无防护的暴露在公网中，任何一台安装了浏览器的电脑都可以随时访问它！\n\n为此，我们必须给这个系统加上一个登录认证，只有知晓了后台密码的人员才可以进行访问\n\n细细想来，完成这个功能你需要：\n\n1. 编写前端登录页面，手写各种表单样式\n2. 寻找合适的ajax类库，`jQuery`？`Axios`？还是直接前后台不分离？\n3. 寻找合适的模板引擎，比如`jsp`、`Thymeleaf`、`FreeMarker`、`Velocity`……选哪个呢？\n4. 处理后台各种拦截认证逻辑，前后台接口对接\n5. 你可能还会遇到令人头痛欲裂的模板引擎中`ContextPath`处理\n6. ……\n\n你马上就会发现，写个监控页你一下午就可以搞定，然而这个登录页你却可能需要花上两三天的时间，这是一笔及其不划算的时间浪费\n\n那么现在你可能就会有个疑问，难道就没有什么方法给我的小项目快速增加一个登录功能吗？\n\nSa-Token-Quick-Login便是为了解决这个问题！\n\n\n### 适用场景\n\nSa-Token-Quick-Login 旨在用最小的成本为项目增加一个登录认证功能\n\n- **简单**：只需要引入一个依赖便可为系统注入登录功能，快速、简单、零代码！\n- **不可定制**：由于登录页面不可定制，所以Sa-Token-Quick-Login非常不适合普通项目的登录认证模块，STQL也无意去解决所有项目的登录认证模块\n\nSa-Token-Quick-Login的定位是这样的场景：你的项目需要一个登录认证功能、这个认证页面可以不华丽、可以烂，但是一定要有，同时你又不想花费太多的时间浪费在登录页面上，\n那么你便可以尝试一下`Sa-Token-Quick-Login`\n\n\n### 集成步骤\n首先我们需要创建一个SpringBoot的demo项目，比如：`sa-token-demo-quick-login`\n\n##### 1、添加pom依赖\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml\n<!-- Sa-Token 启动依赖 -->\n<dependency>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-spring-boot-starter</artifactId>\n        <version>${sa.top.version}</version>\n</dependency>\n<!-- Sa-Token-Quick-Login 插件依赖 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-quick-login</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 启动依赖\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n// Sa-Token-Quick-Login 插件\nimplementation 'cn.dev33:sa-token-quick-login:${sa.top.version}'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n##### 2、启动类\n``` java\n@SpringBootApplication\npublic class SaTokenQuickDemoApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaTokenQuickDemoApplication.class, args);\n\t\t\n\t\tSystem.out.println(\"\\n------ 启动成功 ------\");\n\t\tSystem.out.println(\"name: \" + SaQuickManager.getConfig().getName());\n\t\tSystem.out.println(\"pwd:  \" + SaQuickManager.getConfig().getPwd());\n\t}\n}\n```\n\n##### 3、新建测试Controller\n``` java\n/**\n * 测试专用Controller \n */\n@RestController\npublic class TestController {\n\t// 浏览器访问测试： http://localhost:8081\n\t@RequestMapping({\"/\", \"/index\"})\n\tpublic String index() {\n\t\tString str = \"<br />\"\n\t\t\t\t+ \"<h1 style='text-align: center;'>资源页 （登录后才可进入本页面） </h1>\"\n\t\t\t\t+ \"<hr/>\"\n\t\t\t\t+ \"<p style='text-align: center;'> Sa-Token \" + SaTokenConsts.VERSION_NO + \" </p>\";\n\t\treturn str;\n\t}\n}\n```\n\n### 测试访问\n启动项目，使用浏览器访问：`http://localhost:8081`，首次访问时，由于处于未登录状态，会被强制进入登录页面\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-quick-login.png\" alt=\"登录\" />\n\n使用默认账号：`sa / 123456`进行登录，会看到资源页面\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-quick-login-index.png\" alt=\"登录\" />\n\n也可以通过 Http Basic 的方式直接进行认证 (一般需要在专门的 API 测试工具下才能正常测试，浏览器会自动忽略@之前的信息)\n\n``` url\nhttp://sa:123456@localhost:8081/\n```\n\n\n### 可配置信息\n你可以在yml中添加如下配置 (所有配置都是可选的) \n\n<!---------------------------- tabs:start ---------------------------->\n\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token-Quick-Login 配置\nsa: \n\t# 登录账号\n\tname: sa\n\t# 登录密码\n\tpwd: 123456\n\t# 是否自动随机生成账号密码 (此项为true时, name与pwd失效)\n\tauto: false\n\t# 是否开启全局认证(关闭后将不再强行拦截) \n\tauth: true\n\t# 登录页标题\n\ttitle: Sa-Token 登录\n\t# 是否显示底部版权信息 \n\tcopr: true\n    # 指定拦截路径 \n    # include: /**\n    # 指定排除路径\n    # exclude: /1.jpg\n```\n\n<!------------- tab:properties 风格  ------------->\n``` properties\n####### Sa-Token-Quick-Login 配置 #######\n# 登录账号\nsa.name=sa\n# 登录密码\nsa.pwd=123456\n# 是否自动随机生成账号密码 (此项为true时, name与pwd失效)\nsa.auto=false\n# 是否开启全局认证(关闭后将不再强行拦截) \nsa.auth=true\n# 登录页标题\nsa.title=Sa-Token 登录\n# 是否显示底部版权信息 \nsa.copr=true\n# 指定拦截路径 \n# sa.include=/**\n# 指定排除路径\n# sa.exclude=/1.jpg\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n<br>\n\n**注：**示例源码在`/sa-token-demo/sa-token-demo-quick-login`目录下，可结合源码查看学习\n\n\n\n### 使用独立jar包运行\n使用`sa-token-quick-login`只需引入一个依赖即可为系统注入登录模块，现在我们更进一步，将这个项目打成一个独立的jar包\n\n通过这个jar包，我们可以方便的部署任意静态网站！做到真正的零编码注入登录功能。\n\n\n##### 打包步骤\n\n<!-- [sa-quick-dist.jar](https://gitee.com/dromara/sa-token/attach_files/695353/download) -->\n\n首先放上懒人链接：[sa-quick-dist.jar](https://pan.quark.cn/s/04fd34a24928)，不想手动操作的同学可以直接点此链接下载打包后的jar文件 \n\n1、首先将 `sa-token-demo-quick-login` 模块添加到顶级父模块的`<modules>`节点中\n\n``` xml\n<!-- 所有模块 -->\n<modules>\n\t<module>sa-token-core</module>\n\t<module>sa-token-starter</module>\n\t<module>sa-token-plugin</module>\n\t<module>sa-token-demo\\sa-token-demo-quick-login</module>\n</modules>\n```\n\n2、在项目根目录进入cmd执行打包命令\n\n``` cmd\nmvn clean package\n```\n\n3、进入`\\sa-token-demo\\sa-token-demo-quick-login\\target` 文件夹，找到打包好的jar文件 \n\n``` cmd\nsa-token-demo-quick-login-0.0.1-SNAPSHOT.jar\n```\n\n4、我们将其重命名为`sa-quick-dist.jar`，现在这个jar包就是我们的最终程序，我们在这个`\\target`目录直接进入cmd，执行如下命令启动jar包\n\n``` cmd\njava -jar sa-quick-dist.jar\n```\n\n5、测试访问，根据控制台输出提示，我们使用浏览器访问测试: `http://localhost:8080`\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-quick-start.png\" alt=\"sa-quick-start\" />\n\n如果可以进入登录界面，则代表打包运行成功 <br>\n当然仅仅运行成功还不够，下面我们演示一下如何使用这个jar包进行静态网站部署\n\n\n### 所有功能示例\n\n##### Case 1. 指定静态资源路径\n``` cmd\njava -jar sa-quick-dist.jar --sa.dir file:E:\\www\n```\n使用dir参数指定`E:\\www`目录作为资源目录进行部署 (现在我们可以通过浏览器访问`E:\\www`目录下的文件了！)\n\n##### Case 2. 指定登录名与密码\n``` cmd\njava -jar sa-quick-dist.jar --sa.name=zhang --sa.pwd=zhang123\n```\n现在，默认的账号`sa/123456`将被废弃，而是使用`zhang/zhang123`进行账号校验\n\n##### Case 3. 指定其自动生成账号密码\n``` cmd\njava -jar sa-quick-dist.jar --sa.auto=true\n```\n每次启动时随机生成账号密码（会在启动成功时打印到控制台上）\n\n##### Case 4. 指定登录页的标题\n``` cmd\njava -jar sa-quick-dist.jar --sa.title=\"XXX 系统登录\"\n```\n\n##### Case 5. 关闭账号校验，仅作为静态资源部署使用\n``` cmd\njava -jar sa-quick-dist.jar --sa.auth=false\n```\n\n##### Case 6. 指定启动端口（默认8080）\n``` cmd\njava -jar sa-quick-dist.jar --server.port=80 \n```\n\n注：所有参数可组合使用\n\n\n### 使用SpringBoot默认资源路径\nSpringBoot默认开放了一些路径作为资源目录，比如`classpath:/static/`，\n怎么使用呢？我们只需要在jar包同目录创建一个`\\static`文件夹，将静态资源文件复制到此目录下，然后启动jar包即可访问\n\n同时，我们还可以在jar包同目录创建yml配置文件，来覆盖jar包内的yml配置，如下图所示：\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-quick-case.png\" alt=\"sa-quick-case.png\" />\n\n例如如上目录中`/static`中有一个`1.jpg`文件，我们启动jar包后访问`http://localhost:8080/1.jpg`即可查看到此文件，这是Springboot自带的功能，在此不再赘述\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/plugin/spel-at.md",
    "content": "# SpEL 表达式注解鉴权\n\nSa-Token 提供一个 `@SaCheckEL` 鉴权注解，该注解允许你使用 SpEL 表达式进行鉴权。\n\n\n### 1、引入插件\n\n由于该注解的工作底层需要依赖 SpringAOP 切面编程，因此你需要单独引入插件包 `sa-token-spring-el` 才可以使用此注解。\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 注解鉴权使用 EL 表达式 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-el</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 注解鉴权使用 EL 表达式\nimplementation 'cn.dev33:sa-token-spring-el:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n### 2、简单示例\n\n以下是一些使用示例：\n``` java\n@RestController\n@RequestMapping(\"/check-el/\")\npublic class SaCheckELController {\n\n\t// 登录校验 \n\t@SaCheckEL(\"stp.checkLogin()\")\n\t@RequestMapping(\"test1\")\n\tpublic SaResult test1() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限校验 \n\t@SaCheckEL(\"stp.checkPermission('user:edit')\")\n\t@RequestMapping(\"test3\")\n\tpublic SaResult test3() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 参数长度校验 \n\t@SaCheckEL(\"NEED( #name.length() > 3 )\")\n\t@RequestMapping(\"test5\")\n\tpublic SaResult test5(@RequestParam(defaultValue = \"\") String name) {\n\t\treturn SaResult.ok().set(\"name\", name);\n\t}\n\n\t// SaSession 里取值校验 \n\t@SaCheckEL(\"NEED( stp.getSession().get('name') == 'zhangsan' )\")\n\t@RequestMapping(\"test8\")\n\tpublic SaResult test8() {\n\t\treturn SaResult.ok();\n\t}\n\n}\n```\n\n\n### 3、多账号体系鉴权\n\n要在 EL 表达式中使用多账号体系鉴权模式，你需要在配置类中重写 `SaCheckELRootMap 扩展函数`，增加 EL 表达式可使用的根对象：\n\n``` java\n@Configuration\npublic class SaTokenConfigure {\n\t\n    /**\n     * 重写 Sa-Token 框架内部算法策略 \n     */\n    @PostConstruct\n    public void rewriteSaStrategy() {\n\t\t// 重写 SaCheckELRootMap 扩展函数，增加注解鉴权 EL 表达式可使用的根对象\n\t\tSaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> {\n\t\t\tSystem.out.println(\"--------- 执行 SaCheckELRootMap 增强，目前已包含的的跟对象包括：\" + rootMap.keySet());\n\t\t\t// 新增 stpUser 根对象，使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权\n\t\t\trootMap.put(\"stpUser\", StpUserUtil.getStpLogic());\n\t\t};\n    }\n\n}\n```\n\n然后就可以使用多账号体系鉴权模式了\n\n``` java\n// 多账号体系鉴权测试 \n@SaCheckEL(\"stpUser.checkLogin()\")\n@RequestMapping(\"test9\")\npublic SaResult test9() {\n\treturn SaResult.ok();\n}\n```\n\n\n### 4、调用本类成员变量\n``` java\n// 本模块需要鉴权的权限码\npublic String permissionCode = \"article:add\";\n\n// 调用本类的成员变量 \n@SaCheckEL(\"stp.checkPermission( this.permissionCode )\")\n@RequestMapping(\"test10\")\npublic SaResult test10() {\n\treturn SaResult.ok();\n}\n```\n\n\n### 5、忽略鉴权\n配合 `@SaIgnore` 注解做到忽略某接口的鉴权\n``` java \n// 忽略鉴权测试 \n@SaIgnore\n@SaCheckEL(\"stp.checkPermission( 'abc' )\")\n@RequestMapping(\"test11\")\npublic SaResult test11() {\n\treturn SaResult.ok();\n}\n```\n\n\n### 6、代码提示\n\n如果在书写 SpEL 表达式时需要代码提示：\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-check-el-code-tips.png\" alt=\"sa-check-el-code-tips.png\" />\n\n可以在 idea 中安装 **SpEL Assistant** 插件，该插件由 `@ly-chn` 提供，允许为自定义注解书写 SpEL 表达式时增加代码提示功能，\n开源地址：[https://github.com/ly-chn/SpEL-Assistant](https://github.com/ly-chn/SpEL-Assistant)\n\n安装方式：直接在 idea 插件商店中搜索 “**SpEL Assistant**” 即可\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/sa-check-el-setup-plugin.png\" alt=\"sa-check-el-code-tips.png\" />\n\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/more/SaCheckELController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token SpEL表达式注解鉴权 —— [ SaCheckELController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/plugin/temp-token.md",
    "content": "# 临时 Token 令牌认证  \n\n---\n\n### 1、适用场景 \n\n在部分业务场景，我们需要一种临时授权的能力，即：一个token的有效期并不需要像登录有效期那样需要[七天、三十天]，而是仅仅需要 [五分钟、半小时]。\n\n举个比较明显的例子：超链接邀请机制。\n\n> [!NOTE| label:业务场景] \n> \n> 你在一个游戏中创建一个公会 `(id=10014)`，现在你想邀请你的好朋友加入这个公会，在你点击 **`[邀请]`** 按钮时，系统为你生成一个连接: \n> \n> ``` xml\n> http://xxx.com/apply?id=10014\n> ```\n> \n> 接着，你的好朋友点击这个链接，加入了你的工会。\n> \n> 那么，系统是如何识别这个链接对应的工会是10014呢？很明显，我们可以观察出，这个链接的尾部有个id参数值为10014，这便是系统识别的关键。\n> \n> 此时你可能眉头一紧，就这么简单？那我如果手动更改一下尾部的参数改成10015，然后我再一点，岂不是就可以偷偷加入别人的工会了？\n> \n> 你想的没错，如果这个游戏的架构设计者采用上述方案完成功能的话，这个邀请机制就轻松的被你攻破了。\n> \n> 但是很明显，正常的商业项目一般不会拉跨到这种地步，比较常见的方案是，对这个公会id做一个token映射，最终你看到链接一般是这样的：\n> \n> ``` xml\n> http://xxx.com/apply?token=oEwQBnglXDoGraSJdGaLooPZnGrk\n> ```\n> \n> 后面那一串字母是乱打出来的，目的是为了突出它的随机性，即：使用一个随机的token来代替明文显示真正的数据。\n> \n> 在用户点击这个链接之后，服务器便可根据这个token解析出真正公会id (10014) ，至于伪造？全是随机的你怎么伪造？你又不知道10015会随机出一个什么样的Token 。\n> \n> 而且为了安全性，这个token的有效期一般不会太长，给你预留五分钟、半小时的时间足够你点击它即可。\n\n\n### 2、创建临时 token\n\n**[sa-token-temp 临时 token 认证模块]** 已内嵌到核心包，无需引入其它依赖即可使用：\n\n``` java\n// 根据 value 创建一个 token \nString token = SaTempUtil.createToken(\"10014\", 200);\n\n// 解析 token 获取 value，并转换为指定类型 \nString value = SaTempUtil.parseToken(token, String.class);\n\n// 获取指定 token 的剩余有效期，单位：秒 \nSaTempUtil.getTimeout(token);\n\n// 删除指定 token\nSaTempUtil.deleteToken(token);\n```\n\n\n### 3、前缀拼接与裁剪\n\n``` java\n// 如果由多条业务线都需要生成临时 token，可以加个前缀进行区分\nString token = SaTempUtil.createToken(\"shop_1001\", 1200);\n```\n\n在获取时可以自行裁剪前缀，也可以调用：\n``` java\n// 解析 token 获取 value，并裁剪指定前缀，然后转换为指定类型\nSaTempUtil.parseToken(token, \"shop_\", Long.class)\n```\n\n如果指定了错误的前缀，即使 token 正确，上述方法也将返回 null \n\n\n### 4、根据 value 反查 token\n\n在创建 token 时，框架默认只会保存 `token -> value` 的映射，而不会记录 `value -> token` 的索引信息。\n\n如果想要做到反查 token，则必须在创建 token 指定框架记录 token 索引信息：\n\n``` java\n// 在创建 token 时，指定第三个参数 true，即可让框架在保存 token 时同时记录 token 索引信息\nString token1 = SaTempUtil.createToken(10004, 1200, true);\nString token2 = SaTempUtil.createToken(10004, 1300, true);\nString token3 = SaTempUtil.createToken(10004, -1, true);\n\n// 获取 10004 对应的所有 token \nList<String> list = SaTempUtil.getTempTokenList(10004);\nSystem.out.println(list);\n```\n\n\n\n### 5、集成jwt\n提到 [临时Token认证]，你是不是想到一个专门干这件事的框架？对，就是JWT！\n\n**[sa-token-temp]** 模块允许以JWT作为逻辑内核完成工作，你只需要引入以下依赖，所有上层API保持不变\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-temp-jwt</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\nimplementation 'cn.dev33:sa-token-temp-jwt:${sa.top.version}'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n\n并在配置文件中配置上jwt秘钥 **`(必填!)`**\n``` yml\nsa-token: \n\t# sa-token-temp-jwt 模块的秘钥 （随便乱摁几个字母就行了） \n\tjwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk\n```\n"
  },
  {
    "path": "sa-token-doc/plugin/thymeleaf-extend.md",
    "content": "# Thymeleaf 标签方言\n\n本插件的作用是让我们可以在 Thymeleaf 页面中使用 Sa-Token 相关API，俗称 —— 标签方言。\n\n--- \n\n### 1、引入依赖 \n首先我们确保项目已经引入 Thymeleaf 依赖，然后在此基础上继续添加：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- 在 thymeleaf 标签中使用 Sa-Token -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-thymeleaf</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// 在 thymeleaf 标签中使用 Sa-Token\nimplementation 'cn.dev33:sa-token-thymeleaf:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n### 2、注册标签方言对象 \n在 SaTokenConfigure 配置类中注册 Bean \n``` java\n@Configuration\npublic class SaTokenConfigure {\n\t// Sa-Token 标签方言 (Thymeleaf版)\n\t@Bean\n\tpublic SaTokenDialect getSaTokenDialect() {\n\t\treturn new SaTokenDialect();\n\t}\n}\n```\n\n\n### 3、使用标签方言 \n然后我们就可以愉快的使用在 Thymeleaf 页面中使用标签方言了 \n\n##### 3.1、登录判断 \n``` html\n<h2>标签方言测试页面</h2>\n<p>\n\t登录之后才能显示：\n\t<span sa:login>value</span>\n</p>\n<p>\n\t不登录才能显示：\n\t<span sa:notLogin>value</span>\n</p>\n```\n\n##### 3.2、角色判断\n``` html\n<p>\n\t具有角色 admin 才能显示：\n\t<span sa:hasRole=\"admin\">value</span>\n</p>\n<p>\n\t同时具备多个角色才能显示：\n\t<span sa:hasRoleAnd=\"admin, ceo, cto\">value</span>\n</p>\n<p>\n\t只要具有其中一个角色就能显示：\n\t<span sa:hasRoleOr=\"admin, ceo, cto\">value</span>\n</p>\n<p>\n\t不具有角色 admin 才能显示：\n\t<span sa:notRole=\"admin\">value</span>\n</p>\n```\n\n##### 3.3、权限判断\n``` html\n<p>\n\t具有权限 user-add 才能显示：\n\t<span sa:hasPermission=\"user-add\">value</span>\n</p>\n<p>\n\t同时具备多个权限才能显示：\n\t<span sa:hasPermissionAnd=\"user-add, user-delete, user-get\">value</span>\n</p>\n<p>\n\t只要具有其中一个权限就能显示：\n\t<span sa:hasPermissionOr=\"user-add, user-delete, user-get\">value</span>\n</p>\n<p>\n\t不具有权限 user-add 才能显示：\n\t<span sa:notPermission=\"user-add\">value</span>\n</p>\n```\n\n\n### 4、调用 Sa-Token 相关API  \n\n以上的标签方言，可以满足我们大多数场景下的权限判断，然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API  \n\n首先在 SaTokenConfigure 配置类中为 Thymeleaf 配置全局对象：\n\n``` java\n@Configuration\npublic class SaTokenConfigure{\n\t// ... 其它代码\n\t\n\t// 为 Thymeleaf 注入全局变量，以便在页面中调用 Sa-Token 的方法 \n\t@Autowired\n\tprivate void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) {\n\t\tviewResolver.addStaticVariable(\"stp\", StpUtil.stpLogic);\n\t}\n}\n```\n\n> [!WARNING| label:注意] \n> 如果`SaTokenConfigure`继承了`WebMvcConfigurer`等类，可能会造成循环依赖，如果遇到，请新建一个其他配置类完成此项配置\n\n\n然后我们就可以在页面上调用 StpLogic 的 API 了，例如：\n \n``` html\n<p>调用 StpLogic 方法调用测试</p>\n<p th:if=\"${stp.isLogin()}\">\n\t从SaSession中取值：\n\t<span th:text=\"${stp.getSession().get('name')}\"></span>\n</p>\n```\n\n\n### 5、代码提示\n\n如果想在写标签属性时增加代码提示：\n\n<img class=\"s-w\" src=\"/big-file/doc/plugin/thymeleaf-code-tips.png\" alt=\"thymeleaf-code-tips.png\" />\n\n只需在头部声明增加上对应的命名空间即可：\n\n``` html\n<!DOCTYPE html>\n<html lang=\"zh\" xmlns:sa=\"http://www.thymeleaf.org/extras/sa-token\">\n\t<head>\n\t\t<!-- 代码 -->\n\t</head>\n\t<body>\n\t\t<!-- 代码 -->\n\t</body>\n</html>\n```\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/pro/st_doc_top.md",
    "content": "# Sa-Max 统一认证商业版\n\n### 项目介绍\n\n根据 SSO / OAuth2 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。\n\n然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此我们特意准备了项目：\n[[ Sa-Max 统一认证商业版 ]](https://sa-pro.yun94.cn?way=st_doc_top)。\n\n项目基于 Sa-Token 搭建，集成了统一认证常见技术点，<b style=\"color: #FF5722;\">可大大缩短您的项目接入统一授权认证的开发周期</b>：\n- 支持：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 \n- 支持：API Key、OAuth2.0 统一认证能力， 并提供开放平台，供第三方公司申请应用对接平台能力。\n\n全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。\n\n\n\n\n\n### 释疑\n\n##### 1、Sa-Max 是收费项目吗？与 Sa-Token 有什么不同？\n\n`Sa-Max` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。\n\n`Sa-Max` 与 `Sa-Token` 的区别，简单来讲：\n- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入\n- `Sa-Max` 是一个完整项目，下载源码后可直接启动 \n\n\n##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？\n首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 \n\n其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，\n比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。\n\nSa-Max 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。\n\n另外：即使您没有购买 `Sa-Max`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。\n\n\n\n##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！\n\n答案是不会。\n\n再次强调一下：`Sa-Token` 与 `Sa-Max` 是两个独立的项目，两者互不影响。\n付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。\n\n实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。\n\n<!-- 衷心感谢每一位粉丝的支持！ -->\n\n\n\n"
  },
  {
    "path": "sa-token-doc/pro/st_index_top.md",
    "content": "# Sa-Max 统一认证商业版\n\n### 项目介绍\n\n根据 SSO / OAuth2 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。\n\n然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此我们特意准备了项目：\n[[ Sa-Max 统一认证商业版 ]](https://sa-pro.yun94.cn?way=st_index_top)。\n\n项目基于 Sa-Token 搭建，集成了统一认证常见技术点，<b style=\"color: #FF5722;\">可大大缩短您的项目接入统一授权认证的开发周期</b>：\n- 支持：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 \n- 支持：API Key、OAuth2.0 统一认证能力， 并提供开放平台，供第三方公司申请应用对接平台能力。\n\n全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。\n\n\n\n\n\n### 释疑\n\n##### 1、Sa-Max 是收费项目吗？与 Sa-Token 有什么不同？\n\n`Sa-Max` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。\n\n`Sa-Max` 与 `Sa-Token` 的区别，简单来讲：\n- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入\n- `Sa-Max` 是一个完整项目，下载源码后可直接启动 \n\n\n##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？\n首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 \n\n其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，\n比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。\n\nSa-Max 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。\n\n另外：即使您没有购买 `Sa-Max`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。\n\n\n\n##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！\n\n答案是不会。\n\n再次强调一下：`Sa-Token` 与 `Sa-Max` 是两个独立的项目，两者互不影响。\n付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。\n\n实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。\n\n<!-- 衷心感谢每一位粉丝的支持！ -->\n\n\n\n"
  },
  {
    "path": "sa-token-doc/pro/st_oauth2.md",
    "content": "# Sa-Max 统一认证商业版\n\n### 项目介绍\n\n根据 OAuth2 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个 OAuth2 模式的认证 Demo 示例。\n\n然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此我们特意准备了项目：\n[[ Sa-Max 统一认证商业版 ]](https://sa-pro.yun94.cn?way=st_oauth2)。\n\n项目基于 Sa-Token 搭建，集成了统一认证常见技术点，<b style=\"color: #FF5722;\">可大大缩短您的项目接入统一授权认证的开发周期</b>：\n- 支持：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 \n- 支持：API Key、OAuth2.0 统一认证能力， 并提供开放平台，供第三方公司申请应用对接平台能力。\n\n全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。\n\n\n\n\n\n### 释疑\n\n##### 1、Sa-Max 是收费项目吗？与 Sa-Token 有什么不同？\n\n`Sa-Max` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。\n\n`Sa-Max` 与 `Sa-Token` 的区别，简单来讲：\n- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入\n- `Sa-Max` 是一个完整项目，下载源码后可直接启动 \n\n\n##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？\n首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 \n\n其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，\n比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。\n\nSa-Max 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。\n\n另外：即使您没有购买 `Sa-Max`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。\n\n\n\n##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！\n\n答案是不会。\n\n再次强调一下：`Sa-Token` 与 `Sa-Max` 是两个独立的项目，两者互不影响。\n付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。\n\n实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。\n\n<!-- 衷心感谢每一位粉丝的支持！ -->\n\n\n\n"
  },
  {
    "path": "sa-token-doc/pro/st_sso.md",
    "content": "# Sa-Pro 单点登录商业版\n\n### 项目介绍\n\n根据 SSO 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个SSO模式的认证 Demo 示例。\n\n然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此，我们特意准备了项目：\n[[ Sa-Pro 单点登录商业版 ]](https://sa-pro.yun94.cn?way=st_sso)。\n\n项目基于 Sa-Token 搭建，集成了单点登录常见技术点，可解决： \n同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 \n<b style=\"color: #FF5722;\">可大大缩短您的项目接入单点登录的开发周期</b>。\n\n全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。\n\n\n\n\n\n### 释疑\n\n##### 1、Sa-Pro 是收费项目吗？与 Sa-Token 有什么不同？\n\n`Sa-Pro` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。\n\n`Sa-Pro` 与 `Sa-Token` 的区别，简单来讲：\n- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入\n- `Sa-Pro` 是一个完整项目，下载源码后可直接启动 \n\n\n##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？\n首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 \n\n其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，\n比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。\n\nSa-Pro 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。\n\n另外：即使您没有购买 `Sa-Pro`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。\n\n\n\n##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Pro 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！\n\n答案是不会。\n\n再次强调一下：`Sa-Token` 与 `Sa-Pro` 是两个独立的项目，两者互不影响。\n付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。\n\n实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。\n\n<!-- 衷心感谢每一位粉丝的支持！ -->\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/anon-client.md",
    "content": "# 匿名 Client 接入\n\n匿名 Client 就是指在客户端没有配置 `sso-client` 的应用，没有一个明确的 “Client” 标识名称。\n\n匿名 Client 在一些关键步骤中不会构建 `client` 参数，如：“重定向至认证中心授权地址”、“校验 ticket”、“单点注销” 等。\n\n要想匿名 client 接入，你需要做一些特殊配置。\n\n\n### 1、在 sso-server 端开启匿名 client 接入\n\n开启方式一，通过配置项方式：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token:\n    # SSO-Server 配置\n    sso-server:\n        # 是否启用匿名 client (开启匿名 client 后，允许客户端接入时不提交 client 参数)\n        allow-anon-client: true\n        # 所有允许的授权回调地址 (匿名 client 使用)\n        allow-url: \"*\"\n        # API 接口调用秘钥 (全局默认 + 匿名 client 使用)\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# SSO-Server 配置\n# 是否启用匿名 client (开启匿名 client 后，允许客户端接入时不提交 client 参数)\nsa-token.sso-server.allow-anon-client=true\n# 所有允许的授权回调地址 (匿名 client 使用)\nsa-token.sso-server.allow-url=*\n# API 接口调用秘钥 (全局默认 + 匿名 client 使用)\nsa-token.sso-server.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n开启方式二，通过代码重写方式：\n\n``` java\n/**\n * 重写 SaSsoServerTemplate 部分方法，增强功能\n */\n@Component\npublic class CustomSaSsoServerTemplate extends SaSsoServerTemplate {\n\n    /**\n     * 获取配置项：是否允许匿名 client 接入\n     */\n    @Override\n    public boolean getConfigOfAllowAnonClient() {\n        return true;\n    }\n\n    /**\n     * 获取匿名 client 配置信息\n     */\n    @Override\n    public SaSsoClientModel getAnonClient() {\n        SaSsoClientModel scm = new SaSsoClientModel();\n        scm.setAllowUrl(\"*\");  // 允许的授权地址\n        scm.setIsSlo(true);  // 是否允许单点注销\n        scm.setSecretKey(\"kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\");  // 客户端密钥\n        return scm;\n    }\n}\n```\n\n\n### 2、在 sso-client 端不要配置 client 字段\n\n然后在对应的应用端不要配置 client 字段，例如：\n\n``` yml\n# sa-token配置 \nsa-token:\n    # 配置一个不同的 token-name，以避免在和模式三 demo 一起测试时发生数据覆盖\n    token-name: satoken-client-anon\n    # sso-client 相关配置\n    sso-client:\n        # client 标识 匿名应用就是指不配置 client 标识的应用\n        # client: sso-client3\n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 使用 Http 请求校验ticket (模式三)\n        is-http: true\n        # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个)\n        reg-logout-call: true\n        # API 接口调用秘钥\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n\n\n> [!TIP| label:demo] \n> 匿名 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)\n\n这里有个值得注意的配置项：`reg-logout-call: true`，是干嘛的？\n\n简单来讲，就是匿名应用不包含 client 字段信息，因此 sso-server 端也无法配置此 client 的消息推送地址，所以此 client 无法接受到消息推送，也就无法参与到单点注销的环路中来。\n\n因此，新增一个配置项 `reg-logout-call: true`，代表在登录的同时把当前项目的单点注销回调地址 `/sso/logoutCall` 发送到 sso-server 端，\n这样 sso-server 端有了备案，也就可以成功通知此应用发起单点注销掉了。\n\n如果当前应用不需要单点注销可以不配置此字段。\n\n"
  },
  {
    "path": "sa-token-doc/sso/message-push.md",
    "content": "# 消息推送机制\n\n消息推送机制简单来讲就是：sso-client 端按照特点格式构建一个 http 请求，调用 sso-server 端的 `/sso/pushS` 接口，sso-server 接收到消息后做出处理回应 sso-client 端。\n\n消息推送是相互的，sso-server 端也可以构建 http 请求，调用 sso-client 端的 `/sso/pushC` 接口。\n\n消息推送机制是应用与认证中心相互沟通的桥梁，ticket 校验、单点注销等行为都是依赖消息推送机制来实现的。\n\n本篇将介绍在 Sa-Token SSO 模块中，sso-server 端和 sso-client 端分别内置了哪些消息模块，以及如何自定义消息处理器。\n\n\n### 1、sso-server 端内置消息处理器\n\n#### 1.1、checkTicket（ticket 校验）\n\n作用：在 SSO 模式三下为 sso-client 提供 ticket 校验能力，返回 loginId 等数据\n\n``` url\nhttp://{sso-server主机地址}/sso/pushS\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| msgType\t\t| 是\t\t| 消息类型，此处填 `checkTicket`\t\t\t\t\t\t\t|\n| ticket\t\t| 是\t\t| ticket 码\t\t\t\t\t\t\t|\n| client\t\t| 否\t\t| 客户端标识，可不填，代表是一个匿名应用\t\t\t\t|\n| ssoLogoutCall\t| 否\t\t| Client 端单点注销时 - 回调 URL 参数名称 (匿名 Client 时使用)\t\t\t|\n| timestamp\t\t| 是\t\t| 当前时间戳，13位\t\t\t\t\t\t\t\t\t|\n| nonce\t\t\t| 是\t\t| 随机字符串\t\t\t\t\t\t\t\t\t\t|\n| sign\t\t\t| 是\t\t| 签名，生成算法示例：`md5( client={client值}&msgType={checkTicket}&nonce={随机字符串}&ticket={ticket码}&timestamp={13位时间戳}&key={secretkey秘钥} )`\t\t\t\t\t|\n\n**<font color=\"#080\" >签名算法规则：将所有参数按照字典顺序依次排列（key除外，挂在最后面），然后进行 md5 摘要。以下不再赘述。</font>**\n\n返回值示例：\n``` js\n{\n  \"code\": 200,   // 返回 200=成功，500=失败\n  \"msg\": \"ok\",\n  \"data\": \"10001\",\n  \"loginId\": \"10001\",   // 此 ticket 对应的认证中心 loginId \n  \"tokenValue\": \"5db12b02-9c8e-4e36-8ed9-bf295caed80e\",   // 对应的认证中心会话 token \n  \"deviceId\": \"MxOTCLWi5NXGqFQZBFdsH66Ni5YTJ8q0\",   // 对应的认证中心登录设备 id\n  \"remainTokenTimeout\": 2591999,   // token 剩余有效期\n  \"remainSessionTimeout\": 2591999   // Access-Session 会话剩余有效期\n}\n```\n\n\n#### 1.2、signout（单点注销）\n\n作用：为 sso-client 提供单点注销能力\n\n``` url\nhttp://{sso-server主机地址}/sso/pushS\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| msgType\t\t| 是\t\t| 消息类型，此处填 `signout`\t\t\t\t\t\t\t|\n| loginId\t\t| 是\t\t| 账号id\t\t\t\t\t\t\t|\n| client\t\t| 否\t\t| 客户端标识，可不填，代表是一个匿名应用\t\t\t\t|\n| deviceId\t\t| 否\t\t| 客户端设备 id \t\t\t\t\t\t\t\t|\n| timestamp\t\t| 是\t\t| 当前时间戳，13位\t\t\t\t\t\t\t\t\t|\n| nonce\t\t\t| 是\t\t| 随机字符串\t\t\t\t\t\t\t\t\t\t|\n| sign\t\t\t| 是\t\t| 签名，生成算法示例：`md5( client={client值}&deviceId={设备id}&msgType={signout}&nonce={随机字符串}&loginId={loginId}&timestamp={13位时间戳}&key={secretkey秘钥} )`\t\t\t\t\t|\n\n返回值示例：\n``` js\n{\n  \"code\": 200,   // 返回 200=成功，500=失败\n  \"msg\": \"ok\",\n  \"data\": null\n}\n```\n\n\n### 2、sso-client 端内置消息处理器\n\n#### 2.1、logoutCall（单点注销回调）\n\n作用：接收来自 sso-server 的单点注销回调通知 \n\n``` url\nhttp://{sso-client主机地址}/sso/pushC\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| msgType\t\t| 是\t\t| 消息类型，此处填 `logoutCall`\t\t\t\t\t\t\t|\n| loginId\t\t| 是\t\t| 账号id\t\t\t\t\t\t\t|\n| deviceId\t\t| 否\t\t| 客户端设备 id \t\t\t\t\t\t\t\t|\n| timestamp\t\t| 是\t\t| 当前时间戳，13位\t\t\t\t\t\t\t\t\t|\n| nonce\t\t\t| 是\t\t| 随机字符串\t\t\t\t\t\t\t\t\t\t|\n| sign\t\t\t| 是\t\t| 签名，生成算法示例：`md5( deviceId={设备id}&msgType={logoutCall}&nonce={随机字符串}&loginId={loginId}&timestamp={13位时间戳}&key={secretkey秘钥} )`\t\t\t\t\t|\n\n返回值示例：\n``` js\n{\n  \"code\": 200,   // 返回 200=成功，500=失败\n  \"msg\": \"单点注销回调成功\",\n  \"data\": null\n}\n```\n\n\n### 3、认证中心自定义消息处理器\n\n当然你也可以通过自定义消息处理器的方式，来扩展消息推送能力，这将非常有助于你完成一些应用与认证中心的自定义数据交互。\n\n假设我们现在有如下需求：在 sso-client 获取 sso-server 端指定账号 id 的昵称、头像等信息，即：用户资料的拉取。\n\n首先，我们需要在 sso-server 实现一个消息处理器：\n\n``` java\n@RestController\npublic class SsoServerController {\n\n\t// 配置SSO相关参数 \n\t@Autowired\n\tprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\n\t\t// 添加消息处理器：userinfo (获取用户资料) （用于为 client 端开放拉取数据的接口）\n\t\tssoServerTemplate.messageHolder.addHandle(\"userinfo\", (ssoTemplate, message) -> {\n\t\t\tSystem.out.println(\"收到消息：\" + message);\n\n\t\t\t// 自定义返回结果（模拟）\n\t\t\treturn SaResult.ok()\n\t\t\t\t\t.set(\"id\", message.get(\"loginId\"))\n\t\t\t\t\t.set(\"name\", \"LinXiaoYu\")\n\t\t\t\t\t.set(\"sex\", \"女\")\n\t\t\t\t\t.set(\"age\", 18);\n\t\t});\n\n\t}\n\n}\n```\n\n\n### 4、应用端调用消息推送接口获取数据\n\n首先保证在配置文件里要配置上消息推送的具体地址\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# sa-token配置 \nsa-token:\n    # sso-client 相关配置\n    sso-client:\n        # 应用标识\n        client: sso-client3\n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # sso-server 端推送消息地址\n\t\t# 配置 server-url 后，框架可自动计算对应的 push-url 地址，也可以单独配置 push-url 地址，两者选其一即可\n        # push-url: http://sa-sso-server.com:9000/sso/pushS\n        # API 接口调用秘钥\n        secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# sso-client 相关配置\n# 应用标识\nsa-token.sso-client.client=sso-client3\n# sso-server 端主机地址\nsa-token.sso-client.server-url=http://sa-sso-server.com:9000\n# sso-server 端推送消息地址\n# 配置 server-url 后，框架可自动计算对应的 push-url 地址，也可以单独配置 push-url 地址，两者选其一即可\nsa-token.sso-client.push-url=http://sa-sso-server.com:9000/sso/pushS\n# API 接口调用秘钥\nsa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n然后在需要拉取资料的地方：\n\n``` java\n// 查询我的账号信息：sso-client 前端 -> sso-client 后端 -> sso-server 后端\n@RequestMapping(\"/sso/myInfo\")\npublic Object myInfo() {\n\t// 如果尚未登录\n\tif( ! StpUtil.isLogin()) {\n\t\treturn \"尚未登录，无法获取\";\n\t}\n\n\t// 获取本地 loginId\n\tObject loginId = StpUtil.getLoginId();\n\n\t// 构建消息对象 \n\tSaSsoMessage message = new SaSsoMessage();\n\tmessage.setType(\"userinfo\");\n\tmessage.set(\"loginId\", loginId);\n\t\n\t// 推送至 sso-server，并接收响应数据 \n\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n\t// 返回给前端\n\treturn result;\n}\n```\n\n"
  },
  {
    "path": "sa-token-doc/sso/readme.md",
    "content": "# Sa-Token-SSO 单点登录模块 \n\n<p><a class=\"case-btn case-btn-video\" href=\"https://www.bilibili.com/video/BV1NF1FBpEe6/\" target=\"_blank\">\n\t观看 SSO 模块视频讲解（B站：王清江唷）\n</a></p>\n\n凡是稍微上点规模的系统，统一认证中心都是绕不过去的槛。而单点登录——便是我们搭建统一认证中心的关键。\n\n--- \n\n### 什么是单点登录？解决什么问题？\n\n举个场景，假设我们的系统被切割为N个部分：商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次，那么用户将会疯掉，\n为了优化用户体验，我们急需一套机制将这N个系统的认证授权互通共享，让用户在一个系统登录之后，便可以畅通无阻的访问其它所有系统。 \n\n单点登录——就是为了解决这个问题而生！\n\n简而言之，单点登录可以做到： **`在多个互相信任的系统中，用户只需登录一次，就可以访问所有系统。`**\n\n\n### 架构选型\nSa-Token-SSO 由简入难划分为三种模式，解决不同架构下的 SSO 接入问题：\n\n| 系统架构\t\t\t\t\t| 采用模式\t| 简介\t\t\t\t\t|  文档链接\t|\n| :--------\t\t\t\t\t| :--------\t| :--------\t\t\t\t| :--------\t|\n| 前端同域 + 后端同 Redis\t\t| 模式一\t\t| 共享 Cookie 同步会话\t| [文档](/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client)\t|\n| 前端不同域 + 后端同 Redis\t| 模式二\t\t| URL重定向传播会话 \t\t| [文档](/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client)\t|\n| 前端不同域 + 后端不同 Redis\t| 模式三\t\t| Http请求获取会话\t\t| [文档](/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client)\t|\n\n\n1. 前端同域：就是指多个系统可以部署在同一个主域名之下，比如：`c1.domain.com`、`c2.domain.com`、`c3.domain.com`。\n2. 后端同Redis：就是指多个系统可以连接同一个Redis。PS：这里并不需要把所有项目的数据都放在同一个Redis中，Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案，详情戳： <a href=\"#/plugin/alone-redis\" target=\"_blank\">Alone独立Redis插件</a>。\n3. 如果既无法做到前端同域，也无法做到后端同Redis，那么只能走模式三，Http请求获取会话（Sa-Token对SSO提供了完整的封装，你只需要按照示例从文档上复制几段代码便可以轻松集成）。\n\n<img src=\"/big-file/doc/sso/sa-token-sso--white.png\" alt=\"sa-token-jss\">\n\n\n### Sa-Token-SSO 特性\n1. API 简单易用，文档介绍详细，且提供直接可用的集成示例。\n2. 支持三种模式，不论是否跨域、是否共享Redis、是否前后端分离，都可以完美解决。\n3. 安全性高：内置域名校验、Ticket校验、秘钥校验等，杜绝`Ticket劫持`、`Token窃取`等常见攻击手段（文档讲述攻击原理和防御手段）。\n4. 不丢参数：笔者曾试验多个单点登录框架，均有参数丢失的情况，比如重定向之前是：`http://a.com?id=1&name=2`，登录成功之后就变成了：`http://a.com?id=1`，Sa-Token-SSO内有专门的算法保证了参数不丢失，登录成功之后原路返回页面。\n5. 无缝集成：由于Sa-Token本身就是一个权限认证框架，因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题，让你不再到处搜索：xxx单点登录与xxx权限认证如何整合……\n6. 高可定制：Sa-Token-SSO模块对代码架构侵入性极低，结合Sa-Token本身的路由拦截特性，你可以非常轻松的定制化开发。\n\n\n### 学习注意点\n1. sa-token-sso 虽然是个单独的插件，但其本质仍是对 Sa-Token 本身各个功能的组合使用，所以先熟练掌握 Sa-Token 可有效降低 SSO 章节的学习压力。\n2. 相比单体系统的会话管理，SSO 登录与注销的整体链路较长，出现 bug 时调试步骤也更复杂，因此建议先通过 demo 打通各个技术细节，再正式集成到项目中。\n3. 文档对 跨Redis、跨域、前后端分离 等常见场景提供直接可用的示例，但真实项目往往是多种特殊场景交叉组合存在，每个项目各不相同。\n所以文档无法依次列出所有技术点交叉组合的 demo 示例，文档会更注重解释清楚每一种特殊场景的特殊点所在，以及其解决原理，\n所以推荐大家细心阅读相关段落，以便在真实项目中可以做到灵活组合、举一反三。\n\n"
  },
  {
    "path": "sa-token-doc/sso/signout.md",
    "content": "# 单点注销\n\nSa-Token SSO 提供多种注销模式：\n\n从注销范围上可以分为：\n\n- 单端注销：会话只在当前应用注销，其它应用和认证中心不受影响。\n- 全端注销：一处注销，全端下线。也即：单点注销。\n- 单浏览器注销：该账号的只在当前浏览器登录的应用注销，其它浏览器/设备不受影响。\n\n从注销方式上可以分为：\n- ajax 无刷单点注销：调用指定的 RestAPI 接口完成注销。\n- 跳页面注销：跳转到指定接口进行注销，注销完成后原路返回或跳转到指定页面。\n\n--- \n\n### 1、单端注销\n\n在后端添加接口：\n``` java\n// 当前应用独自注销 (不退出其它应用)\n@RequestMapping(\"/sso/logoutByAlone\")\npublic Object logoutByAlone() {\n\tStpUtil.logout();\n\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n}\n```\n\n在前端或跳转或 ajax 异步调用此接口即可。\n\n如果是跳转可指定 back 参数，代表注销成功后跳转的地址，例如：`http://sso-client.com/sso/logoutByAlone?back=https://sa-token.cc` \n\n\n### 2、全端注销\n\n此处先简单看一下 Sa-Token SSO 的单点注销链路过程：\n\n1. sso-client 的前端向 sso-client 的后端发起单点注销请求。(调用 `http://{sso-client}/sso/logout`)\n2. sso-client 的后端向 sso-server 的后端发送单点注销请求。(调用 `http://{sso-server}/sso/pushS?msgType=signout`)\n3. sso-server 端遍历 client 列表，逐个推送消息通知 sso-client 端下线。(`http://{sso-client}/sso/pushC?msgType=logoutCall`)\n4. sso-server 端注销下线。\n5. sso-server 后端响应 sso-client 后端：注销完成。\n6. sso-client 后端响应 sso-client 前端：注销完成。\n7. 整体完成。\n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/sso/g3--sso3-logout.gif\">加载动态演示图</button>\n\n\n这些逻辑 Sa-Token 内部已经封装完毕，你只需按照文档步骤集成即可。以模式三 demo 为例：\n\n#### 2.1、更改注销方案\n\n单点注销是 Sa-Token SSO 内部已封装的接口，无需手动再添加，只需要在前端调用即可。\n\n``` java\n// SSO-Client端：首页\n@RequestMapping(\"/\")\npublic String index() {\n\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>\" +\n\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\"<p> \" +\n\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> \" +\n\t\t\t\"</p>\";\n\treturn str;\n}\n```\n\n重点在第 9 行。\n\n#### 2.2、启动测试 \n重启项目，依次登录三个 client：\n- [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/)\n- [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/)\n- [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/)\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-type3-client-index.png\" alt=\"sso-type3-client-index.png\" />\n\n在任意一个 client 里，点击 **`[注销]`** 按钮，即可单点注销成功（打开另外两个client，刷新一下页面，登录态丢失）。\n\n<!-- ![sso-type3-slo.png](https://oss.dev33.cn/sa-token/doc/sso/sso-type3-slo.png 's-w-sh') -->\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-type3-slo-index.png\" alt=\"sso-type3-slo-index.png\" />\n\nPS：这里我们为了方便演示，使用的是超链接跳页面的形式，正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。\n\n例如，我们使用 [Apifox 接口测试工具](https://www.apifox.cn/) 可以做到同样的效果：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-slo-apifox.png\" alt=\"sso-slo-apifox.png\" />\n\n\n\n### 3、单浏览器注销\n\n单浏览器注销的前提是在登录时按照 `deviceId` 设备ID 参数为登录进行分组，这样在发起注销时即可格局设备ID参数做到单浏览器注销功能。\n\n#### 3.1、sso-server 端加上设备ID参数登录\n\n首先在 sso-server 的登录方法内，加上 deviceId 参数，例如：\n\n``` java\n@RestController\npublic class SsoServerController {\n\t\n\t// 其它代码，非重点，省略展示...\n\t\n\t// 配置SSO相关参数 \n\t@Autowired\n\tprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\t\t// 配置：登录处理函数 \n\t\tssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {\n\t\t\t// 此处仅做模拟登录，真实环境应该查询数据库进行登录\n\t\t\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\tString deviceId = SaHolder.getRequest().getParam(\"deviceId\", SaFoxUtil.getRandomString(32));\n\t\t\t\tStpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId));\n\t\t\t\treturn SaResult.ok(\"登录成功！\").setData(StpUtil.getTokenValue());\n\t\t\t}\n\t\t\treturn SaResult.error(\"登录失败！\");\n\t\t};\n\n\t}\n}\n```\n\n如上代码，在登录时获取前端提交的 deviceId 参数，如果前端没有提交则随机生成一个。\n\n\n#### 3.2、sso-client 端发起注销时指定单设备注销参数\n\n然后在 sso-client 发起单点注销时，加上 `singleDeviceIdLogout=true` 参数，代表按照设备 id 进行分组注销，非本设备id的会话不参与注销行为：\n\n``` java\n// SSO-Client端：首页\n@RequestMapping(\"/\")\npublic String index() {\n\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>\" +\n\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\"<p> \" +\n\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - \" +\n\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> \" +\n\t\t\t\"</p>\";\n\treturn str;\n}\n```\n\n重点在第 9 行。\n\n\n> [!WARNING| label:测试注意点] \n> 在进行测试时，同时将一个浏览器双击打开两次，是不算 “不同浏览器” 的，虽然你打开了两个浏览器窗口，但是这两个浏览器的会话数据是互通的。\n> \n> 必须打开两个不同的浏览器来测试，或者按快捷键 `ctrl + shift + N` 打开隐私模式，才可以做到会话相互隔离。\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-apidoc.md",
    "content": "# SSO-Server 认证中心开放接口\n\n--- \n\n## 一、对接方式说明\n\n在前一章节，我们成功搭建了 SSO-Server 端。SSO-Server 可以为各个子系统提供中央认证服务。\n\n在默认代码架构下，SSO-Server 将提供一套标准 HTTP 接口对外开放。要对接 SSO-Server，有两种方式：\n- SDK 方式：在你的 SSO-Client 端，也引入 Sa-Token SSO 框架，通过框架提供的 API 方法完成对接。\n\t<!-- - 适用于：java + Sa-Token 项目，较为简单、直接。 -->\n- NoSDK 方式：在你的 SSO-Client 端，不引入 Sa-Token SSO 框架，通过工具库调用 Http 接口的方式完成对接。\n\t<!-- - 适用于：非java、非 Sa-Token 项目，稍微复杂一些，但适用性更广。 -->\n\n如果你的 SSO-Client 端是 java 项目，且支持引入 sa-token-sso 框架，我们强烈推荐你使用 SDK 方式对接。**你可以直接跳过本章**，开始 [SSO模式一 共享Cookie同步会话](/sso/sso-type1) 的学习。\n\n如果你的 SSO-Client 端是 非java 项目，或不支持引入 sa-token-sso 框架，那么你可以使用 NoSDK 方式进行对接。\n在之后的 [SSO整合 - NoSdk 模式对接](/sso/sso-nosdk) 章节我们会详细介绍对接步骤，下面的 API 文档将给你的对接步骤做一份参考。\n\n\n## 二、STS 协议\n\nSa-Token SSO 模块的开放接口标准为 STS 协议。\n\nSTS 协议并非一套公共授权协议，而是 sa-token-sso 框架本身内化、抽离出的一套授权协议标准。\n\n- 一般的公共协议遵循的路线是：发现需求 -> 先为解决方案定义一套协议 -> 再进行框架实现。\n- 而 Sa-Token SSO 的路线为：发现需求 -> 先进行框架实现 -> 再基于框架实现定义一套协议，将解决方案进行标准化。\n\n定义 STS 协议将有助于让 Sa-Token SSO 模块进行标准化，也为日后实现多语言 SDK 提供基础支持。\n\n目前 STS 协议规定了应用进行 单点登录、单点注销、消息推送 等动作的标准流程。\n可解决：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、Sa-Token 项目、非 Sa-Token 项目、java 项目、非 java 项目 等架构下的 SSO 认证需求。\n\n下面两节将介绍 STS 协议在 SSO-Server 端和 SSO-Client 端开放的接口标准。\n\n\n\n## 三、SSO-Server 认证中心接口 \n\n\n### 1、单点登录授权地址\n``` url\nhttp://{host}:{port}/sso/auth\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| redirect\t\t| 否\t\t| 登录成功后的重定向地址，一般填写 location.href（从哪来回哪去），如不填，则跳转至 home-route\t\t\t\t\t\t|\n| mode\t\t\t| 否\t\t| 授权模式，取值 [simple, ticket]，simple=登录后直接重定向，ticket=带着ticket参数重定向，默认值为ticket\t\t\t|\n| client\t\t| 否\t\t| 客户端标识，可不填，代表是一个匿名应用，若填写了，则校验 ticket 时也必须是这个 client 才可以校验成功\t\t\t|\n\n访问接口后有两种情况：\n- 情况一：当前会话在 SSO 认证中心未登录，会进入登录页开始登录。\n- 情况二：当前会话在 SSO 认证中心已登录，会被重定向至 `redirect` 地址，并携带 `ticket` 参数。\n\nTicket 码具有以下特点：\n1. 每次授权产生的 `ticket` 码都不一样。\n2. `ticket` 码用完即废，不能二次使用。\n3. 一个 `ticket` 的有效期默认为五分钟，超时自动作废。\n4. 每次授权产生新 `ticket` 码，会导致旧 `ticket` 码立即作废，即使旧 `ticket` 码尚未使用。\n\n\n### 2、RestAPI 登录接口\n``` url\nhttp://{host}:{port}/sso/doLogin\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| name\t\t\t| 是\t\t| 用户名  |\n| pwd\t\t\t| 是\t\t| 密码\t |\n\n此接口属于 RestAPI (使用ajax访问)，会进入后端配置的 `ssoServerTemplate.strategy.doLoginHandle` 函数中，此函数的返回值即是此接口的响应值。\n\n另外需要注意：此接口并非只能携带 name、pwd 参数，因为你可以在方法里通过 `SaHolder.getRequest().getParam(\"xxx\")` 来获取前端提交的其它参数。 \n\n\n### 3、单点注销接口\n``` url\nhttp://{host}:{port}/sso/signout\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t|\n| back\t\t\t| 否\t\t| 注销成功后的重定向地址，一般填写 location.href（从哪来回哪去），也可以填写 self 字符串，含义同上\t\t\t|\n\n\n### 4、消息推送接口\n\n``` url\nhttp://{host}:{port}/sso/pushS\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| client\t\t| 否\t\t| 客户端标识，可不填，代表是一个匿名应用\t\t\t\t|\n| timestamp\t\t| 是\t\t| 当前时间戳，13位\t\t\t\t\t\t\t\t\t|\n| msgType\t\t| 是\t\t| 消息类型\t\t\t\t\t\t\t\t\t\t|\n| nonce\t\t\t| 是\t\t| 随机字符串\t\t\t\t\t\t\t\t\t\t|\n| sign\t\t\t| 是\t\t| 签名，生成算法：`md5( client={client值}&msgType={消息类型}&nonce={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )`\t\t\t\t\t|\n\n此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。\n\n返回值示例：\n\n- 推送成功时：\n\n``` js\n{\n    \"code\": 200,\n    \"msg\": \"ok\",\n    \"data\": \"10001\",\t// 返回的数据 \n}\n```\n\n- 推送失败时：\n\n``` js\n{\n    \"code\": 500,    // 200表示请求成功，非200标识请求失败\n    \"msg\": \"签名无效：xxx\",    // 失败原因 \n    \"data\": null\n}\n```\n\n- 也有可能消息推送成功了，但是处理消息失败，例如校验 ticket 时：\n\n``` js\n{\n    \"code\": 500,\n    \"msg\": \"无效ticket：vESj0MtqrtSoucz4DDHJnsqU3u7AKFzbj0KH57EfJvuhkX1uAH23DuNrMYSjTnEq\",\n    \"data\": null\n}\n```\n\n详细可参考：[消息推送机制](/sso/message-push)\n\n\n\n<br>\n\n<!-- SSO 认证中心只有这四个接口，接下来让我一起来看一下 Client 端的对接流程：[SSO模式一 共享Cookie同步会话](/sso/sso-type1) -->\n\n\n\n---\n\n## 四、SSO-Client 应用端开放接口 \n\n### 1、登录地址\n``` url\nhttp://{host}:{port}/sso/login\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| back\t\t\t| 是\t\t| 登录成功后的重定向地址，一般填写 location.href（从哪来回哪去）\t\t\t|\n| ticket\t\t| 否\t\t| 授权 ticket 码\t\t\t|\n\n此接口有两种访问方式：\n- 方式一：我们需要登录操作，所以带着 back 参数主动访问此接口，框架会拼接好参数后再次将用户重定向至认证中心。\n- 方式二：用户在认证中心登录成功后，带着 ticket 参数重定向而来，此为框架自动处理的逻辑，开发者无需关心。\n\n\n### 2、注销地址\n``` url\nhttp://{host}:{port}/sso/logout\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| back\t\t\t| 否\t\t| 注销成功后的重定向地址，一般填写 location.href（从哪来回哪去），也可以填写 self 字符串，含义同上\t\t\t|\n\n此接口有两种访问方式：\n- 方式一：直接 `location.href` 网页跳转，此时可携带 back 参数。\n- 方式二：使用 Ajax 异步调用（此方式不可携带 back 参数，但是需要提交会话 Token ），注销成功将返回以下内容：\n\n``` js\n{\n    \"code\": 200,    // 200表示请求成功，非200标识请求失败\n    \"msg\": \"单点注销成功\",\n    \"data\": null\n}\n```\n\n\n### 3、单点注销回调接口\n此接口仅配置 `(reg-logout-call=true)` 时打开，且为框架回调，开发者无需关心\n\n``` url\nhttp://{host}:{port}/sso/logoutCall\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t|\n| loginId\t\t| 是\t\t| 要注销的账号 id\t\t\t \t\t\t\t\t|\n| timestamp\t\t| 是\t\t| 当前时间戳，13位\t\t\t\t\t\t\t\t|\n| nonce\t\t\t| 是\t\t| 随机字符串\t\t\t\t\t\t\t\t\t\t|\n| sign\t\t\t| 是\t\t| 签名，生成算法：`md5( loginId={账号id}&nonce={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )` |\n| client\t\t| 否\t\t| 客户端标识，如果你在登录时向 sso-server 端传递了 client 值，那么在此处 sso-server 也会给你回传过来，否则此参数无值。如果此参数有值，则此参数也要参与签名，放在 loginId 参数前面（字典顺序）\t\t|\n| autoLogout\t| 否\t\t| 是否为“登录client超过最大数量”引起的自动注销（true=超限系统自动注销，false=用户主动发起注销）。如果此参数有值，则此参数也要参与签名，放在 client 参数前面（字典顺序）\t\t|\n\n\n返回数据：\n\n``` js\n{\n    \"code\": 200,    // 200表示请求成功，非200标识请求失败\n    \"msg\": \"单点注销回调成功\",\n    \"data\": null\n}\n```\n\n\n\n### 4、消息推送接口\n\n``` url\nhttp://{host}:{port}/sso/pushC\n```\n\n接收参数：\n\n| 参数\t\t\t| 是否必填\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t|\n| timestamp\t\t| 是\t\t| 当前时间戳，13位\t\t\t\t\t\t\t\t\t|\n| msgType\t\t| 是\t\t| 消息类型\t\t\t\t\t\t\t\t\t\t|\n| nonce\t\t\t| 是\t\t| 随机字符串\t\t\t\t\t\t\t\t\t\t|\n| sign\t\t\t| 是\t\t| 签名，生成算法：`md5( msgType={消息类型}&nonce={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )`\t\t\t\t\t|\n\n此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。\n\n返回值示例：\n\n- 推送成功时：\n\n``` js\n{\n    \"code\": 200,\n    \"msg\": \"ok\",\n    \"data\": \"10001\",\t// 返回的数据 \n}\n```\n\n- 推送失败时：\n\n``` js\n{\n    \"code\": 500,    // 200表示请求成功，非200标识请求失败\n    \"msg\": \"签名无效：xxx\",    // 失败原因 \n    \"data\": null\n}\n```\n\n详细可参考：[消息推送机制](/sso/message-push)\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-check-domain.md",
    "content": "# SSO整合-配置域名校验\n\n--- \n\n### 1、Ticket劫持攻击\n在前面章节的 SSO-Server 示例中，配置项 `sa-token.sso-server.clients.sso-client3.allow-url=*` 意为该 client 所有允许的授权地址，不在此配置项中的 URL 将无法单点登录成功。\n\n为了方便测试，上述代码将其配置为`*`，但是，<font color=\"#FF0000\" >在生产环境中，此配置项绝对不能配置为 * </font>，否则会有被 Ticket 劫持的风险。\n\n假设攻击者根据模仿我们的授权地址，巧妙的构造一个URL：\n\n> [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/)\n\n当不知情的小红被诱导访问了这个URL时，它将被重定向至百度首页：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-ticket-jc.png\" alt=\"sso-ticket-jc\">\n\n可以看到，代表着用户身份的 Ticket 码也显现到了 URL 之中，借此漏洞，攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器，伪造小红身份登录网站\n\n### 2、防范方法\n\n造成此漏洞的直接原因就是SSO-Server认证中心没有对 `redirect地址` 进行任何的限制，防范的方法也很简单，就是对`redirect参数`进行校验，如果其不在指定的URL列表中时，拒绝下放ticket \n\n我们将其配置为一个具体的URL：\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\tsso-server: \n\t\tclients:\n\t\t\tsso-client3:\n\t\t\t\t# 配置允许单点登录的 url \n\t\t\t\tallow-url: http://sa-sso-client1.com:9003/sso/login\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 配置允许单点登录的 url \nsa-token.sso-server.clients.so-client3.allow-url=http://sa-sso-client1.com:9003/sso/login\n```\n<!---------------------------- tabs:end ---------------------------->\n\n再次访问上述链接：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-feifa-rf.png\" alt=\"sso-feifa-rf\">\n\n域名没有通过校验，拒绝授权！\n\n### 3、配置安全性参考表\n\n| 配置方式\t\t| 举例\t\t\t\t\t\t\t\t\t\t| 安全性\t\t\t\t\t\t\t\t|  建议\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t\t\t\t\t\t\t\t\t| :--------\t\t\t\t\t\t\t| :--------\t\t\t\t\t\t\t\t|\n| 配置为*\t\t| `*`\t\t\t\t\t\t\t\t\t\t| <font color=\"#F00\" >低</font>\t\t| **<font color=\"#F00\" >禁止在生产环境下使用</font>**\t|\n| 配置到域名\t| `http://sa-sso-client1.com/*`\t\t\t\t\t| <font color=\"#F70\" >中</font>\t\t| <font color=\"#F70\" >不建议在生产环境下使用</font>\t|\n| 配置到详细地址| `http://sa-sso-client1.com:9001/sso/login`\t| <font color=\"#080\" >高</font>\t\t| <font color=\"#080\" >可以在生产环境下使用</font>\t|\n\n\n### 4、疑问：为什么不直接回传 Token，而是先回传 Ticket，再用 Ticket 去查询对应的账号id？\nToken 作为长时间有效的会话凭证，在任何时候都不应该直接暴露在 URL 之中（虽然 Token 直接的暴露本身不会造成安全漏洞，但会为很多漏洞提供可乘之机）\n\n为了不让系统安全处于亚健康状态，Sa-Token-SSO 选择先回传 Ticket，再由 Ticket 获取账号id，且 Ticket 一次性用完即废，提高安全性。\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-custom-api.md",
    "content": "# SSO整合-自定义 API 路由 \n\n---\n\n### 方式一：修改全局变量\n\n在之前的章节中，我们演示了如何搭建一个SSO认证中心：\n``` java\n/**\n * Sa-Token-SSO Server端 Controller \n */\n@RestController\npublic class SsoServerController {\n\n\t// SSO-Server端：处理所有SSO相关请求 \n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoServerProcessor.instance.dister();\n\t}\n\t\n\t// ... 其它代码\n\t\n}\n```\n\n这种写法集成简单但却不够灵活。例如认证中心地址只能是：`http://{host}:{port}/sso/auth`，如果我们想要自定义其API地址，应该怎么做呢？\n\n打开SSO模块相关源码，有关 API 的设计都定义在：\n[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)\n中，我们可以对其进行二次修改。\n\n例如，我们可以在 Main 方法启动类或者 SSO 配置方法中修改变量值：\n``` java\n// 配置SSO相关参数 \n@Autowired\nprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\t// 自定义API地址\n\tSaSsoServerProcessor.instance.ssoServerTemplate.apiName.ssoAuth = \"/sso/auth2\";\n\t// ... \n\t\n}\n```\n\n启动项目，统一认证地址就被我们修改成了：`http://{host}:{port}/sso/auth2`\n\n\n### 方式二：拆分路由入口\n根据上述路由入口：`@RequestMapping(\"/sso/*\")`，我们给它起一个合适的名字 —— 聚合式路由。\n\n与之对应的，我们可以将其修改为拆分式路由：\n\n``` java\n/**\n * Sa-Token-SSO Server端 Controller \n */\n@RestController\npublic class SsoServerController {\n\n\t// SSO-Server：统一认证地址 \n\t@RequestMapping(\"/sso/auth\")\n\tpublic Object ssoAuth() {\n\t\treturn SaSsoServerProcessor.instance.ssoAuth();\n\t}\n\n\t// SSO-Server：RestAPI 登录接口 \n\t@RequestMapping(\"/sso/doLogin\")\n\tpublic Object ssoDoLogin() {\n\t\treturn SaSsoServerProcessor.instance.ssoDoLogin();\n\t}\n\n\t// SSO-Server：接收推送消息地址\n\t@RequestMapping(\"/sso/pushS\")\n\tpublic Object ssoPushS() {\n\t\treturn SaSsoServerProcessor.instance.ssoPushS();\n\t}\n\n\t// SSO-Server：单点注销 \n\t@RequestMapping(\"/sso/signout\")\n\tpublic Object ssoSignout() {\n\t\treturn SaSsoServerProcessor.instance.ssoSignout();\n\t}\n\t\n\t// ... 其它方法 \n\t\n}\n```\n\n拆分式路由 与 聚合式路由 在功能上完全等价，且提供了更为细致的路由管控。\n\n\n### SSO-Client 端拆分路由入口示例\n\n``` java\n/**\n * Sa-Token-SSO Client端 Controller \n */\n@RestController\npublic class SsoClientController {\n\n\t// SSO-Client：登录地址\n\t@RequestMapping(\"/sso/login\")\n\tpublic Object ssoLogin() {\n\t\treturn SaSsoClientProcessor.instance.ssoLogin();\n\t}\n\n\t// SSO-Client：单点注销地址\n\t@RequestMapping(\"/sso/logout\")\n\tpublic Object ssoLogout() {\n\t\treturn SaSsoClientProcessor.instance.ssoLogout();\n\t}\n\n\t// SSO-Client：单点注销回调\n\t@RequestMapping(\"/sso/logoutCall\")\n\tpublic Object ssoLogoutCall() {\n\t\treturn SaSsoClientProcessor.instance.ssoLogoutCall();\n\t}\n\n\t// SSO-Client：接收消息推送地址\n\t@RequestMapping(\"/sso/ssoPushC\")\n\tpublic Object ssoPushC() {\n\t\treturn SaSsoClientProcessor.instance.ssoPushC();\n\t}\n\n\t// ... 其它方法 \n\t\n}\n```"
  },
  {
    "path": "sa-token-doc/sso/sso-custom-login.md",
    "content": "# SSO整合-定制化登录页面\n\n---\n\n### 1、何时引导用户去登录？\n\n#### 方案一：前端按钮跳转 \n前端页面准备一个 **`[登录]`** 按钮，当用户点击按钮时，跳转到登录接口 \n``` html\n<a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a>\n```\n\n#### 方案二：后端拦截重定向\n在后端注册全局过滤器（或拦截器、或全局异常处理），拦截需要登录后才能访问的页面资源，将未登录的访问重定向至登录接口 \n``` java\n/**\n * Sa-Token 配置类 \n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t/** 注册 [Sa-Token全局过滤器] */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t.addInclude(\"/**\")\n        \t\t.addExclude(\"/sso/*\", \"/favicon.ico\")\n        \t\t.setAuth(obj -> {\n        \t\t\tif(StpUtil.isLogin() == false) {\n        \t\t\t\tString back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());\n        \t\t\t\tSaHolder.getResponse().redirect(\"/sso/login?back=\" + SaFoxUtil.encodeUrl(back));\n        \t\t\t\tSaRouter.back();\n        \t\t\t}\n        \t\t})\n        \t\t;\n    }\n}\n```\n\n#### 方案三：后端拦截 + 前端跳转 \n首先，后端仍需要提供拦截，但是不直接引导用户重定向，而是返回未登录的提示信息 \n```  java\n/**\n * Sa-Token 配置类 \n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t/** 注册 [Sa-Token全局过滤器] */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n        \t\t.addInclude(\"/**\")\n        \t\t.addExclude(\"/sso/*\", \"/favicon.ico\")\n        \t\t.setAuth(obj -> {\n        \t\t\tif(StpUtil.isLogin() == false) {\n        \t\t\t\t// 与前端约定好，code=401时代表会话未登录 \n        \t\t\t\tSaRouter.back(SaResult.ok().setCode(401));\n        \t\t\t}\n        \t\t})\n        \t\t;\n    }\n}\n```\n\n前端接受到返回结果 `code=401` 时，开始跳转至登录接口\n``` js\nif(res.code == 401) {\n\tlocation.href = '/sso/login?back=' + encodeURIComponent(location.href);\n}\n```\n\n这种方案比较适合以 Ajax 访问的 RestAPI 接口重定向 \n\n\n\n\n### 2、如何自定义登录视图？\n\n#### 方式一：在demo示例中直接更改 login.html 页面代码即可 \n\n#### 方式二：在配置中配置登录视图地址 \n\n``` java\n// 配置：未登录时返回的View \nssoServerTemplate.strategy.notLoginView = () -> {\n\treturn new ModelAndView(\"xxx.html\");\n}\n```\n\n\n### 3、如何自定义登录API的接口地址？\n根据需求点选择解决方案：\n\n#### 3.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数？\n``` java\n// 在任意代码处获取前端提交的参数 \nString xxx = SaHolder.getRequest().getParam(\"xxx\");\n```\n\n#### 3.2、想完全自定义一个接口来接受前端登录请求？\n``` java\n// 直接定义一个拦截路由为 `/sso/doLogin` 的接口即可 \n@RequestMapping(\"/sso/doLogin\")\npublic SaResult ss(String name, String pwd) {\n\tSystem.out.println(\"------ 请求进入了自定义的API接口 ---------- \");\n\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\tStpUtil.login(10001);\n\t\treturn SaResult.ok(\"登录成功！\");\n\t}\n\treturn SaResult.error(\"登录失败！\");\n}\n```\n\n#### 3.3、不想使用`/sso/doLogin`这个接口，想自定义一个API地址？\n\n答：直接在前端更改点击按钮时 Ajax 的请求地址即可 \n\n\n### 4、不同 client 不同登录页\n\n如果你的不同应用覆盖的用户群体差异极大，此时你可能想针对不同的应用跳转到不同的登录页，让每个应用的用户在登录时能够看到当前应用的专属信息，怎么做呢？\n\n首先，保证每个 sso-client 端都配置了不同的 client 标识：\n\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token:\n    sso-client:\n        # 当前 client 标识\n        client: sso-client-shop\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 当前 client 标识\nsa-token.sso-client.client=sso-client-shop\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n然后在 `sso-server` 里为每个系统开发不同的登录页，并在 `configSso` 方法里 `notLoginView` 函数中根据 client 值，返回不同的登录视图：\n\n``` java\n// 配置SSO相关参数 \n@Autowired\nprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\t\n\t// 配置：未登录时返回的View \n\tssoServerTemplate.strategy.notLoginView = () -> {\n\n\t\tString client = SaHolder.getRequest().getParam(\"client\");\n\t\tif(\"sso-client-shop\".equals(client)) {\n\t\t\treturn new ModelAndView(\"sa-shop-login.html\");\n\t\t}\n\t\tif(\"sso-client-video\".equals(client)) {\n\t\t\treturn new ModelAndView(\"sa-video-login.html\");\n\t\t}\n\t\t// 更多 ... \n\t\t\n\t\t// 都不匹配，返回一个默认的 \n\t\treturn new ModelAndView(\"sa-login.html\");\n\t};\n\t\n\t// ... \n\t\n}\n```\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-dev.md",
    "content": "# Sa-Token SSO Server端 二次开发用到的所有函数说明 \n\n本篇展示一下 SSO 模块常用的工具类、方法\n\n## Sso-Server 工具类\n\n### Ticket 操作\n``` java\n// 增删改\n\n// 删除 Ticket\nSaSsoServerUtil.deleteTicket(String ticket);\n\n// 根据参数创建一个 ticket 码，并保存\nSaSsoServerUtil.createTicketAndSave(String client, Object loginId, String tokenValue);\n\n// 查\n\n// 查询 ticket ，如果 ticket 无效则返回 null\nSaSsoServerUtil.getTicket(String ticket);\n\n// 查询 ticket 指向的 loginId，如果 ticket 码无效则返回 null\nSaSsoServerUtil.getLoginId(String ticket);\n\n// 查询 ticket 指向的 loginId，并转换为指定类型\nSaSsoServerUtil.getLoginId(String ticket, Class<T> cs);\n\n// 校验\n\n// 校验 Ticket，无效 ticket 会抛出异常\nSaSsoServerUtil.checkTicket(String ticket);\n\n// 校验 Ticket 码，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\nSaSsoServerUtil.checkTicketParamAndDelete(String ticket);\n\n// 校验 Ticket，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\nSaSsoServerUtil.checkTicketParamAndDelete(String ticket, String client);\n\n// ticket 索引\n\n// 查询 指定 client、loginId 其所属的 ticket 值\nSaSsoServerUtil.getTicketValue(String client, Object loginId);\n```\n\n\n\n### Client 信息获取 \n``` java\n// 获取所有 Client\nSaSsoServerUtil.getClients();\n\n// 获取应用信息，无效 client 返回 null\nSaSsoServerUtil.getClient(String client);\n\n// 获取应用信息，无效 client 则抛出异常\nSaSsoServerUtil.getClientNotNull(String client);\n\n// 获取匿名 client 信息\nSaSsoServerUtil.getAnonClient();\n\n// 获取所有需要接收消息推送的 Client\nSaSsoServerUtil.getNeedPushClients();\n```\n\n\n### 重定向 URL 构建与校验\n``` java\n// 构建 URL：sso-server 端向 sso-client 下放 ticket 的地址\nSaSsoServerUtil.buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue);\n\n// 校验重定向 url 合法性\nSaSsoServerUtil.checkRedirectUrl(String client, String url);\n```\n\n\n### 单点注销\n``` java\n// 指定账号单点注销\nSaSsoServerUtil.ssoLogout(Object loginId);\n\n// 指定账号单点注销\nSaSsoServerUtil.ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient);\n```\n\n\n\n\n\n### 消息推送 \n``` java\n// 向指定 Client 推送消息\nSaSsoServerUtil.pushMessage(SaSsoClientModel clientModel, SaSsoMessage message);\n\n// 向指定 client 推送消息，并将返回值转为 SaResult\nSaSsoServerUtil.pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message);\n\n// 向指定 Client 推送消息\nSaSsoServerUtil.pushMessage(String client, SaSsoMessage message);\n\n// 向指定 client 推送消息，并将返回值转为 SaResult\nSaSsoServerUtil.pushMessageAsSaResult(String client, SaSsoMessage message);\n\n// 向所有 Client 推送消息\nSaSsoServerUtil.pushToAllClient(SaSsoMessage message);\n\n// 向所有 Client 推送消息，并忽略掉某个 client\nSaSsoServerUtil.pushToAllClient(SaSsoMessage message, String ignoreClient);\n```\n\n\n详情请参考源码：[码云：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)\n\n\n\n## Sso-Client 工具类\n\n\n### 构建交互地址 \n``` java\n// 构建URL：Server端 单点登录授权地址 \nSaSsoClientUtil.buildServerAuthUrl(String clientLoginUrl, String back);\n```\n\n\n### 消息推送 \n\n``` java\n// 向 sso-server 推送消息\nSaSsoClientUtil.pushMessage(SaSsoMessage message);\n\n// 向 sso-server 推送消息，并将返回值转为 SaResult\nSaSsoClientUtil.pushMessageAsSaResult(SaSsoMessage message);\n\n// 构建消息：校验 ticket\nSaSsoClientUtil.buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl);\n\n// 构建消息：单点注销\nSaSsoClientUtil.buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter);\n```\n\n详情请参考源码：[码云：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)\n\n\n\n\n## Sso-Server 所有可重写策略 \n\n``` java\n// 发送 Http 请求的处理函数\nSaSsoServerProcessor.instance.ssoServerTemplate.strategy.sendRequest = url -> {\n\t// ...\n}\n\n// 使用异步模式执行一个任务\nSaSsoServerProcessor.instance.ssoServerTemplate.strategy.asyncRun = fun -> {\n\t// ...\n}\n\n// 未登录时返回的 View\nSaSsoServerProcessor.instance.ssoServerTemplate.strategy.notLoginView = () -> {\n\t// ...\n}\n\n// SSO-Server端：登录函数\nSaSsoServerProcessor.instance.ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {\n\t// ...\n}\n\n//SSO-Server端：在授权重定向之前的通知\nSaSsoServerProcessor.instance.ssoServerTemplate.strategy.jumpToRedirectUrlNotice = (redirectUrl) -> {\n\t// ...\n}\n\n// SSO-Server端：在校验 ticket 后，给 sso-client 端追加返回信息的函数\nSaSsoServerProcessor.instance.ssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> {\n\t// ...\n}\n```\n\n\n\n## Sso-Client 所有可重写策略\n\n``` java\n// 发送 Http 请求的处理函数\nSaSsoClientProcessor.instance.ssoClientTemplate.strategy.sendRequest = url -> {\n\t// ...\n}\n\n// 自定义校验 ticket 返回值的处理逻辑 （每次从认证中心获取校验 ticket 的结果后调用）\nSaSsoClientProcessor.instance.ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> {\n\t// ...\n}\n\n// 转换：认证中心 centerId > 本地 loginId\nSaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> {\n\t// ...\n}\n\n// 转换：本地 loginId > 认证中心 centerId\nSaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> {\n\t// ...\n}\n```\n"
  },
  {
    "path": "sa-token-doc/sso/sso-diff-key.md",
    "content": "# 不同 SSO Client 配置不同秘钥\n\n在校验 ticket、单点注销等操作发起的 http 调用时，需要配置秘钥参数，像这样：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n    sign:\n        # API 接口调用秘钥\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 接口调用秘钥 \nsa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n如果 SSO Client 端和 SSO Server 端配置的秘钥不同，则无法调通请求，显示无效签名：\n``` js\n{\n  \"code\": 500,\n  \"msg\": \"无效签名：9f1b453817bfeac56d2f772a66c01eb2\",\n  \"data\": null\n}\n```\n\n如果你有多个 SSO Client，你可能想让每个应用配置不同的秘钥，让它们彼此之间不能互相“冒充”，怎么做呢？\n\n### 1、首先在 SSO Client 端，你需要配置上不同的 Client 标识参数：\n\n例如在 client1 我们配置上：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token:\n    sso-client:\n        # 当前 client 标识\n        client: sso-client1\n        # ... \n    sign:\n        # sso-client1 使用的秘钥 \n        secret-key: secret-key-xxxx-1\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 当前 client 标识\nsa-token.sso-client.client=sso-client1\n\n# sso-client1 使用的秘钥 \nsa-token.sign.secret-key=secret-key-xxxx-1\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n在 client2 我们配置上：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token:\n    sso-client:\n        # 当前 client 标识\n        client: sso-client2\n        # ... \n    sign:\n        # sso-client2 使用的秘钥 \n        secret-key: secret-key-xxxx-2\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 当前 client 标识\nsa-token.sso-client.client=sso-client2\n\n# sso-client2 使用的秘钥 \nsa-token.sign.secret-key=secret-key-xxxx-2\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n### 2、然后在 SSO Server 端，重写获取秘钥的函数\n\n在 SSO Server 端新建 `CustomSaSsoServerTemplate.java`，继承 `SaSsoServerTemplate`，重写其 `getSignTemplate` 函数：\n\n``` java\n/**\n * 自定义 SaSsoServerTemplate 子类 \n */\n@Component\npublic class CustomSaSsoServerTemplate extends SaSsoServerTemplate {\n\n\t// 存储所有 client 的秘钥 \n    static Map<String, SaSignTemplate> signMap = new HashMap<>();\n    static {\n        signMap.put(\"sso-client1\", new SaSignTemplate(new SaSignConfig(\"secret-key-xxxx-1\")));\n        signMap.put(\"sso-client2\", new SaSignTemplate(new SaSignConfig(\"secret-key-xxxx-2\")));\n        signMap.put(\"sso-client3\", new SaSignTemplate(new SaSignConfig(\"secret-key-xxxx-3\")));\n\t\t// ... \n    }\n\n    @Override\n    public SaSignTemplate getSignTemplate(String client) {\n        // 先从自定义的 signMap 中获取\n        SaSignTemplate saSignTemplate = signMap.get(client);\n        if (saSignTemplate != null) {\n            return saSignTemplate;\n        }\n        // 找不到就返回全局默认的 SaSignTemplate\n        return SaManager.getSaSignTemplate();\n    }\n}\n```\n\n至此完成。\n\n\n### 3、其它注意点\n\n有同学反馈，集成 “不同 SSO Client 配置不同秘钥” 模式后，客户端发起调用 `/sso/getData` 调用时会报如下错误：\n\n``` \n无效签名：5a7fc42836deba12d96527d43c1301ea\n```\n\n或者：\n```\n参与参数签名的秘钥不可为空\n```\n\n这大概率是因为在 sso-server 端的 `/sso/getData` 接口在校验签名时忘了加 client 参数导致的，修改为如下代码即可：\n\n``` java\n// 示例：获取数据接口（用于在模式三下，为 client 端开放拉取数据的接口）\n@RequestMapping(\"/sso/getData\")\npublic SaResult getData(String apiType, String loginId) {\n    System.out.println(\"---------------- 获取数据 ----------------\");\n    System.out.println(\"apiType=\" + apiType);\n    System.out.println(\"loginId=\" + loginId);\n\n    // ↓↓↓ ⚠️ 重点代码 ↓↓↓\n    // 校验签名：只有拥有正确秘钥发起的请求才能通过校验  \n    String client = SaHolder.getRequest().getHeader(\"client\");\n    SaSsoServerProcessor.instance.ssoServerTemplate.getSignTemplate(client).checkRequest(SaHolder.getRequest());\n    // ↑↑↑ ⚠️ 重点代码 ↑↑↑\n\n    // 自定义返回结果（模拟）\n    return SaResult.ok()\n            .set(\"id\", loginId)\n            .set(\"name\", \"LinXiaoYu\")\n            .set(\"sex\", \"女\")\n            .set(\"age\", 18);\n}\n```\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-h5.md",
    "content": "# SSO整合-前后端分离架构下的整合方案\n\n---\n\n## SSO-Client 前后端分离\n\n要在前后端分离的环境中接入 SSO，思路不难，主要的工作是把后端 `/sso/login` 接口的路由中转工作拿到前端来，以`sa-token-demo-sso3-client`为例：\n\n### 1、在 sso-client 后端新建`H5Controller`，开放接口：\n\n``` java\n/**\n * 前后台分离架构下集成 SSO 所需的代码 （SSO-Client端）\n */\n@RestController\npublic class H5Controller {\n\n\t// 判断当前是否登录\n\t@RequestMapping(\"/sso/isLogin\")\n\tpublic Object isLogin() {\n\t\treturn SaResult.data(StpUtil.isLogin()).set(\"loginId\", StpUtil.getLoginIdDefaultNull());\n\t}\n\t\n\t// 返回SSO认证中心登录地址 \n\t@RequestMapping(\"/sso/getSsoAuthUrl\")\n\tpublic SaResult getSsoAuthUrl(String clientLoginUrl) {\n\t\tString serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, \"\");\n\t\treturn SaResult.data(serverAuthUrl);\n\t}\n\t\n\t// 根据 ticket 进行登录\n\t@RequestMapping(\"/sso/doLoginByTicket\")\n\tpublic SaResult doLoginByTicket(String ticket) {\n\t\tSaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);\n\t\tStpUtil.login(ctr.loginId, new SaLoginParameter()\n\t\t\t\t.setTimeout(ctr.remainTokenTimeout)\n\t\t\t\t.setDeviceId(ctr.deviceId)\n\t\t);\n\t\treturn SaResult.data(StpUtil.getTokenValue());\n\t}\n\n}\n```\n\n\n### 2、增加跨域处理策略 \n\n``` java\n/**\n * [Sa-Token 权限认证] 配置类\n */\n@Configuration\npublic class SaTokenConfigure {\n\n    /**\n     * CORS 跨域处理策略\n     */\n    @Bean\n    public SaCorsHandleFunction corsHandle() {\n        return (req, res, sto) -> {\n            res.\n                    // 允许指定域访问跨域资源\n                    setHeader(\"Access-Control-Allow-Origin\", \"*\")\n                    // 允许所有请求方式\n                    .setHeader(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, DELETE\")\n                    // 有效时间\n                    .setHeader(\"Access-Control-Max-Age\", \"3600\")\n                    // 允许的header参数\n                    .setHeader(\"Access-Control-Allow-Headers\", \"*\");\n\n            // 如果是预检请求，则立即返回到前端\n            SaRouter.match(SaHttpMethod.OPTIONS)\n                    .free(r -> System.out.println(\"--------OPTIONS预检请求，不做处理\"))\n                    .back();\n        };\n    }\n\n}\n```\n\n详细参考：[解决跨域问题](/fun/cors-filter)\n\n\n### 3、新建前端项目 \n任意文件夹新建前端项目：`sa-token-demo-sso-client-h5`，在根目录添加测试文件：`index.html`\n``` html\n<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-Token-SSO-Client端-测试页（前后端分离版-原生h5）</title>\n\t</head>\n\t<body>\n\t\t<h2>Sa-Token SSO-Client 应用端（前后端分离版-原生h5）</h2>\n\t\t<p>当前是否登录：<b class=\"is-login\"></b></p>\n\t\t<p>\n\t\t\t<a href=\"javascript: login();\">登录</a> - \n\t\t\t<a href=\"javascript: doLogoutByAlone();\">单应用注销</a> - \n\t\t\t<a href=\"javascript: doLogoutBySingleDeviceId();\">单浏览器注销</a> - \n\t\t\t<a href=\"javascript: doLogout();\">全端注销</a> - \n\t\t\t<a href=\"javascript: doMyInfo();\">账号资料</a>\n\t\t</p>\n\t\t<script src=\"sso-common.js\"></script>\n\t\t<script type=\"text/javascript\">\n\t\t\t\n\t\t\t// 登录 \n\t\t\tfunction login() {\n\t\t\t\tlocation.href = 'sso-login.html?back=' + encodeURIComponent(location.href);\n\t\t\t}\n\t\t\t\n\t\t\t// 单应用注销\n\t\t\tfunction doLogoutByAlone() {\n\t\t\t\tajax('/sso/logoutByAlone', {}, function(res){\n\t\t\t\t\tdoIsLogin();\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 单浏览器注销\n\t\t\tfunction doLogoutBySingleDeviceId() {\n\t\t\t\tajax('/sso/logout', { singleDeviceIdLogout: true }, function(res){\n\t\t\t\t\tdoIsLogin();\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 全端注销\n\t\t\tfunction doLogout() {\n\t\t\t\tajax('/sso/logout', {  }, function(res){\n\t\t\t\t\tdoIsLogin();\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 账号资料\n\t\t\tfunction doMyInfo() {\n\t\t\t\tajax('/sso/myInfo', {  }, function(res){\n\t\t\t\t\talert(JSON.stringify(res));\n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t\t// 判断是否登录 \n\t\t\tfunction doIsLogin() {\n\t\t\t\tajax('/sso/isLogin', {}, function(res){\n\t\t\t\t\tif(res.data) {\n\t\t\t\t\t\tsetHtml('.is-login', res.data + ' (' + res.loginId + ')');\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsetHtml('.is-login', res.data);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\tdoIsLogin();\n\t\t\t\n\t\t</script>\n\t</body>\n</html>\n```\n\n### 4、添加单点登录登录中转页\n\n在根目录创建文件：`sso-login.html`\n\n``` html\n<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Sa-Token-SSO-Client端-登录中转页页</title>\n\t\t<style type=\"text/css\">\n\t\t\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"login-box\">\n\t\t\t加载中 ... \n\t\t</div>\n\t\t<script src=\"sso-common.js\"></script>\n\t\t<script type=\"text/javascript\">\n\t\t\n\t\t\tvar back = getParam('back', '/');\n\t\t\tvar ticket = getParam('ticket');\n\t\t\t\n\t\t\twindow.onload = function(){\n\t\t\t\tif(ticket) {\n\t\t\t\t\tdoLoginByTicket(ticket);\n\t\t\t\t} else {\n\t\t\t\t\tgoSsoAuthUrl();\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// 重定向至认证中心 \n\t\t\tfunction goSsoAuthUrl() {\n\t\t\t\tajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {\n\t\t\t\t\tlocation.href = res.data;\n\t\t\t\t})\n\t\t\t}\n\t\t\n\t\t\t// 根据ticket值登录 \n\t\t\tfunction doLoginByTicket(ticket) {\n\t\t\t\tajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {\n\t\t\t\t\tlocalStorage.setItem('satoken', res.data);\n\t\t\t\t\tlocation.href = decodeURIComponent(back); \n\t\t\t\t})\n\t\t\t}\n\t\t\t\n\t\t</script>\n\t</body>\n</html>\n```\n\n\n### 5、添加公共 js文件\n新建 `sso-common.js`：\n\n``` js\n// 服务器接口主机地址\n// var baseUrl = \"http://sa-sso-client1.com:9002\";  // 模式二后端 \nvar baseUrl = \"http://sa-sso-client1.com:9003\";  // 模式三后端 \n\n// 封装一下Ajax\nfunction ajax(path, data, successFn, errorFn) {\n\tconsole.log('发起请求：', baseUrl + path, JSON.stringify(data));\n\tfetch(baseUrl + path, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t\t'satoken': localStorage.getItem('satoken')\n\t\t\t},\n\t\t\tbody: serializeToQueryString(data),\n\t\t})\n\t\t.then(response => response.json())\n\t\t.then(res => {\n\t\t\tconsole.log('返回数据：', res);\n\t\t\tif(res.code === 500) {\n\t\t\t\treturn alert(res.msg);\n\t\t\t}\n\t\t\tsuccessFn(res);\n\t\t})\n\t\t.catch(error => {\n\t\t\tconsole.error('请求失败:', error);\n\t\t\treturn alert(\"异常：\" + JSON.stringify(error));\n\t\t});\n}\n\n// ------------ 工具方法 ---------------\n\n// 从url中查询到指定名称的参数值\nfunction getParam(name, defaultValue) {\n\tvar query = window.location.search.substring(1);\n\tvar vars = query.split(\"&\");\n\tfor (var i = 0; i < vars.length; i++) {\n\t\tvar pair = vars[i].split(\"=\");\n\t\tif (pair[0] == name) {\n\t\t\treturn pair[1];\n\t\t}\n\t}\n\treturn (defaultValue == undefined ? null : defaultValue);\n}\n\n// 将 json 对象序列化为kv字符串，形如：name=Joh&age=30&active=true\nfunction serializeToQueryString(obj) {\n\treturn Object.entries(obj)\n\t\t.filter(([_, value]) => value != null) // 过滤 null 和 undefined\n\t\t.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n\t\t.join('&');\n}\n\n// 向指定标签里 set 内容 \nfunction setHtml(select, html) {\n\tconst dom = document.querySelector('.is-login');\n\tif(dom) {\n\t\tdom.innerHTML = html;\n\t}\n}\n```\n\n\n### 6、测试运行\n先启动 Server 服务端与 Client 服务端，再随便找个能预览html的工具打开前端项目（比如[HBuilderX](https://www.dcloud.io/hbuilderx.html)），测试流程与一体版一致，暂不赘述。\n\n\n\n\n> [!TIP| label:另附其它技术栈的前后端分离 demo 示例：] \n> - sso-client 前后端分离 - 原生h5：[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5) \n> - sso-client 前后端分离 - vue2：[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2) \n> - sso-client 前后端分离 - vue3：[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3) \n\n\n## SSO-Server 前后端分离\n\n解决思路与 SSO-Client 一样，我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。\n\n由于集成代码与 Client 端类似，这里暂不贴详细代码，我们可以下载官方仓库，里面有搭建好的demo。\n\n使用前端 ide 导入项目 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5`，浏览器访问 `sso-auth.html` 页面：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-type2-server-h5-auth.png\" alt=\"sso-type2-server-h5-auth.png\" />\n\n复制上述地址，将其配置到 Client 端的配置项 `sa-token.sso-client.auth-url` ，例如：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\tsso-client: \n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）\n\t    auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# SSO-Server端 统一认证地址 \nsa-token.sso-client.server-url=http://sa-sso-server.com:9000\n# 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）\nsa-token.sso-client.auth-url=http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n然后我们启动项目 sso-server 与 sso-client ，按照之前的测试步骤访问：\n[http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/)，即可以前后端分离模式完成 SSO-Server 端的授权登录。\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-home-jump.md",
    "content": "# SSO 平台中心跳转模式，点连接跳入子系统\n\n--- \n\n有的时候，我们需要把 sso-server 搭建成一个平台中心，效果图大致如下：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-home-jump.png\" alt=\"sso-home-jump.png\" />\n\n如图所示，用户先从 sso-server 登录进入平台首页，在首页上有各个子系统的进入链接，用户点击链接进入子系统（免登录）。\n\n怎么做到如上效果呢？当然，加个超链接跳到子系统并不难，难点在于跳转的同时我们需要让用户自动登录上子系统，从而达到：平台中心一处登录，所有子系统无障碍通行的效果。\n\n怎么做到跳转的时候自动登录呢？直接跳转肯定是不会自动登录的，我们需要对链接改造一下：\n\n假设子系统的地址是：\n\n``` url\nhttp://sa-sso-client1.com:9003/\n```\n\n那么我们改造后的地址就是：\n\n``` url\n/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/\n```\n\n格式形如：`/sso/auth?client={client标识}&redirect=${子系统首页}/sso/login?back=${子系统首页}`\n\n--- \n\n### 完整代码示例：\n\n1、在 sso-server 中配置 `home-route` 字段：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token:\n    # SSO-Server 配置\n    sso-server:\n        # 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\n        home-route: /home\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\nsa-token.sso-server.home-route: /home\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n2、在 sso-server 中添加 `HomeController`，作为平台中心首页：\n\n``` java\n/**\n * SSO 平台中心模式示例，跳连接进入子系统 \n */\n@RestController\npublic class HomeController {\n\t// 平台化首页\n    @RequestMapping({\"/\", \"/home\"})\n    public Object index() {\n        // 如果未登录，则先去登录\n        if(!StpUtil.isLogin()) {\n            return SaHolder.getResponse().redirect(\"/sso/auth\");\n        }\n\n        // 拼接各个子系统的地址，格式形如：/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页}\n        String link1 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/\";\n        String link2 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/\";\n        String link3 = \"/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/\";\n\n        // 组织网页结构返回到前端\n        String title = \"<h2>SSO 平台首页 (平台中心模式)</h2>\";\n        String client1 = \"<p><a href='\" + link1 + \"' target='_blank'> 进入Client1系统 </a></p>\";\n        String client2 = \"<p><a href='\" + link2 + \"' target='_blank'> 进入Client2系统 </a></p>\";\n        String client3 = \"<p><a href='\" + link3 + \"' target='_blank'> 进入Client3系统 </a></p>\";\n\n        return title + client1 + client2 + client3;\n    }\n}\n```\n\n\n### 测试访问\n\n启动项目，访问：[http://sa-sso-server.com:9000](http://sa-sso-server.com:9000)\n\n首次访问，因为我们没有登录，所以会被重定向到 `/sso/auth` 登录页，我们登录上之后，便会跳转到平台中心首页：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-home-jump-do.png\" alt=\"sso-home-jump-do.png\" />\n\n依次点击三个链接，便可在跳转的同时自动登录上子系统。\n"
  },
  {
    "path": "sa-token-doc/sso/sso-nosdk.md",
    "content": "# SSO整合 - NoSdk、ReSdk 模式与非 java 项目\n\n---\n\n经常有小伙伴提问：客户端不使用 Sa-Token，能否接入 SSO 认证中心？当然是可以的。\n\nSSO-Server 所有接口都是通过 http 协议开放的，这意味着原则上只要一个语言支持 http 请求调用就可以对接 SSO-Server，参考： [SSO 认证中心开放接口](/sso/sso-apidoc)\n\n### NoSdk 模式\n\nNoSdk 模式（不使用SDK）：通过 http 工具类调用接口的方式来对接 SSO-Server。\n\n参考 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)\n\n该 demo 假设应用端没有使用任何“权限认证框架”，使用最基础的 ServletAPI 进行会话管理，模拟了 `/sso/login`、 `/sso/logout`、 `/sso/logoutCall` 三个接口的处理逻辑。\n\n> [!WARNING| label:NoSdk 示例不再主维护] \n> 基于以下原因：\n> - 1、NoSdk demo 相当于通过 http 工具类再次重写了一遍 Sa-Token SSO 模块代码，繁琐且冗余。\n> - 2、重写的代码无法拥有 Sa-Token SSO 模块全部能力，仅能完成基本对接，算是一个简化版 SDK。\n> \n> 自 v1.43.0 版本起，不再主维护 NoSdk 模式，仓库示例仅做留档参考，大家可以转为 ReSdk 模式。\n\n\n### ReSdk 模式\n\nReSdk 模式（重写SDK部分方法）：通过重写框架关键步骤点，来对接 SSO-Server。\n\n参考 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)\n\n> [!INFO| label:ReSdk 模式优点] \n> - 1、依然支持客户端使用任意技术栈。\n> - 2、仅重写少量部分关键代码，即可完成对接。几乎可以得到 Sa-Token SSO 模块全量能力。\n\n建议新项目首选 ReSdk 模式作为参考。\n\n\n\n\n### 非 java 语言项目\n\nsso-server 的所有接口均以 http 协议对外开放，因此原则上支持任何语言对接，只要这个语言支持 http 请求调用。\n\n例如 PHP、.NET、Node.js 等语言的项目，无法集成 Sa-Token，同上，也可以通过 http 工具类调用接口的方式来对接 SSO-Server。\n\n建议各位同学先搞懂 NoSdk 模式的对接流程，再参照 [SSO 认证中心开放接口](/sso/sso-apidoc) 章节进行对接。\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-pro.md",
    "content": "# Sa-Sso-Pro 单点登录商业版\n\n### 项目介绍\n\n根据 Sa-Token SSO 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个SSO模式的认证Demo。\n\n<!-- 然而对于一些企业级项目，简单的demo示例，显然无法完成我们的项目需求，要真正开发一个商业级项目的认证中心系统，绝非一朝一夕可以搭建完毕， -->\n\n然而，要真正开发一个商业级项目的认证中心系统，绝非一朝一夕可以搭建完毕，其必不可少的一些功能，\n比如：用户账号增删改查维护、登录日志统计、新增用户数据报表、新增 Client 应用接入域名配置……等等，\n仍需要大量的开发时间。\n\n为此，我们特意准备了项目：[[ Sa-Sso-Pro 单点登录商业版 ]](https://sa-pro.yun94.cn?way=st_md)，\n项目集成了单点登录常见技术点， 绝大多数功能无需二次开发，直接可用，<b style=\"color: #FF5722;\">可大大缩短您的项目接入单点登录的开发周期</b>。\n\n\n### 释疑\n\n##### 1、Sa-Sso-Pro 是收费项目吗？与 Sa-Token 有什么不同？\n\n`Sa-Sso-Pro` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。\n\n`Sa-Sso-Pro` 与 `Sa-Token` 的区别，简单来讲：\n- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入\n- `Sa-Sso-Pro` 是一个完整项目，下载源码后可直接启动 \n\n\n##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？\n首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 \n\n其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，\n比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。\n\nSa-Sso-Pro 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。\n\n另外：即使您没有购买 `Sa-Sso-Pro`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。\n\n\n\n##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Sso-Pro 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！\n\n答案是不会。\n\n再次强调一下：`Sa-Token` 与 `Sa-Sso-Pro` 是两个独立的项目，两者互不影响。\n付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。\n\n实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。\n\n<!-- 衷心感谢每一位粉丝的支持！ -->\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-questions.md",
    "content": "# Sa-Token-SSO整合-常见问题总结\n\nSSO 集成常见问题整理\n\n[[toc]]\n\n--- \n\n\n### 问：在模式一与模式二中，Client端 必须通过 Alone-Redis 插件来访问 Redis 吗？\n答：不必须，只是推荐，权限缓存与业务缓存分离后会减少 `SSO-Redis` 的访问压力，且可以避免多个 `Client端` 的缓存读写冲突。\n\n\n### 问：搭建好 sso-server 或 sso-client 服务后，访问返回：`{\"msg\": \"not handle\"}`。\n\n返回这个信息，代表你访问的路由有错误，比如说：\n\n- 统一认证登录地址是：`http://{host}:{port}/sso/auth`。\n- 而你访问的却是：`http://{host}:{port}/sso/auth2`。\n\n地址写错了，框架就不会处理这个请求，会直接返回 `{\"msg\": \"not handle\"}`，所有开放地址可参考：[SSO 开放接口](/sso/sso-apidoc)\n\n如果仔细检查地址后没有写错，却依然返回了这个信息，那有可能是对应的接口没有打开，比如说：\n\n- sso-server 端的单点注销地址：`http://{host}:{port}/sso/signout`；\n- sso-client 端的注销地址：`http://{host}:{port}/sso/logout`；\n\n都需要在配置文件配置：`sa-token.sso-server.is-slo=true`(client端为 `sa-token.sso-client.is-slo=true` )后，才会打开。\n\n\n### 问：我参照文档搭建SSO-Client，一直提示：Ticket无效，请问怎么回事？\n如果使用的是模式二，出现此异常概率最大的原因是因为 `Client` 与 `Server` 没有连接同一个Redis，SSO模式二中两者必须连接同一个 Redis 才可以登录成功。\n\n你可能会问：我看配置文件明明是同一个啊？\n\n我的建议是：排查时不要仅凭肉眼判断，分别在你的 `Client` 与 `Server` 启动后调用 `SaManager.getSaTokenDao().set(\"name\", \"value\", 100000);` \n随便写入一个值，看看能不能根据你的预期写进同一个Redis里，如果能的话才能证明 `Client` 与 `Server` 连接的Reids 是同一个，再进行下一步排查。\n\n``` java\n@SpringBootApplication\npublic class SaSsoServerApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSsoServerApplication.class, args);\n\t\tSystem.out.println(\"\\n------ Sa-Token-SSO 统一认证中心启动成功 \");\n\t\t// 分别在 Client 与 Server 启动后调用 set 数据代码，看看能否根据预期写入同一个 reids \n\t\tSaManager.getSaTokenDao().set(\"name\", \"value\", 100000);\n\t}\t\n}\n```\n\n如果使用的是模式三，则排查是否有重复校验 ticket 的代码，一个 ticket 码只能使用一次，多次重复使用就会提示这个。\n\n\n### 问：模式一或者模式二报错：Could not write JSON: No serializer found for class com.pj.sso.SysUser and no properties discovered to create BeanSerializer \n\n一般是因为在 sso-server 端往 session 上写入了某个实体类（比如 User），而在 sso-client 端没有这个实体类，导致反序列化失败。\n\n解决方案：在 sso-client 也新建上这个类，而且包名需要与 sso-server 端的一致（直接从 sso-server 把实体类复制过来就好了）\n\n\n\n### 在测试模式一时，出现一些难以理解的现象 \n\n测试模式一时，三种异常现象：\n\n1、在 sso-client 端点击登录，可以成功跳转到 sso-server 端，登录后可以跳回 sso-client 端，但显示 sso-client 端未登录\t\n- 原因：sso-server 后端没有配置 sa-token.cookie.domain 值。\n\n2、在 sso-client 端点击登录，可以成功跳转到 sso-server 端，登录页面刷新一下，并没有跳转回 sso-client 端，依然提示让你登录\n- 可能1：sso-server 后端配置了错误的 sa-token.cookie.domain 值。\t\n- 可能2：sso-server 后端没有打开 sa-token.is-read-cookie 值。（测试模式二三时发生这种现象，有时候也是因为这个）\n\n3、在 sso-client 端点击登录，页面只是闪了一下，肉眼没有观察到页面跳转，页面也没有显示登录上。\n\n原因：sso-server 的页面存储了不带 . 的有效 Cookie，为什么会这样：常常是因为测试模式二三后，没有清除redis记录或者浏览器记录，直接再开始测试模式一，\n模式二三登录成功后遗留的有效cookie，影响了模式一的行为逻辑。\n\n解决方案：\n- 1、手动清空 redis里的所有数据，\n- 2、或者手动清空 sso-server 域名下的所有 Cookie \n- 3、换一个新的干净浏览器来测试。\n\t\n\t\n\n\n\n### 问：模式三配置一堆 xxx-url ，有办法简化一下吗？\n可以使用 `sa-token.sso-client.server-url` 来简化：\n\n配置含义：配置 Server 端主机总地址，拼接在 authUrl、getDataUrl、sloUrl 属性前面，用以简化各种 url 配置。\n\n在开发 SSO 模块时，我们需要在 sso-client 配置认证中心的各种地址，特别是在模式三下，一般代码会变成这样：\n\n``` yaml\nsa-token: \n    sso-client: \n        # SSO-Server端 统一认证地址 \n        auth-url: http://sa-sso-server.com:9000/sso/auth\n        # 单点注销地址 \n        slo-url: http://sa-sso-server.com:9000/sso/signout\n        # SSO-Server端 查询数据地址 \n        get-data-url: http://sa-sso-server.com:9000/sso/getData\n```\n\n一堆 xxx-url 配置比较繁琐，且含有大量重复字符，现在我们可以将其简化为：\n``` yaml\nsa-token: \n    sso-client: \n        server-url: http://sa-sso-server.com:9000\n```\n\n只要你配置了 `server-url` 地址，Sa-Token 就可以自动拼接出其它四个地址：\n\n**例1，使用 server-url 简化：**\n- 你配置的 server-url 值是：`http://sa-sso-server.com:9000`。\n- 框架拼接出的 auth-url 值就是：`http://sa-sso-server.com:9000/sso/auth`，其它三个 url 配置项同理。\n\n**例2，使用 server-url + auth-url 简化：**\n- 你配置的 server-url 值是：`http://sa-sso-server.com:9000`，auth-url 是：`/sso/auth2`。\n- 框架拼接出的 auth-url 值就是：`http://sa-sso-server.com:9000/sso/auth2`，其它三个 url 配置项同理。\n\n**例3，auth-url 地址以 http 字符开头：**\n- 你配置的 server-url 值是：`http://sa-sso-server.com:9000`，auth-url 是：`http://my-site.com/sso/auth2`。\n- 此时框架只以 auth-url 值为准，得到的 auth-url 值是：`http://my-site.com/sso/auth2`，其它三个 url 配置项同理。\n\n\n### 问：我接手了一个项目，里面集成了 Sa-Token SSO ，请问怎么快速分辨它用的模式几？\n\n**方法一：看代码注释。**\n\n如果开发这个项目的人没有写清楚注释那就 gg 了。\n\n**方法二，根据配置项来分析，例如：**\n\n- 先看配置项 `sa-token.cookie.domain`，如果此配置项有值，一般是在使用模式一开发，否则就是模式二或者模式三。\n- 再看配置项 `sa-token.sso-client.is-http` ，如果有值且为 true，一般是在使用模式三，否则就是模式二。\n\n**方法三，根据配置项 `sa-token.sso-client.mode` 的提示来判断**\n\n`sa-token.sso-client.mode` 是框架预留的约定型配置项，此配置项不对代码逻辑产生任何影响，只为系统做一个标记，标注此系统用到了SSO的哪个模式。\n\n例如你可以将其配置为 `sa-token.sso-client.mode=client-2`，代表当前系统为 sso-client 端，使用 SSO 模式二来对接。\n\n需要注意，这个配置项不是必须的，你不写也不会对代码造成任何影响，只有在你需要为系统做一个明确的标记时才需要去配置它，方便后人阅读代码时快速分析使用的模式。\n\n例如我们可以使用以下约定：\n\n- `sa-token.sso-client.mode=client-2`：代表当前系统为 sso-client 端，使用 SSO 模式二来对接。\n- `sa-token.sso-client.mode=client-2,h5`：代表当前系统为 sso-client 端，使用 SSO 模式二来对接，并且是前后端分离模式。\n- `sa-token.sso-server.mode=server-123`：代表当前系统为 sso-server 端，同时开放了 SSO 模式一、模式二、模式三。\n- `sa-token.sso-server.mode=server-2,client-2`：代表当前系统既是 sso-server 端，又是 sso-clent 端，使用模式二来对接。\n- 等等等等...\n\n此配置项可以是任意字符串，你也可以自己整理一套合适的表达规则。\n\n\n\n\n\n\n### 问：SSO模式二或模式三，第一个 client 登录成功之后再访问其它两个 client 不会自动登录，需要点一下登录按钮才会登录上？\n答：这是正常现象，系统 1 登录成功之后，系统 2 与系统 3 需要点击登录按钮，才会登录成功。\n\n> 第一个系统，需要：点击 [登录] 按钮 -> 跳转到登录页 -> 输账号密码 -> 登录成功 <br>\n> 第二个系统，需要：点击 [登录] 按钮 -> 登录成功 <br>\n> 第三个系统，需要：点击 [登录] 按钮 -> 登录成功 \n> \n> （系统二、三 免去重复跳转登录页输入账号密码的步骤）\n\n\n### 追问：那我是否可以设计成不需要点登录按钮的，只要访问页面，它就能登录成功？\n可以的。\n\n其实思路很简单，我们只需要给 client 项目加个过滤器，拦截所有请求，只要检测到未登录就将其重定向至登录页面：\n\n``` java\n/**\n * Sa-Token 配置类\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n    /** 注册 [Sa-Token全局过滤器] */\n    @Bean\n    public SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n                .addInclude(\"/**\")\n                .addExclude(\"/sso/*\", \"/favicon.ico\")   // 这里需要注意排除掉 /sso/* 相关请求不拦截，否则就会触发无限重定向\n                .setAuth(obj -> {\n                    /*\n                     * 这里会被分为两种情况：\n                     *  情况1：这个请求在当前 client 已经登录，此时会顺利进入网站\n                     *  情况2：这个请求在当前 client 尚未登录，此时会被拦截，重定向至当前系统的 /sso/login?back=当前地址\n                     *\n                     *  情况2会带领着用户继续重定向至 sso-server 认证中心，此时又分为两种情况：\n                     *      情况2.1：此用户在 sso-server 尚未登录，此时会停留在登录页面，开始输入账号密码进行登录\n                     *      情况2.2：此用户在 sso-server 已经登录（这证明此用户已经在其它至少一个 sso-client 处完成了登录）\n                     *              此时用户会继续重定向回当前 client，并携带 ticket 参数，完成登录。\n                     */\n                    if(StpUtil.isLogin() == false) {\n                        String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());\n                        SaHolder.getResponse().redirect(\"/sso/login?back=\" + SaFoxUtil.encodeUrl(back));\n                        SaRouter.back();\n                    }\n                })\n                ;\n    }\n}\n```\n\n\n更多登录姿势可以参考 [[何时引导用户去登录]](/sso/sso-custom-login) 给出的建议进行设计。\n\n\n\n### 问：Client 信息可以做成从数据库读取的吗？\n可以，自定义 `SaSsoServerTemplate` 实现类，重写 `getClient` 与 `getClients` 方法即可：\n``` java\n/**\n * 重写 SaSsoServerTemplate 部分方法，增强功能\n */\n@Component\npublic class CustomSaSsoServerTemplate extends SaSsoServerTemplate {\n\n    // 获取指定 client 的配置信息\n    @Override\n    public SaSsoClientModel getClient(String client) {\n        if(\"sso-client1\".equals(client)) {\n            SaSsoClientModel scm = new SaSsoClientModel();\n            scm.setAllowUrl(\"sso-client1\");\n            scm.setSecretKey(\"kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\");\n            return scm;\n        }\n\n        // ...\n\n        return null;\n    }\n\n    // 返回所有 client 信息\n    @Override\n    public List<SaSsoClientModel> getClients() {\n        // 模拟示例代码，真实项目可改为从数据查询\n\n        SaSsoClientModel scm1 = new SaSsoClientModel();\n        scm1.setAllowUrl(\"sso-client1\");\n        scm1.setSecretKey(\"kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\");\n\n        SaSsoClientModel scm2 = new SaSsoClientModel();\n        scm2.setAllowUrl(\"sso-client2\");\n        scm2.setSecretKey(\"kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\");\n\n        // ...\n\n        return Arrays.asList(scm1, scm2);\n    }\n\n}\n```\n\n\n### 问：如果 sso-client 端我没有集成 sa-token-sso，如何对接？\n需要手动调用 http 请求来对接 sso-server 开放的接口，参考示例：\n[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)\n\n\n### 问：如果 sso-client 端不是 java语言，可以对接吗？\n可以，只不过有点麻烦，基本思路和上个问题一致，需要手动调用 http 请求来对接 sso-server 开放的接口，参考：\n[SSO-Server 认证中心开放接口](/sso/sso-apidoc)\n\n\n### 问：将旧有系统改造为单点登录时，应该注意哪些？\n答：建议不要把其中一个系统改造为SSO服务端，而是新起一个项目作为 SSO-Server 端，所有旧有项目全部作为 Client 端与此对接。\n\n\n### 问：怎么在一个项目里同时搭建 sso-server 和 sso-client？\n\n难点在于解决两边的路由冲突，示例代码：\n\n``` java\n// Sa-Token SSO Controller \n@RestController\npublic class SsoController {\n\t\n\t// 处理 SSO-Server 端所有请求 \n\t@RequestMapping({\"/sso/auth\", \"/sso/doLogin\", \"/sso/signout\", \"/sso/pushS\"})\n\tpublic Object ssoServerRequest() {\n\t\treturn SaSsoServerProcessor.instance.dister();\n\t}\n\t\n\t// 处理 SSO-Client 端所有请求 \n\t@RequestMapping({\"/sso/login\", \"/sso/logout\", \"/sso/logoutCall\", \"/sso/pushC\"})\n\tpublic Object ssoClientRequest() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\t\n\t// 配置SSO相关参数 \n\t@Autowired\n\tprivate void configSsoServer(SaSsoServerTemplate ssoServerTemplate) {\n\t\t// SSO Server 配置代码，参考文档前几章 ... \n\t}\n\t@Autowired\n\tprivate void configSsoClient(SaSsoClientTemplate ssoClientTemplate) {\n\t\t// SSO Client 配置代码，参考文档前几章 ... \n\t}\n\t\n}\n```\n\n\n### 问：我一个项目里有两套账号体系，都需要单点登录，怎么在一个项目里同时搭建两个 sso-server 服务？\n\n首先推荐你不要在一个项目里同时搭建两个 sso-server，建议创建两个项目，分别搭建各自的 sso-server 服务。\n\n如果一定要在一个项目中搭建两套 sso-server 服务，参考方案如下：\n\n第一套，还是用前面几章文档给出的示例代码。\n\n第二套，修改一些参数属性，使之与第一套不产生冲突，参考代码如下：\n\n``` java\n/**\n * Sa-Token-SSO 第二套 SSO-Server端 Controller\n */\n@RestController\npublic class SsoUserServerController {\n\n    /**\n     * 新建一个 SaSsoServerProcessor 请求处理器\n     */\n    public static SaSsoServerProcessor ssoUserServerProcessor = new SaSsoServerProcessor();\n    static {\n\n        // 自定义一个 getServerConfig\n        SaSsoServerConfig serverConfig = new SaSsoServerConfig();\n        serverConfig.setSecretKey(\"xxx\");\n        // 更多配置 ...\n\n        // 自定义一个 SaSsoServerTemplate 对象\n        SaSsoServerTemplate ssoUserTemplate = new SaSsoServerTemplate() {\n            @Override\n            public SaSsoServerConfig getServerConfig() {\n                return serverConfig;\n            }\n        };\n\n        // 使用自定义的 StpLogic 会话对象\n        ssoUserTemplate.setStpLogic(StpUserUtil.stpLogic);\n\n        // 让这个SSO请求处理器，使用的路由前缀是 /sso-user，而不是原先的 /sso\n        ssoUserTemplate.apiName.replacePrefix(\"/sso-user\");\n\n        // 给这个 SSO 请求处理器使用自定义的 SaSsoTemplate 对象\n        ssoUserServerProcessor.ssoServerTemplate = ssoUserTemplate;\n    }\n\n    /*\n     * 第二套 sso-server 服务：处理所有SSO相关请求\n     * \t\thttp://{host}:{port}/sso-user/auth\t\t\t-- 单点登录授权地址 \n     * \t\thttp://{host}:{port}/sso-user/doLogin\t\t-- 账号密码登录接口 \n     * \t\thttp://{host}:{port}/sso-user/signout\t\t-- 单点注销地址（isSlo=true时打开） \n     */\n    @RequestMapping(\"/sso-user/*\")\n    public Object ssoUserRequest() {\n        return ssoUserServerProcessor.dister();\n    }\n\n    // 自定义 doLogin 方法 */\n    // 注意点：\n    // \t\t1、第2套 sso-server 对应的 RestApi 登录接口也应该更换为 /sso-user/doLogin，而不是原先的 /sso/doLogin\n    // \t\t2、在这里，登录函数要使用自定义的 StpUserUtil.login()，而不是原先的 StpUtil.login()\n    @RequestMapping(\"/sso-user/doLogin\")\n    public Object ssoUserRequest(String name, String pwd) {\n        if(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n            StpUserUtil.login(10001);\n            return SaResult.ok(\"登录成功！\").setData(StpUserUtil.getTokenValue());\n        }\n        return SaResult.error(\"登录失败！\");\n    }\n\n}\n```\n\n\n\n<br/>\n\n--- \n\n<details>\n<summary>还有其它问题？</summary>\n<p>\n可以加群反馈一下，比较典型的问题我们解决之后都会提交到此页面方便大家快速排查\n</p>\n</details>\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-server.md",
    "content": "# 搭建统一认证中心 SSO-Server  \n\n在开始SSO三种模式的对接之前，我们必须先搭建一个 SSO-Server 认证中心 \n\n> [!TIP| label:demo] \n> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`，如遇到难点可结合源码进行测试学习，demo里有制作好的登录页面 \n\n--- \n\n### 1、添加依赖 \n创建 SpringBoot 项目 `sa-token-demo-sso-server`，在引入 SpringBoot 依赖的基础上，继续引入：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证，在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 插件：整合SSO -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-sso</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n        \n<!-- Sa-Token 插件：整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n\t<groupId>org.apache.commons</groupId>\n\t<artifactId>commons-pool2</artifactId>\n</dependency>\n\n<!-- 视图引擎（在前后端不分离模式下提供视图支持） -->\n<dependency>\n\t<groupId>org.springframework.boot</groupId>\n\t<artifactId>spring-boot-starter-thymeleaf</artifactId>\n</dependency>\n\n<!-- Sa-Token 插件：整合 Forest 请求工具 (模式三需要通过 http 请求推送消息) -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-forest</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n\n// Sa-Token 插件：整合SSO\nimplementation 'cn.dev33:sa-token-sso:${sa.top.version}'\n\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n\n// 视图引擎（在前后端不分离模式下提供视图支持）\nimplementation 'org.springframework.boot:spring-boot-starter-thymeleaf'\n\n// Sa-Token 插件：整合 Forest 请求工具 (模式三需要通过 http 请求推送消息)\nimplementation 'cn.dev33:sa-token-forest:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n> [!NOTE| label:引包简化] \n> 除了 `sa-token-spring-boot-starter` 和 `sa-token-sso` 以外，其它包都是可选的：\n> \n> - 在 SSO 模式三时 Redis 相关包是可选的。\n> - 在前后端分离模式下可以删除 thymeleaf 相关包。\n> - 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包。\n> \n> 建议先完整测试三种模式之后再对 pom 依赖进行酌情删减。\n\n\n### 2、开放认证接口  \n新建 `SsoServerController`，用于对外开放接口：\n\n``` java\n/**\n * Sa-Token-SSO Server端 Controller \n */\n@RestController\npublic class SsoServerController {\n\n\t/**\n\t * SSO-Server端：处理所有SSO相关请求 \n\t * \t\thttp://{host}:{port}/sso/auth\t\t\t-- 单点登录授权地址\n\t * \t\thttp://{host}:{port}/sso/doLogin\t\t-- 账号密码登录接口，接受参数：name、pwd\n\t * \t\thttp://{host}:{port}/sso/signout\t\t-- 单点注销地址（isSlo=true时打开）\n\t */\n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoServerProcessor.instance.dister();\n\t}\n\n\t/**\n\t * 配置SSO相关参数 \n\t */\n\t@Autowired\n\tprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\t\t// 配置：未登录时返回的View \n\t\tssoServerTemplate.strategy.notLoginView = () -> {\n\t\t\t// 简化模拟表单\n\t\t\tString doLoginCode =\n\t\t\t\t\t\"fetch(`/sso/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) \" +\n\t\t\t\t\t\" .then(res => res.json()) \" +\n\t\t\t\t\t\" .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )\";\n\t\t\tString res =\n\t\t\t\t\t\"<h2>当前客户端在 SSO-Server 认证中心尚未登录，请先登录</h2>\" +\n\t\t\t\t\t\"用户：<input id='name' /> <br> \" +\n\t\t\t\t\t\"密码：<input id='pwd' /> <br>\" +\n\t\t\t\t\t\"<button onclick=\\\"\" + doLoginCode + \"\\\">登录</button>\";\n\t\t\treturn res;\n\t\t};\n\t\t\n\t\t// 配置：登录处理函数 \n\t\tssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {\n\t\t\t// 此处仅做模拟登录，真实环境应该查询数据库进行登录 \n\t\t\tif(\"sa\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\t\tStpUtil.login(10001);\n\t\t\t\treturn SaResult.ok(\"登录成功！\").setData(StpUtil.getTokenValue());\n\t\t\t}\n\t\t\treturn SaResult.error(\"登录失败！\");\n\t\t};\n\t}\n\t\n}\n```\n\n注意：在`doLoginHandle`函数里如果要获取 name, pwd 以外的参数，可通过`SaHolder.getRequest().getParam(\"xxx\")`来获取。\n<!-- - `deviceId` 参数代表登录端设备id，是为了后续的 “单设备注销” 功能做准备，如果不需要此功能可以省略此参数。 -->\n\n全局异常处理：\n``` java\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n}\n```\n\n\n### 3、application.yml配置\n\n<!---------------------------- tabs:start ---------------------------->\n\n<!------------- tab:yaml 风格  ------------->\n``` yml\n# 端口\nserver:\n    port: 9000\n\n# Sa-Token 配置\nsa-token: \n    # 打印操作日志\n    is-log: true\n\t\n    # SSO-模式一相关配置  (非模式一不需要配置) \n    # cookie: \n        # 配置 Cookie 作用域 \n        # domain: stp.com \n        \n    # SSO-Server 配置\n    sso-server:\n        # Ticket有效期 (单位: 秒)，默认五分钟 \n        ticket-timeout: 300\n        # 应用列表：配置接入的应用信息\n        clients:\n            # 应用 sso-client1：采用模式一对接 (同域、同Redis)\n            sso-client1:\n                client: sso-client1\n                allow-url: \"*\"\n            # 应用 sso-client2：采用模式二对接 (跨域、同Redis)\n            sso-client2:\n                client: sso-client2\n                allow-url: \"*\"\n                secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n            # 应用 sso-client3：采用模式三对接 (跨域、跨Redis)\n            sso-client3:\n                # 应用名称\n                client: sso-client3\n                # 允许授权地址\n                allow-url: \"*\"\n                # 是否接收消息推送\n                is-push: true\n                # 消息推送地址\n                push-url: http://sa-sso-client1.com:9003/sso/pushC\n                # 接口调用秘钥 (如果不配置则使用全局默认秘钥)\n                secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n        \nspring: \n    # Redis配置 （SSO模式一和模式二使用Redis来同步会话）\n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 端口\nserver.port=9000\n\n################## Sa-Token 配置 ##################\n# 打印操作日志\nsa-token.is-log=true\n\n# SSO-模式一相关配置  (非模式一不需要配置) \n# 配置 Cookie 作用域 \n# sa-token.cookie.domain=stp.com\n\n# SSO-Server 配置\n# Ticket有效期 (单位: 秒)，默认五分钟 \nsa-token.sso-server.ticket-timeout=300\n\n# 应用列表：配置接入的应用信息\n# 应用 sso-client1：采用模式一对接 (同域、同Redis)\nsa-token.sso-server.clients.sso-client1.client=sso-client1\nsa-token.sso-server.clients.sso-client1.allow-url=*\n\n# 应用 sso-client2：采用模式二对接 (跨域、同Redis)\nsa-token.sso-server.clients.sso-client2.client=sso-client2\nsa-token.sso-server.clients.sso-client2.allow-url=*\nsa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\n# 应用 sso-client3：采用模式三对接 (跨域、跨Redis)\n# 应用名称\nsa-token.sso-server.clients.sso-client3.client=sso-client3\n# 允许授权地址\nsa-token.sso-server.clients.sso-client3.allow-url=*\n# 是否接收消息推送\nsa-token.sso-server.clients.sso-client3.is-push=true\n# 消息推送地址\nsa-token.sso-server.clients.sso-client3.push-url=http://sa-sso-client1.com:9003/sso/pushC\n# 接口调用秘钥 (如果不配置则使用全局默认秘钥)\nsa-token.sso-server.clients.sso-client3.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\n# Redis配置 （SSO模式一和模式二使用Redis来同步会话）\n# Redis数据库索引（默认为0）\nspring.redis.database=1\n# Redis服务器地址\nspring.redis.host=127.0.0.1\n# Redis服务器连接端口\nspring.redis.port=6379\n# Redis服务器连接密码（默认为空）\nspring.redis.password=\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n注意点：`sa-token.sso-server.clients.xxx.allow-url`为了方便测试配置为`*`，线上生产环境一定要配置为详细 URL 地址 （之后的章节我们会详细阐述此配置项）\n\n\n### 4、创建启动类\n``` java \n@SpringBootApplication\npublic class SaSsoServerApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSsoServerApplication.class, args);\n\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getServerConfig());\n\t\tSystem.out.println(\"统一认证登录地址：http://sa-sso-server.com:9000/sso/auth\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n}\n```\n\n启动项目，不出意外的情况下我们将看到如下输出：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-server-start.png\" alt=\"sso-server-start\">\n\n访问统一授权地址（仅测试 SSO-Server 是否部署成功，暂时还不需要点击登录）：\n- [http://localhost:9000/sso/auth](http://localhost:9000/sso/auth)\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-server-init-login--v43.png\" alt=\"sso-server-init-login.png\">\n\n可以看到这个页面目前非常简陋，这是因为我们以上的代码示例，主要目标是为了带大家从零搭建一个可用的SSO认证服务端，所以就对一些不太必要的步骤做了简化。\n\n大家可以下载运行一下官方仓库里的示例`/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`，里面有制作好的登录页面：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-server-init-login2.png\" alt=\"sso-server-init-login2.png\">\n\n默认账号密码为：`sa / 123456`，先别着急点击登录，因为我们还没有搭建对应的 Client 端项目，\n真实项目中我们一般不会直接从浏览器访问 `/sso/auth` 授权地址的，我们需要在 Client 端点击登录按钮重定向而来。\n\n\n---\n\n现在我们先来看看除了 `/sso/auth` 统一授权地址，这个 SSO-Server 认证中心还开放了哪些API：[SSO-Server 认证中心开放接口](/sso/sso-apidoc)。\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-type1.md",
    "content": "# SSO模式一 共享Cookie同步会话\n\n如果我们的多个系统可以做到：前端同域、后端同Redis，那么便可以使用 **`[共享Cookie同步会话]`** 的方式做到单点登录。\n\n--- \n\n### 1、设计思路\n\n首先我们分析一下多个系统之间，为什么无法同步登录状态？\n1. 前端的 `Token` 无法在多个系统下共享。\n2. 后端的 `Session` 无法在多个系统间共享。\n\n所以单点登录第一招，就是对症下药：\n1. 使用 `共享Cookie` 来解决 Token 共享问题。\n2. 使用 `Redis` 来解决 Session 共享问题。\n\n所谓共享Cookie，就是主域名Cookie在二级域名下的共享，举个例子：写在父域名`stp.com`下的Cookie，在`s1.stp.com`、`s2.stp.com`等子域名都是可以共享访问的。\n\n而共享Redis，并不需要我们把所有项目的数据都放在同一个Redis中，Sa-Token提供了 **[权限缓存与业务缓存分离]** 的解决方案，详情戳：[Alone独立Redis插件](/plugin/alone-redis)。\n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/sso/g3--sso1.gif\">加载动态演示图</button>\n\n\nOK，所有理论就绪，下面开始实战：\n\n\n### 2、准备工作\n\n首先修改hosts文件`(C:\\windows\\system32\\drivers\\etc\\hosts)`，添加以下IP映射，方便我们进行测试：\n``` url\n127.0.0.1 sso.stp.com\n127.0.0.1 s1.stp.com\n127.0.0.1 s2.stp.com\n127.0.0.1 s3.stp.com\n```\n\n其中：`sso.stp.com`为统一认证中心地址，当用户在其它 Client 端发起登录请求时，均将其重定向至认证中心，待到登录成功之后再原路返回到 Client 端。\n\n[Some Name](../include/include-qa.md#hostsInvalid ':include')\n\n\n### 3、指定Cookie的作用域\n在`sso.stp.com`访问服务器，其Cookie也只能写入到`sso.stp.com`下，为了将Cookie写入到其父级域名`stp.com`下，我们需要更改 SSO-Server 端的 yml 配置：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n    cookie: \n        # 配置 Cookie 作用域 \n        domain: stp.com \n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 配置 Cookie 作用域 \nsa-token.cookie.domain=stp.com\n```\n<!---------------------------- tabs:end ---------------------------->\n\n**这个配置原本是被注释掉的，现在将其打开。**另外我们格外需要注意：\n在SSO模式一测试完毕之后，一定要将这个配置再次注释掉，因为模式一与模式二三使用不同的授权流程，这行配置会影响到我们模式二和模式三的正常运行。 \n\n\n\n\n### 4、搭建 Client 端项目 \n\n> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client/`，如遇到难点可结合源码进行测试学习。\n\n\n#### 4.1、引入依赖\n新建项目 sa-token-demo-sso1-client，并添加以下依赖：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<!-- Sa-Token 插件：整合SSO -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-sso</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 整合 RedisTemplate  -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n\t<groupId>org.apache.commons</groupId>\n\t<artifactId>commons-pool2</artifactId>\n</dependency>\n\n<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-alone-redis</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n\n// Sa-Token 插件：整合SSO\nimplementation 'cn.dev33:sa-token-sso:${sa.top.version}'\n\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n\n// Sa-Token插件：权限缓存与业务缓存分离\nimplementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 4.2、新建 Controller 控制器\n\n``` java\n/**\n * Sa-Token-SSO Client端 Controller \n * @author click33\n */\n@RestController\npublic class SsoClientController {\n\n\t// SSO-Client端：首页 \n\t@RequestMapping(\"/\")\n\tpublic String index(HttpServletRequest request) {\n\t\tString url = SaFoxUtil.encodeUrl( SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), request.getQueryString()) );\n\t\tSaSsoClientConfig cfg = SaSsoManager.getClientConfig();\n\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式一)</h2>\" +\n\t\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\t\"<p>\" +\n\t\t\t\t\t\t\"<a href='\" + cfg.splicingAuthUrl() + \"?mode=simple&client=\" + cfg.getClient() + \"&redirect=\" + url + \"'>登录</a> - \" +\n\t\t\t\t\t\t\"<a href='\" + cfg.splicingSignoutUrl() + \"?back=\" + url + \"'>注销</a> \" +\n\t\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\t\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n\t\n}\n```\n\n#### 4.3、application.yml 配置 \n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# 端口\nserver:\n    port: 9001\n\n# Sa-Token 配置 \nsa-token: \n    # SSO-相关配置\n    sso-client:\n        # client 标识\n        client: sso-client1\n        # SSO-Server端主机地址\n        server-url: http://sso.stp.com:9000\n        \n    # 配置 Sa-Token 单独使用的Redis连接（此处需要和 SSO-Server 端连接同一个 Redis）\n    # 注：使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖\n    alone-redis: \n        # Redis数据库索引\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 端口\nserver.port=9001\n\n######### Sa-Token 配置 #########\n\n# client 标识 \nsa-token.sso-client.client=sso-client1\n# SSO-Server端主机地址\nsa-token.sso-client.server-url=http://sso.stp.com:9000\n    \n# 配置 Sa-Token 单独使用的Redis连接（此处需要和 SSO-Server 端连接同一个 Redis）\n# 注：使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖\n# Redis数据库索引\nsa-token.alone-redis.database=1\n# Redis服务器地址\nsa-token.alone-redis.host=127.0.0.1\n# Redis服务器连接端口\nsa-token.alone-redis.port=6379\n# Redis服务器连接密码（默认为空）\nsa-token.alone-redis.password=\n# 连接超时时间\nsa-token.alone-redis.timeout=10s\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 4.4、启动类\n\n``` java\n/**\n * SSO模式一，Client端 Demo \n */\n@SpringBootApplication\npublic class SaSso1ClientApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSso1ClientApplication.class, args);\n\t\t\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式一 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://s1.stp.com:9001\");\n\t\tSystem.out.println(\"测试访问应用端二: http://s2.stp.com:9001\");\n\t\tSystem.out.println(\"测试访问应用端三: http://s3.stp.com:9001\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改hosts文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n}\n```\n\n\n### 5、访问测试\n启动项目，依次访问三个应用端：\n- [http://s1.stp.com:9001/](http://s1.stp.com:9001/)\n- [http://s2.stp.com:9001/](http://s2.stp.com:9001/)\n- [http://s3.stp.com:9001/](http://s3.stp.com:9001/)\n\n\n均返回：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso1--index.png\" alt=\"sso1--index.png\" />\n\n然后点击登录，被重定向至SSO认证中心：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso1--login-page2--v43.png\" alt=\"sso1--login-page2.png\" />\n\n我们登录之后，然后刷新页面：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso1-login-ok.png\" alt=\"sso1-login-ok.png\" />\n\n刷新另外两个Client端，均显示已登录 \n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso1-login-ok2.png\" alt=\"sso1-login-ok2.png\" />\n\n测试完成 \n\n\n\n### 6、跨域模式下的解决方案 \n\n如上，我们使用简单的步骤实现了同域下的单点登录，聪明如你😏，马上想到了这种模式有着一个不小的限制：\n\n> [!TIP| style:callout] \n> 所有子系统的域名，必须同属一个父级域名\n\n如果我们的子系统在完全不同的域名下，我们又该怎么完成单点登录功能呢？\n\n且往下看，[SSO模式二：URL重定向传播会话](/sso/sso-type2)\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-type2.md",
    "content": "# SSO模式二 URL重定向传播会话\n\n如果我们的多个系统：部署在不同的域名之下，但是后端可以连接同一个Redis，那么便可以使用 **`[URL重定向传播会话]`** 的方式做到单点登录。\n\n\n### 1、设计思路\n\n首先我们再次复习一下，多个系统之间为什么无法同步登录状态？\n\n1. 前端的`Token`无法在多个系统下共享。\n2. 后端的`Session`无法在多个系统间共享。\n\n关于第二点，我们已在 \"SSO模式一\" 章节中阐述，使用 [Alone独立Redis插件](/plugin/alone-redis) 做到权限缓存直连 SSO-Redis 数据中心，在此不再赘述。\n\n而第一点，才是我们解决问题的关键所在，在跨域模式下，意味着 \"共享Cookie方案\" 的失效，我们必须采用一种新的方案来传递Token。\n\n1. 用户在 子系统 点击 `[登录]` 按钮。\n2. 用户跳转到子系统登录接口 `/sso/login`，并携带 `back参数` 记录初始页面URL。\n\t- 形如：`http://{sso-client}/sso/login?back=xxx` \n3. 子系统检测到此用户尚未登录，再次将其重定向至SSO认证中心，并携带`redirect参数`记录子系统的登录页URL。\n\t- 形如：`http://{sso-server}/sso/auth?redirect=xxx?back=xxx` \n4. 用户进入了 SSO认证中心 的登录页面，开始登录。\n5. 用户 输入账号密码 并 登录成功，SSO认证中心再次将用户重定向至子系统的登录接口`/sso/login`，并携带`ticket码`参数。\n\t- 形如：`http://{sso-client}/sso/login?back=xxx&ticket=xxxxxxxxx`\n6. 子系统根据 `ticket码` 从 `SSO-Redis` 中获取账号id，并在子系统登录此账号会话。\n7. 子系统将用户再次重定向至最初始的 `back` 页面。\n\n整个过程，除了第四步用户在SSO认证中心登录时会被打断，其余过程均是自动化的，当用户在另一个子系统再次点击`[登录]`按钮，由于此用户在SSO认证中心已有会话存在，\n所以第四步也将自动化，也就是单点登录的最终目的 —— 一次登录，处处通行。\n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/sso/g3--sso2.gif\">加载动态演示图</button>\n\n\n下面我们按照步骤依次完成上述过程：\n\n### 2、准备工作 \n首先修改hosts文件`(C:\\windows\\system32\\drivers\\etc\\hosts)`，添加以下IP映射，方便我们进行测试：\n``` url\n127.0.0.1 sa-sso-server.com\n127.0.0.1 sa-sso-client1.com\n127.0.0.1 sa-sso-client2.com\n127.0.0.1 sa-sso-client3.com\n```\n\n[Some Name](../include/include-qa.md#hostsInvalid ':include')\n\n\n### 3、搭建 Client 端项目 \n\n> [!TIP| label:demo | style:callout] \n> 搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/`，如遇到难点可结合源码进行测试学习\n\n#### 3.1、去除 SSO-Server 的 Cookie 作用域配置 \n在SSO模式一章节中我们打开了配置：\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n    cookie: \n        # 配置 Cookie 作用域 \n        domain: stp.com \n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 配置 Cookie 作用域 \nsa-token.cookie.domain=stp.com\n```\n<!---------------------------- tabs:end ---------------------------->\n\n此为模式一专属配置，现在我们将其注释掉**（一定要注释掉！）**\n\n\n#### 3.2、创建 SSO-Client 端项目\n创建一个 SpringBoot 项目 `sa-token-demo-sso2-client`，引入依赖：\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<!-- Sa-Token 插件：整合SSO -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-sso</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n<dependency>\n\t<groupId>org.apache.commons</groupId>\n\t<artifactId>commons-pool2</artifactId>\n</dependency>\n\n<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-alone-redis</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n\n<!-- Sa-Token 插件：整合 Forest 请求工具 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-forest</artifactId>\n\t<version>${sa-token.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n\n// Sa-Token 插件：整合SSO\nimplementation 'cn.dev33:sa-token-sso:${sa.top.version}'\n\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\nimplementation 'org.apache.commons:commons-pool2'\n\n// Sa-Token插件：权限缓存与业务缓存分离\nimplementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}'\n\n// Sa-Token插件：整合 Forest 请求工具\nimplementation 'cn.dev33:sa-token-forest:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 3.3、创建 SSO-Client 端认证接口\n\n同 SSO-Server 一样，Sa-Token 为 SSO-Client 端所需代码也提供了完整的封装，你只需提供一个访问入口，接入 Sa-Token 的方法即可。\n\n``` java\n\n/**\n * Sa-Token-SSO Client端 Controller \n */\n@RestController\npublic class SsoClientController {\n\n\t// 首页 \n\t@RequestMapping(\"/\")\n\tpublic String index() {\n\t\tString str = \"<h2>Sa-Token SSO-Client 应用端 (模式二)</h2>\" +\n\t\t\t\t\t\"<p>当前会话是否登录：\" + StpUtil.isLogin() + \" (\" + StpUtil.getLoginId(\"\") + \")</p>\" +\n\t\t\t\t\t\"<p> \" +\n\t\t\t\t\t\t\"<a href='/sso/login?back=/'>登录</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/logout?back=self'>全端注销</a> - \" +\n\t\t\t\t\t\t\"<a href='/sso/myInfo' target='_blank'>账号资料</a>\" +\n\t\t\t\t\t\"</p>\";\n\t\treturn str;\n\t}\n\t\n\t/*\n\t * SSO-Client端：处理所有SSO相关请求 \n\t * \t\thttp://{host}:{port}/sso/login\t\t\t-- Client 端登录地址\n\t * \t\thttp://{host}:{port}/sso/logout\t\t\t-- Client 端注销地址（isSlo=true时打开）\n\t * \t\thttp://{host}:{port}/sso/pushC\t\t\t-- Client 端接收消息推送地址\n\t */\n\t@RequestMapping(\"/sso/*\")\n\tpublic Object ssoRequest() {\n\t\treturn SaSsoClientProcessor.instance.dister();\n\t}\n\n\t// 配置SSO相关参数\n\t@Autowired\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t}\n\n\t// 当前应用独自注销 (不退出其它应用)\n\t@RequestMapping(\"/sso/logoutByAlone\")\n\tpublic Object logoutByAlone() {\n\t\tStpUtil.logout();\n\t\treturn SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());\n\t}\n\n}\n```\n\n全局异常处理：\n``` java\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\t// 全局异常拦截 \n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n}\n```\n\n\n##### 3.4、配置SSO认证中心地址 \n你需要在 `application.yml` 配置如下信息：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# 端口\nserver:\n    port: 9002\n\n# sa-token配置 \nsa-token: \n    # 打印操作日志\n    is-log: true\n    # SSO-相关配置\n    sso-client: \n        # 应用标识\n        client: sso-client2\n        # SSO-Server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # API 接口调用秘钥 (单点注销时会用到)\n        secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\n    # 配置 Sa-Token 单独使用的Redis连接（此处需要和 SSO-Server 端连接同一个 Redis）\n    # 注：使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖\n    alone-redis: \n        # Redis数据库索引 (默认为0)\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 端口\nserver.port=9002\n\n######### Sa-Token 配置 #########\n# 打印操作日志 \nsa-token.is-log=true\n# 应用标识 \nsa-token.sso-client.client=sso-client2\n# SSO-Server端 统一认证地址 \nsa-token.sso-client.server-url=http://sa-sso-server.com:9000\n# API 接口调用秘钥 (单点注销时会用到)\nsa-token.sso-client.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\n# 配置 Sa-Token 单独使用的Redis连接（此处需要和 SSO-Server 端连接同一个 Redis）\n# 注：使用 alone-redis 需要在 pom.xml 引入 sa-token-alone-redis 依赖\n# Redis数据库索引\nsa-token.alone-redis.database=1\n# Redis服务器地址\nsa-token.alone-redis.host=127.0.0.1\n# Redis服务器连接端口\nsa-token.alone-redis.port=6379\n# Redis服务器连接密码（默认为空）\nsa-token.alone-redis.password=\n# 连接超时时间\nsa-token.alone-redis.timeout=10s\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n注意点：`sa-token.alone-redis` 的配置需要和SSO-Server端连接同一个Redis**（database 值也要一样！database 值也要一样！database 值也要一样！重说三！）**\n\n#### 3.5、写启动类\n``` java\n@SpringBootApplication\npublic class SaSso2ClientApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(SaSso2ClientApplication.class, args);\n\t\t\n\t\tSystem.out.println();\n\t\tSystem.out.println(\"---------------------- Sa-Token SSO 模式二 Client 端启动成功 ----------------------\");\n\t\tSystem.out.println(\"配置信息：\" + SaSsoManager.getClientConfig());\n\t\tSystem.out.println(\"测试访问应用端一: http://sa-sso-client1.com:9002\");\n\t\tSystem.out.println(\"测试访问应用端二: http://sa-sso-client2.com:9002\");\n\t\tSystem.out.println(\"测试访问应用端三: http://sa-sso-client3.com:9002\");\n\t\tSystem.out.println(\"测试前需要根据官网文档修改hosts文件，测试账号密码：sa / 123456\");\n\t\tSystem.out.println();\n\t}\n}\n```\n启动项目 \n\n\n### 4、测试访问 \n\n(1) 依次启动 `SSO-Server` 与 `SSO-Client`，然后从浏览器访问：[http://sa-sso-client1.com:9002/](http://sa-sso-client1.com:9002/)\n\n（注：先前版本文档测试demo端口号为9001，后为了方便区分三种模式改为了9002，因此出现文字描述与截图端口号不一致情况，请注意甄别，后不再赘述）\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-client-index.png\" alt=\"sso-client-index.png\" />\n\n(2) 首次打开，提示当前未登录，我们点击 **`登录`** 按钮，页面会被重定向到登录中心\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-server-auth--v43.png\" alt=\"sso-server-auth.png\" />\n\n(3) SSO-Server提示我们在认证中心尚未登录，我们点击 **`登录`** 按钮进行模拟登录\n\n<!-- ![sso-server-dologin.png](https://oss.dev33.cn/sa-token/doc/sso/sso-server-dologin.png 's-w-sh') -->\n\n(4) SSO-Server认证中心登录成功，系统重定向回 client\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-client-index-ok.png\" alt=\"sso-client-index-ok.png\" />\n\n(5) 页面被重定向至`Client`端首页，并提示登录成功，至此，`Client1`应用已单点登录成功！ \n\n(6) 我们再次访问`Client2`：[http://sa-sso-client2.com:9002/](http://sa-sso-client2.com:9002/)\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-client2-index.png\" alt=\"sso-client2-index.png\" />\n\n(7) 提示未登录，我们点击 **`登录`** 按钮，会直接提示登录成功\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-client2-index-ok.png\" alt=\"sso-client2-index-ok.png\" />\n\n(8) 同样的方式，我们打开`Client3`，也可以直接登录成功：[http://sa-sso-client3.com:9002/](http://sa-sso-client3.com:9002/)\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-client3-index-ok.png\" alt=\"sso-client3-index-ok.png\" />\n\n至此，测试完毕！\n\n可以看出，除了在`Client1`端我们需要手动登录一次之外，在`Client2端`和`Client3端`都是可以无需再次认证，直接登录成功的。\n\n我们可以通过 F12控制台 Network 跟踪整个过程\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-genzong.png\" alt=\"sso-genzong\" />\n\n<!-- \n### 5、运行官方仓库 \n\n以上示例，虽然完整的复现了单点登录的过程，但是页面还是有些简陋，我们可以运行一下官方仓库的示例，里面有制作好的登录页面\n\n> 下载官方示例，依次运行：\n> - `/sa-token-demo/sa-token-demo-sso2-server/`\n> - `/sa-token-demo/sa-token-demo-sso2-client/`\n> \n> 然后访问：\n> - [http://sa-sso-client1.com:9002/](http://sa-sso-client1.com:9002/)\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/sso/sso-server-login-hua.png\" alt=\"sso-server-login-hua\" />\n\n默认测试密码：`sa / 123456`，其余流程保持不变 \n -->\n \n \n\n### 5、跨 Redis 的单点登录\n以上流程解决了跨域模式下的单点登录，但是后端仍然采用了共享Redis来同步会话，如果我们的架构设计中Client端与Server端无法共享Redis，又该怎么完成单点登录？\n\n这就要采用模式三了，且往下看：[SSO模式三：Http请求获取会话](/sso/sso-type3)\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/sso-type3.md",
    "content": "# SSO模式三 Http请求获取会话\n\n如果既无法做到前端同域，也无法做到后端同Redis，那么可以使用模式三完成单点登录 \n\n> [!WARNING| label:小提示] \n> 阅读本篇之前请务必先熟读SSO模式二！因为模式三仅仅属于模式二的一个特殊场景，熟读模式二有助于您快速理解本章内容\n\n\n### 1、问题分析\n我们先来分析一下，当后端不使用共享 Redis 时，会对架构产生哪些影响：\n\n1. sso-client 端无法直连 Redis 校验 ticket，取出账号id。\n2. sso-client 端无法与 sso-server 端共用一套会话，需要自行维护子会话。\n3. 由于不是一套会话，所以无法“一次注销，全端下线”，需要额外编写代码完成单点注销。\n\n所以模式三的主要目标：也就是在 模式二的基础上 解决上述 三个难题 \n\n> [!TIP| label:demo | style:callout] \n> 模式三的 Demo 示例地址：`/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client/` \n> [源码链接](https://gitee.com/dromara/sa-token/tree/dev/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client)，如遇难点可参考示例 \n\n\n### 2、在Client 端更改 Ticket 校验方式\n\n如果想要更直观的感受模式二与模式三的差距，可以把前面章节创建的模式二 demo 代码复制一份，在新复制的项目上继续更改来测试模式三。\n\n#### 2.1、去除 Alone-Redis 依赖\n\n模式三要求 sso-client 与 sso-server 连接不同的 redis，所以此处没有必要再引入 sa-token-alone-redis 机制，可以去除相关依赖：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token插件：权限缓存与业务缓存分离 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-alone-redis</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token插件：权限缓存与业务缓存分离\nimplementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 2.2、SSO-Client 端更改配置\n\n更改 `application.yml` ：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# 端口\nserver:\n    port: 9003\n\t\n# sa-token配置 \nsa-token:\n    # 打印操作日志\n    is-log: true\n\n    # sso-client 相关配置\n    sso-client:\n        # 应用标识\n        client: sso-client3\n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 使用 Http 请求校验 ticket (模式三)\n        is-http: true\n        # API 接口调用秘钥\n        secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\t\t\nspring: \n    # 配置 Redis 连接 （此处与 SSO-Server 端连接不同的 Redis）\n    redis: \n        # Redis数据库索引\n        database: 3\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间\n        timeout: 10s\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 端口\nserver.port=9003\n\t\n# sa-token配置 \n\n# 打印操作日志\nsa-token.is-log=true\n\n# sso-client 相关配置\n# 应用标识\nsa-token.sso-client.client=sso-client3\n# sso-server 端主机地址\nsa-token.sso-client.server-url=http://sa-sso-server.com:9000\n# 使用 Http 请求校验 ticket (模式三)\nsa-token.sso-client.is-http=true\n# API 接口调用秘钥\nsa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\n# 配置 Redis 连接 （此处与 SSO-Server 端连接不同的 Redis）\n# Redis数据库索引\nspring.redis.database=3\n# Redis服务器地址\nspring.redis.host=127.0.0.1\n# Redis服务器连接端口\nspring.redis.port=6379\n# Redis服务器连接密码（默认为空）\nspring.redis.password=\n# 连接超时时间\nspring.redis.timeout=10s\n```\n<!---------------------------- tabs:end ---------------------------->\n\n<!-- \n#### 2.3、SSO-Client 配置 http 请求处理器\n``` java\n// 配置SSO相关参数\n@Autowired\nprivate void configSso(SaSsoClientConfig ssoClient) {\n\t// 配置Http请求处理器\n\tssoClient.sendHttp = url -> {\n\t\tSystem.out.println(\"------ 发起请求：\" + url);\n\t\tString resStr = Forest.get(url).executeAsString();\n\t\tSystem.out.println(\"------ 请求结果：\" + resStr);\n\t\treturn resStr;\n\t};\n}\n``` -->\n\n\n#### 2.3、测试\n\n重启项目，访问测试：\n- [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/)\n- [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/)\n- [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/)\n\n> [!WARNING| label:小提示] \n> 注：如果已测试运行模式二，可先将Redis中的数据清空，以防旧数据对测试造成干扰\n\n测试步骤同模式二，不再赘述。\n\n\n\n<!-- \n### 3、获取 UserInfo \n除了账号id，我们可能还需要将用户的昵称、头像等信息从 Server端 带到 Client端，即：用户资料的拉取。\n\n在模式二中我们只需要将需要同步的资料放到 SaSession 即可，但是在模式三中两端不再连接同一个 Redis，这时候我们需要通过 http 接口来同步信息。\n\n在旧版本`（<= v1.34.0）` 框架提供的方案是配置 getUserinfo 接口地址，从 client 调用拉取数据，该方案有以下缺点：\n- 每次调用只能传递固定 loginId 一个参数，不方便。\n- 只能拉取 userinfo 数据，不通用。\n- 如果还需要拉取其它业务数据，需要再自定义一个接口，比较麻烦。\n\n为此，我们设计了更通用、灵活的 getData 接口，解决上述三个难题。\n\n#### 3.1、首先在 Server 端开放一个查询数据的接口\n\n``` java\n// 示例：获取数据接口（用于在模式三下，为 client 端开放拉取数据的接口）\n@RequestMapping(\"/sso/getData\")\npublic SaResult getData(String apiType, String loginId) {\n\tSystem.out.println(\"---------------- 获取数据 ----------------\");\n\tSystem.out.println(\"apiType=\" + apiType);\n\tSystem.out.println(\"loginId=\" + loginId);\n\n\t// 校验签名：只有拥有正确秘钥发起的请求才能通过校验\n\tSaSignUtil.checkRequest(SaHolder.getRequest());\n\n\t// 自定义返回结果（模拟）\n\treturn SaResult.ok()\n\t\t\t.set(\"id\", loginId)\n\t\t\t.set(\"name\", \"LinXiaoYu\")\n\t\t\t.set(\"sex\", \"女\")\n\t\t\t.set(\"age\", 18);\n}\n```\n\n> [!WARNING| label:小提示] \n> 如果配置了 “不同 client 不同秘钥” 模式，则需要将上述的： <br>\n> &emsp;&emsp;SaSignUtil.checkRequest(SaHolder.getRequest());  <br>\n> \n> 改为以下方式： <br>\n> &emsp;&emsp;String client = SaHolder.getRequest().getHeader(\"client\"); <br>\n> &emsp;&emsp;SaSsoServerProcessor.instance.ssoServerTemplate.getSignTemplate(client).checkRequest(SaHolder.getRequest()); <br>\n> \n> 如果没有配置 “不同 client 不同秘钥” 模式，则请忽略本条提示。\n\n\n#### 3.2、在 Client 端调用此接口查询数据\n\n在 `SsoClientController` 中新增接口 \n``` java\n// 查询我的账号信息 \n@RequestMapping(\"/sso/myInfo\")\npublic Object myInfo() {\n\t// 组织请求参数\n\tMap<String, Object> map = new HashMap<>();\n\tmap.put(\"apiType\", \"userinfo\");\n\tmap.put(\"loginId\", StpUtil.getLoginId());\n\n\t// 发起请求\n\tObject resData = SaSsoUtil.getData(map);\n\tSystem.out.println(\"sso-server 返回的信息：\" + resData);\n\treturn resData;\n}\n```\n\n#### 3.3、访问测试\n访问测试：[http://sa-sso-client1.com:9001/sso/myInfo](http://sa-sso-client1.com:9001/sso/myInfo)\n\n\n### 4、自定义接口通信\n\n上述示例展示在 client 端向 server 拉取 userinfo 数据的步骤，如果你还需要拉取其它业务的数据，稍加改造示例便可以实现。\n\n#### 4.1、方式一，使用 apiType 参数来区分业务\n\n我们可以约定好，使用 apiType 来区分不同的业务，例如：\n- 当 `apiType=userinfo` 时：代表拉取用户资料。\n- 当 `apiType=followList` 时：代表拉取用户的关注列表。\n- 当 `apiType=fansList` 时：代表拉取用户的粉丝列表。\n\n此时，我们便可以通过在 client 端传入不同的 apiType 参数，来区分不同的业务。\n\n``` java\n// 查询我的账号信息 \n@RequestMapping(\"/sso/myFollowList\")\npublic Object myFollowList() {\n\t// 组织请求参数\n\tMap<String, Object> map = new HashMap<>();\n\tmap.put(\"apiType\", \"followList\");  // 关键代码，代表本次我要拉取关注列表\n\tmap.put(\"loginId\", StpUtil.getLoginId());\n\n\t// 发起请求\n\tObject resData = SaSsoUtil.getData(map);\n\tSystem.out.println(\"sso-server 返回的信息：\" + resData);\n\treturn resData;\n}\n```\n\n然后在 server 端我们通过不同的 apiType 值，返回不同的信息即可。\n\n\n#### 4.2、方式二：直接在调用接口时传入一个自定义 path \n\n我们可以 client 端，调用 `SaSsoUtil.getData` 方法时，传入一个自定义 path，例如：\n\n``` java\n// 查询我的账号信息 \n@RequestMapping(\"/sso/myFansList\")\npublic Object myFansList() {\n\t// 组织请求参数\n\tMap<String, Object> map = new HashMap<>();\n\t// map.put(\"apiType\", \"userinfo\");   // 此时已经不需要 apiType 参数了 \n\tmap.put(\"loginId\", StpUtil.getLoginId());\n\n\t// 发起请求 （传入自定义的 path 地址）\n\tObject resData = SaSsoUtil.getData(\"/sso/getFansList\", map);\n\tSystem.out.println(\"sso-server 返回的信息：\" + resData);\n\treturn resData;\n}\n```\n\n同时，我们需要在 server 端开放这个自定义的 `/sso/getFansList` 接口：\n\n``` java\n// 获取指定用户的粉丝列表\n@RequestMapping(\"/sso/getFansList\")\npublic Object getFansList(Long loginId) {\n\tSystem.out.println(\"---------------- 获取 loginId=\" + loginId + \" 的粉丝列表 ----------------\");\n\n\t// 校验签名：只有拥有正确秘钥发起的请求才能通过校验\n\tSaSignUtil.checkRequest(SaHolder.getRequest());\n\n\t// 查询数据 (此处仅做模拟)\n\tList<Integer> list = Arrays.asList(10041, 10042, 10043, 10044);\n\n\t// 返回\n\treturn list;\n}\n```\n\n#### 4.3、访问测试\n访问测试：[http://sa-sso-client1.com:9001/sso/myFansList](http://sa-sso-client1.com:9001/sso/myFansList)\n -->\n\n\n### 3、后记\n当我们熟读三种模式的单点登录之后，其实不难发现：所谓单点登录，其本质就是多个系统之间的会话共享。\n\n当我们理解这一点之后，三种模式的工作原理也浮出水面：\n\n- 模式一：采用共享 Cookie 来做到前端 Token 的共享，从而达到后端的 Session 会话共享。\n- 模式二：采用 URL 重定向，以 ticket 码为授权中介，做到多个系统间的会话传播。\n- 模式三：采用 Http 请求主动查询会话，做到 Client 端与 Server 端的会话同步。\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/sso/user-data-sync.md",
    "content": "# 用户数据同步 / 迁移\n\n本篇文章仅提供架构设计的略微参考，真实场景中每个公司的架构设计都是千差万别的，一套设计理论未必能够适应所有公司的项目。\n所以如果你觉着本篇文章的设计理念不能契合你公司的需求，请以你公司的原设计为准。\n\n--- \n\n### 数据同步需求\n\n在前面的不同架构 SSO 对接示例中，我们均假设了一个前提：\n\n-- _所有的 sso-client 只负责业务操作，不存储 user 数据，user 数据全部来源于 sso-server，包括登录认证也都是基于 sso-server 里的 user 账号进行校验操作。_\n\n这种架构比较简洁、清晰，是一种理想化的 SSO 架构模型。\n\n然而更多时候，我们遇到的实际情况是：\n\n-- _公司已经有了 N 多个系统，每个系统都有自己独立的一套账号认证体系，现在老板要让这 N 个毫无关系的系统集成单点登录。_\n\n要完成这种需求，首先你得考虑两个问题：\n1. 问题一：sso-client 需不需要保留 user 数据。\n\t- sso-client 不涉及 user 信息连表查的业务，就可以不保留 user 信息。\n\t- sso-client 涉及 user 信息连表查业务，就需要在 sso-client 保留 user 数据。\n2. 问题二：如果保留的话，是和 sso-server 强同步，还是弱同步。\n\t- 强同步就是指 sso-client 的 user 数据和 sso-server 的 user 数据，字段值必须保持一致。比如说：一个用户在server端昵称修改为“张三”，那么在 client 端也要实时同步修改。\n\t- 弱同步就是指两边可以各改各的。比如说：一个用户 server 端修改了昵称为 “张三”，他在 client 端依然可以昵称为 “李四”。\n\n\n由此可大致分为三种设计方案：\n\n| 方案序号\t| 方案名称\t| 简单说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|  适用系统\t\t\t\t\t\t\t\t\t|\n| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t| :--------\t\t\t\t\t\t\t\t\t|\n| 方案一\t \t| 统一迁移\t| 统一把用户数据迁移到 sso-server 认证中心再进行对接\t\t\t\t\t| 比较简单的系统，业务上不需要 user 信息连表查\t|\n| 方案二\t \t| 实时同步\t| 按照一定的规则，使 sso-client 和 sso-server 保持 user 信息实时同步 \t| 一般业务上需要 user 信息连表查的系统\t\t\t|\n| 方案三\t \t| 字段关联\t| 不同步，但找一个关键字段，将 sso-client 和 sso-server 的 user 账号进行关联起来\t| sso-client 不打算过分依赖 sso-server 的 user 数据，只是想借助 sso-server 完成一下统一登录\t\t|\n\n下面逐一拆解三种方案具体实现。\n\n\n### 1、方案一：统一迁移\n\n对接工作开发前，sso-client 的 user 数据完全迁移到 sso-server 中，且自身不再保留 user 数据，只进行业务数据处理操作。\n\n这种方案其实不必过多讲解，因为数据完成迁移后整个架构就转化为了上述的“理想化SSO模型”，后续对接也比较方便。\n迁移方式可以选择数据库同步工具，或者手写代码从 sso-client 库读取数据然后 insert 到 sso-server 库中。\n这并非此文探讨的重点，因此不再过多赘述了。\n\n- 方案优点：架构简洁明了，SSO 登录、注销对接起来非常方便\n- 方案缺点：sso-client 不存储 user 信息，因此业务上需要连表查询 user 信息的地方会比较麻烦（例如：拉取帖子列表时需要附加显示用户头像和昵称信息）\n\n方案适用范围：适合业务比较简单，不涉及 user资料连表查业务 的子系统。\n\n\n### 2、方案二：实时同步\n\n首先，对接前，数据还是要迁移的，只不过迁移后 sso-client 的 user 数据不删除掉，依然保留。\n\n然后在项目运行阶段，每当 sso-server 的 user 数据发生变动时（增删改），逐一向每个 sso-client 推送变化信息。使 sso-client 与 sso-server 的 user 数据保持强同步。\n\n你可能会有疑问，那 sso-client 的 user 数据发生变动时，要不要向 sso-server 推送信息，我的建议是：尽量不要让 sso-client 的 user 信息主动发生变化。\n\n举个例子：\n\n> 公司有电商、论坛、短视频 3 个子系统 + 1 个 sso-server 认证中心，无论用户从哪个子系统点击 “修改我的资料” 按钮时，都应该统一跳转到 sso-server 认证中心进行修改，\n> 修改完毕后再由 sso-server 将 user 信息推送至 3 个子系统。以此来保证 4 个系统间的 user 信息同步。\n\n- 方案优点：sso-client 存储了 user 信息，可以比较方便的进行 user 连表查操作。\n- 方案缺点：sso-server 与 sso-client 的 user 数据同步功能不算简单，开发起来可能要耗费一段不小的工期。\n\n方案适用范围：一般业务上需要 user 信息连表查的子系统都适合。\n\n\n### 3、方案三：字段关联\n\n如果子系统不需要和 sso-server 做到信息强同步，可以使用字段关联法做到账户关联进行登录。\n\n举个例子：公司有三个子系统，电商、论坛、短视频。同一个用户可以在这三个子系统以及 sso-server 认证中心拥有不同的昵称、头像等信息，互不干扰。\n\n例如，在 sso-server 认证中心里，张三的数据库信息为：\n\n| id\t\t| username\t| avatar \t| password \t\t| age \t\t\t| email\t\t\t\t|\n| :--------\t| :--------\t| :--------\t| :--------\t\t| :--------\t\t| :--------\t\t\t|\n| 10001\t\t| ...\t\t| ...\t\t| ...\t\t\t| ...\t\t\t| ...\t\t\t\t|\n| 10002\t\t| 小明\t\t| cat.jpg\t| 123456\t\t| 18 \t\t\t| `23397@xx.com`\t\t|\n| 10003\t\t| ...\t\t| ...\t\t| ...\t\t\t| ...\t\t\t| ...\t\t\t\t|\n\n在电商系统里中，张三的数据库信息为：\n\n| id\t\t\t| name\t\t| avatar \t\t| money\t\t\t| email\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t| :--------\t\t| :--------\t\t\t|\n| 100334\t\t| ...\t\t| ...\t\t\t| ...\t\t\t| ...\t\t\t\t|\n| 100335\t\t| 二明\t\t| dog.jpg\t\t| 1000 \t\t\t| `23397@xx.com`\t\t|\n| 100336\t\t| ...\t\t| ...\t\t\t| ...\t\t\t| ...\t\t\t\t|\n\n这里的关键点在于，虽然用户 “张三” 在每个系统里的资料都是不同的，但是程序要想办法将它们识别为同一个用户，\n要做到这一点，就需要我们准备一个关键字段将信息打通串联起来。例如表中的 “邮箱” 信息可以作为这个“关联字段”。\n\n（注：此处仅展示使用邮箱作为关联字段的操作，实际上除了邮箱以外，手机号、身份证号等具有唯一性的信息都可以作为关联字段）\n\n首先，在 sso-server 端，我们需要重写一下 `checkTicketAppendData` 函数，使其在 “校验 ticket 返回 loginId” 时，追加返回 email 字段。\n\n```  java \n// 配置SSO相关参数 \n@Autowired\nprivate void configSso(SaSsoServerTemplate ssoServerTemplate) {\n\t\n\t// 其它配置 ...\n\t\n\t// 配置：Ticket校验函数\n\tssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> {\n\t\tSystem.out.println(\"-------- 追加返回信息到 sso-client --------\");\n\n\t\t// 在校验 ticket 后，给 sso-client 端追加返回信息的函数\n\t\tSysUser user = sysUserMapper.getById(loginId);\n\t\tresult.set(\"email\", user.getEmail());\n\t\t// result.set(\"user\", user);  // 你也可以将整个user 对象的信息都返回到 sso-client，自由决定\n\n\t\treturn result;\n\t};\n\t\n}\n```\n\n在 sso-client 端，重写 ticketResultHandle 函数，根据 sso-server 返回的信息查询本地 user 信息并登录：\n\n``` java\n// 配置SSO相关参数 \n@Autowired\nprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t// 其它配置 ... \n\n\t// 自定义校验 ticket 返回值的处理逻辑 （每次从认证中心获取校验 ticket 的结果后调用）\n\tssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> {\n\t\tSystem.out.println(\"--------- 自定义 ticket 校验结果处理函数 ---------\");\n\t\tSystem.out.println(\"此账号在 sso-server 的 userId：\" + ctr.loginId);\n\t\tSystem.out.println(\"此账号在 sso-server 会话剩余有效期：\" + ctr.remainSessionTimeout + \" 秒\");\n\t\tSystem.out.println(\"此账号返回的 email 信息：\" + ctr.result.get(\"email\"));\n\n\t\t// 模拟代码：\n\t\t// 根据 email 字段找到此账号在本系统对应的 user 信息\n\t\tString email = (String) ctr.result.get(\"email\");\n\t\tSysUser user = sysUserMapper.getByEmail(email);\n\n\t\t// 如果找不到，说明是首次登录本系统的新用户，需要自动注册一个新账号给他\n\t\tif(user == null) {\n\t\t\t// 涉及到数据库操作，此处仅做模拟代码\n\t\t\t// 1、构建 user 信息\n\t\t\t// 2、插入到数据库\n\t\t\t// 3、查询出最新刚插入的这条 user 信息\n\t\t\tuser = sysUserMapper.getByEmail(email);\n\t\t}\n\n\t\t// 进行登录\n\t\tStpUtil.login(user.getId(), ctr.remainSessionTimeout);\n\t\tStpUtil.getSession().set(\"user\", user);\n\n\t\t// 一切工作完毕，重定向回 back 页面\n\t\treturn SaHolder.getResponse().redirect(back);\n\t};\n\n}\n```\n\n至此完毕。\n\n- 方案优点：\n\t- 1、sso-client 不需要和 sso-server 保持信息强同步，实现起来不复杂，架构也比较清晰易维护。\n\t- 2、同一个用户的信息，sso-client 可以和 sso-client 保持不同，各自维护各自的，互不干扰。\n- 方案缺点：好像没啥缺点，除非你觉着上述的第2条优点属于缺点。\n\n方案适用范围：在 user 信息方面不打算过分依赖 sso-server 的系统，希望自己维护自己的 user 信息，只是想借助 sso-server 完成一下统一登录。\n\n\n### 4、扩展：没有关联字段\n\n如果我们的子系统 user 表没有邮箱、手机号等唯一性字段和 sso-server 的 user 表进行关联，该怎么办呢？\n\n没有字段，那就创造个字段，例如：\n\n| id\t\t\t| name\t\t| avatar \t\t| age\t\t\t| center_id\t\t\t|\n| :--------\t\t| :--------\t| :--------\t\t| :--------\t\t| :--------\t\t\t|\n| 205421\t\t| ...\t\t| ...\t\t\t| ...\t\t\t| ...\t\t\t\t|\n| 205422\t\t| 小风筝\t\t| dog.jpg\t\t| 21 \t\t\t| 10002\t\t\t\t|\n| 205423\t\t| ...\t\t| ...\t\t\t| ...\t\t\t| ...\t\t\t\t|\n\n如上表所示，我们可以在子系统的 user 表新增一列 `center_id`，记录这个用户在认证中心所属的账号id。然后在登录时根据这个 `center_id` 来查找相应的用户。 \n\n由于 sso-server 端默认就是会返回 loginId 参数的，因此在 sso-server 端不必再重写一下 `checkTicketAppendData` 函数来追加返回信息了，\n我们只需要重写 sso-client 端的 `ticketResultHandle` 函数即可：\n\n``` java\n// 配置SSO相关参数 \n@Autowired\nprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\n\t// 其它配置 ... \n\n\t// 自定义校验 ticket 返回值的处理逻辑 （每次从认证中心获取校验 ticket 的结果后调用）\n\tssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> {\n\t\tSystem.out.println(\"--------- 自定义 ticket 校验结果处理函数 ---------\");\n\t\tSystem.out.println(\"此账号在 sso-server 的 userId：\" + ctr.loginId);\n\t\tSystem.out.println(\"此账号在 sso-server 会话剩余有效期：\" + ctr.remainSessionTimeout + \" 秒\");\n\n\t\t// 模拟代码：\n\t\t// 根据 center_id 字段找到此账号在本系统对应的 user 信息\n\t\tlong centerId = SaFoxUtil.getValueByType(ctr.loginId, long.class);\n\t\tSysUser user = sysUserMapper.getByCenterId(centerId);\n\n\t\t// 如果找不到，说明是首次登录本系统的新用户，需要自动注册一个新账号给他\n\t\tif(user == null) {\n\t\t\t// 涉及到数据库操作，此处仅做模拟\n\t\t\t// 1、构建 user 信息\n\t\t\t// 2、插入到数据库\n\t\t\t// 3、查询出最新刚插入的这条 user 信息\n\t\t\tuser = sysUserMapper.getByCenterId(userId);\n\t\t}\n\n\t\t// 进行登录\n\t\t//     注意此处需要使用 centerId 进行登录，否则该账号将无法正常完成单点注销功能 \n\t\tStpUtil.login(centerId, ctr.remainSessionTimeout);\n\t\tStpUtil.getSession().set(\"user\", user);\n\n\t\t// 一切工作完毕，重定向回 back 页面\n\t\treturn SaHolder.getResponse().redirect(back);\n\t};\n\n}\n```\n\n至此完毕。\n\n\n> [!INFO| label:提问：按照方案三，一个用户登录过程中，sso-server 和 sso-client 对这个用户账号的完整处理步骤是怎样的？] \n> 1. 用户进入 sso-client 登录页面，点击上面的 [ 使用 xx 认证中心快捷登录 ] 按钮，浏览器跳转至 sso-server 认证中心。\n> 2. 如果用户在 sso-server 有账号，则直接登录，如果没有，则注册账号并登录。\n> 3. sso-server 重定向回 sso-client 端，并携带 ticket 参数。\n> 4. sso-client 获取 ticket 参数，并解析出 center_id 值。\n> 5. 根据 center_id 从 user 表查数据：\n> \t- 5.1 查的到，证明有账号，直接登录。\n> \t- 5.2 查不到，证明无账号，程序自动给他添加一条 user 账号，并登录。\n> 6. 登录完成。\n\n\n\n### 5、解决模式三下，loginId 与 centerId 不一致的问题\n\n按照字段关联法登录之后，如果一个用户在本地应用端的 userId 和认证中心端的 userId 不一致，则可能发生单点注销失败的情况：\n\n假设，一个用户在认证中心的 userId=10002，在本地应用端的 userId=100335，\n则在本地应用端发起单点注销时，其传递的 loginId 值是 100335，在 sso-server 是找不到 userId=100335 用户的，自然无法单点注销成功。\n\n解决方案是在本地应用端重写 loginId 与 centerId 转换策略函数，做到本地应用 userId 与认证中心 userId 的互相映射：\n\n``` java\n@RestController\npublic class SsoClientController {\n\n\t// 配置SSO相关参数\n\t@Autowired\n\tprivate void configSso(SaSsoClientTemplate ssoClientTemplate) {\n\t\t// 重写 loginId 与 centerId 转换策略函数，做到本地应用 userId 与认证中心 userId 的互相映射\n\t\t\n\t\t// 将 centerId 转换为 loginId 的函数\n\t\tssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> {\n\t\t\treturn \"Stu\" + centerId;\n\t\t};\n\t\t// 将 loginId 转换为 centerId 的函数\n\t\tssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> {\n\t\t\treturn loginId.toString().substring(3);\n\t\t};\n\t}\n\n}\n```\n\n如上代码，演示了应用本地 loginId 与认证中心 centerId 不一致时的转换写法（演示的逻辑为添加和裁剪指定前缀），真实项目中，应该根据用户表存储的映射关系来做查询返回。\n\n值得注意的是，在重写转换策略后，我们在消息推送时也应该严格按照转换写法提交 loginId 参数，例如：\n\n``` java\n// 查询我的账号信息：sso-client 前端 -> sso-center 后端 -> sso-server 后端\n@RequestMapping(\"/sso/myInfo\")\npublic Object myInfo() {\n\t// 如果尚未登录\n\tif( ! StpUtil.isLogin()) {\n\t\treturn \"尚未登录，无法获取\";\n\t}\n\n\t// 原写法：直接调用 StpUtil.getLoginId() 当做 centerId 来提交\n\t// Object centerId = StpUtil.getLoginId();\n\n\t// 新写法：获取本地 loginId 对应的认证中心 centerId\n\tObject centerId = SaSsoClientUtil.getSsoTemplate().strategy.convertLoginIdToCenterId.run(StpUtil.getLoginId());\n\n\t// 推送消息\n\tSaSsoMessage message = new SaSsoMessage();\n\tmessage.setType(\"userinfo\");\n\tmessage.set(\"loginId\", centerId);\n\tSaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);\n\n\t// 返回给前端\n\treturn result;\n}\n```"
  },
  {
    "path": "sa-token-doc/start/download.md",
    "content": "# 其它环境引入 Sa-Token 的示例\n\n目前已实现的对接框架综合\n\n------\n\n## Maven依赖 \n根据不同基础框架引入不同的 Sa-Token 依赖：\n\n<!------------------------------ tabs:start ------------------------------>\n\n<!------------- tab:SpringBoot环境 （ServletAPI）  ------------->\n如果你使用的框架基于 ServletAPI 构建（ SpringMVC、SpringBoot等 ），请引入此包\n``` xml\n<!-- Sa-Token 权限认证, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n\n<!------------- tab:WebFlux环境 （Reactor）  ------------->\n注：如果你使用的框架基于 Reactor 模型构建（WebFlux、SpringCloud Gateway 等），请引入此包\n``` xml\n<!-- Sa-Token 权限认证（Reactor响应式集成）, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n\n<!------------- tab:Solon 集成  ------------->\n参考：[Solon官网](https://solon.noear.org/)\n``` xml\n<!-- Sa-Token 整合 Solon, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-solon-plugin</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n\n<!------------- tab:JFinal 集成  ------------->\n参考：[JFinal官网](https://jfinal.com/)\n``` xml\n<!-- Sa-Token 整合 JFinal, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-jfinal-plugin</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n\n<!------------- tab:Jboot 集成  ------------->\n参考：[Jboot官网](http://www.jboot.com.cn/)\n``` xml\n<!-- Sa-Token 整合 Jboot, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-jboot-plugin</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n\n<!------------- tab:LoveQQ-Framework 集成  ------------->\n参考：[LoveQQ-Framework](https://gitee.com/kfyty725/loveqq-framework)\n``` xml\n<!-- Sa-Token 整合 LoveQQ-Framework, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-loveqq-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n\n<!------------- tab:Quarkus 集成  ------------->\n参考：[quarkus-sa-token](https://github.com/quarkiverse/quarkus-sa-token)\n``` xml\n<!-- Sa-Token 整合 Quarkus, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>io.quarkiverse.satoken</groupId>\n\t<artifactId>quarkus-satoken-resteasy</artifactId>\n\t<version>1.30.0</version>\n</dependency>\n```\n\n<!------------- tab:裸Servlet容器环境   ------------->\n注：如果你的项目没有使用Spring，但是Web框架是基于 ServletAPI 规范的，可以引入此包\n``` xml\n<!-- Sa-Token 权限认证（ServletAPI规范）, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-servlet</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n引入此依赖需要自定义 SaTokenContext 实现，参考：[自定义 SaTokenContext 指南](/fun/sa-token-context)\n\n<!------------- tab:其它   ------------->\n注：如果你的项目既没有使用 SpringMVC、WebFlux，也不是基于 ServletAPI 规范，那么可以引入core核心包\n``` xml\n<!-- Sa-Token 权限认证（core核心包）, 在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-core</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n引入此依赖需要自定义 SaTokenContext 实现，参考：[自定义 SaTokenContext 指南](/fun/sa-token-context)\n\n<!---------------------------- tabs:end ------------------------------>\n\n\n## Gradle依赖\n<!-- tabs:start -->\n<!-- tab:SpringBoot环境 （ServletAPI）  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n\n<!-- tab:WebFlux环境 （Reactor）  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n\n<!-- tab:Solon 集成  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-solon-plugin:${sa.top.version}'\n```\n\n<!-- tab:JFinal 集成  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-jfinal-plugin:${sa.top.version}'\n```\n\n<!-- tab:Jboot 集成  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-jboot-plugin:${sa.top.version}'\n```\n\n<!-- tab:LoveQQ-Framework 集成  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-loveqq-boot-starter:${sa.top.version}'\n```\n\n<!-- tab:Quarkus 集成  -->\n``` gradle\nimplementation 'io.quarkiverse.satoken:quarkus-satoken-resteasy:1.30.0'\n```\n\n<!-- tab:裸Servlet容器环境  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-servlet:${sa.top.version}'\n```\n\n<!-- tab:其它  -->\n``` gradle\nimplementation 'cn.dev33:sa-token-core:${sa.top.version}'\n```\n\n<!-- tabs:end -->\n\n注：JDK版本：`v1.8+`，SpringBoot：`建议2.0以上`\n\n\n## 测试版\n更多内测版本了解：[Sa-Token 最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md)\n\nMaven依赖一直无法加载成功？[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull)\n\n## jar包下载\n<!-- [点击下载：sa-token-1.6.0.jar](https://oss.dev33.cn/sa-token/sa-token-1.6.0.jar) -->\n[点击下载：sa-token-1.6.0.jar](https://pan.quark.cn/s/85e4d75f500c)\n\n注：当前仅提供 `v1.6.0` 版本jar包下载，更多版本请前往 maven 中央仓库获取，[直达链接](https://search.maven.org/search?q=sa-token)\n\n\n## 获取源码\n如果你想深入了解 Sa-Token，你可以通过`Gitee`或者`GitHub`来获取源码 （**学习测试请拉取 master 分支**，dev为正在开发的分支，有很多特性并不稳定）\n- **Gitee**地址：[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token)\n- **GitHub**地址：[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token)\n- 开源不易，求鼓励，点个`star`吧\n- 源码目录介绍: - [仓库目录](/arch/dir-intro)\n\n\n\n\n\n## 运行示例\n\n- 1、下载代码（学习测试用 master 分支）。\n- 2、从根目录导入项目。\n- 3、选择相应的示例添加为 Maven 项目，打开 XxxApplication.java 运行。\n\n<img src=\"/big-file/doc/start/import-demo-run.png\" alt=\"运行示例\" title=\"s-w-sh\">"
  },
  {
    "path": "sa-token-doc/start/example.md",
    "content": "# SpringBoot 集成 Sa-Token 示例\n\n\n本篇带你从零开始集成 Sa-Token，只需简单 5 步，你就可以快速熟悉框架的使用姿势。\n\n整合示例在官方仓库的`/sa-token-demo/sa-token-demo-springboot`文件夹下，如遇到难点可结合源码进行学习测试。\n\n---\n\n### 1、创建项目\n在 IDE 中新建一个 SpringBoot 项目，例如：`sa-token-demo-springboot`（不会的同学请自行百度或者参考：[SpringBoot-Pure](https://gitee.com/click33/springboot-pure)）\n\n\n### 2、添加依赖\n在项目中添加依赖：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证，在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。\n<!---------------------------- tabs:end ---------------------------->\n\n\nMaven依赖一直无法加载成功？[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull)\n\n更多内测版本了解：[Sa-Token最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md)\n\n### 3、设置配置文件\n你可以**零配置启动项目** ，但同时你也可以在 `application.yml` 中增加如下配置，定制性使用框架：\n\n<!---------------------------- tabs:start ---------------------------->\n\n<!------------- tab:application.yml 风格  ------------->\n``` yaml\nserver:\n\t# 端口\n    port: 8081\n\t\n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n\t# token 名称（同时也是 cookie 名称）\n\ttoken-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n\ttimeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\tactive-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\tis-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n\tis-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n\ttoken-style: uuid\n    # 是否输出操作日志 \n\tis-log: true\n```\n\n<!------------- tab:application.properties 风格  ------------->\n``` properties\n# 端口\nserver.port=8081\n\t\n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\n\n# token 名称（同时也是 cookie 名称）\nsa-token.token-name=satoken\n# token 有效期（单位：秒） 默认30天，-1 代表永久有效\nsa-token.timeout=2592000\n# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\nsa-token.active-timeout=-1\n# 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\nsa-token.is-concurrent=true\n# 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\nsa-token.is-share=false\n# token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\nsa-token.token-style=uuid\n# 是否输出操作日志 \nsa-token.is-log=true\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n### 4、创建启动类\n在项目中新建包 `com.pj` ，在此包内新建主类 `SaTokenDemoApplication.java`，复制以下代码：\n\n``` java\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\tpublic static void main(String[] args) throws JsonProcessingException {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args);\n\t\tSystem.out.println(\"启动成功，Sa-Token 配置如下：\" + SaManager.getConfig());\n\t}\n}\n```\n\n### 5、创建测试Controller\n``` java\n@RestController\n@RequestMapping(\"/user/\")\npublic class UserController {\n\n\t// 测试登录，浏览器访问： http://localhost:8081/user/doLogin?username=zhang&password=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic String doLogin(String username, String password) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(username) && \"123456\".equals(password)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn \"登录成功\";\n\t\t}\n\t\treturn \"登录失败\";\n\t}\n\n\t// 查询登录状态，浏览器访问： http://localhost:8081/user/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic String isLogin() {\n\t\treturn \"当前会话是否登录：\" + StpUtil.isLogin();\n\t}\n\t\n}\n```\n\n### 6、运行\n启动代码，从浏览器依次访问上述测试接口：\n\n<img src=\"/big-file/doc/start/test-do-login.png\" alt=\"运行结果\">\n\n<img src=\"/big-file/doc/start/test-is-login.png\" alt=\"运行结果\">\n\n<!-- \n### 普通Spring环境\n普通spring环境与springboot环境大体无异，只不过需要在项目根目录手动创建配置文件`sa-token.properties`来完成配置 \n-->\n\n\n### 出发\n通过这个示例，你已经对 Sa-Token 有了初步的了解。那么，坐稳扶好，让我们开始吧：[登录认证](/use/login-auth) \n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/start/maven-pull.md",
    "content": "# Maven 依赖一直无法拉取成功？\n\n--- \n方法1、先重启一下试试。\n\n--- \n方法2、可能依赖还没有下载完毕，请看一下编辑器下方是否有正在构建项目的进度条。\n\n--- \n方法3、可能是网络不太稳定，导致本地下载了一些残碎文件，先把这些残碎文件删除了，再重新构建项目试试。\n\n一般本地的文件都在 `C:\\Users\\你的电脑用户名\\.m2\\repository\\cn\\dev33`，打开后，把文件全部删除。注：如果你修改过 Maven jar 下载目录，就按照你修改的来。\n\n--- \n方法4、可能你给你的 Maven 配置了阿里云镜像，而部分 jar 包无法通过阿里云镜像加载成功。\n\n打开你的 Maven setting.xml 文件，看看有没有以下配置：\n\n``` xml\n<mirror>\n\t<id>nexus-aliyun</id>\n\t<mirrorOf>central</mirrorOf>\n\t<name>Nexus aliyun</name>\n\t<url>http://maven.aliyun.com/nexus/content/groups/public</url> \n</mirror>\n```\n\n如果有的话，先把它注释掉（注释掉就直连 Maven 中央仓库了），或者修改为其它的镜像，例如腾讯云的：\n\n``` xml\n<mirror> \n\t<id>tencent</id> \n\t<name>tencent maven</name> \n\t<url>http://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url>\n\t<mirrorOf>central</mirrorOf> \n</mirror>\n```\n\n然后重启你的代码编辑器，重新构建项目。\n\n--- --- \n方法5、如果使用的是父子Maven项目，在父项目导入该依赖后,Pom无法识别的情况：\n\n需要先在子项目中引用该依赖，再进行重新加载。\n\n若还是不行，可以新建先一个小的Maven项目尝试将该依赖下载后，再返回原父子项目中将该依赖导入。\n\n--- --- \n再不行的话，就加群反馈吧。\n"
  },
  {
    "path": "sa-token-doc/start/new-version.md",
    "content": "# Sa-Token 最新版本\n\n在线文档：[https://sa-token.cc/](https://sa-token.cc/)\n\n--- \n\n### 正式版本 \nv1.45.0 正式版，可上生产：\n\n``` xml\n<!-- Sa-Token 权限认证 -->\n<dependency>\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-spring-boot-starter</artifactId>\n    <version>1.45.0</version>\n</dependency>\n```\n\nMaven依赖一直无法加载成功？[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull)\n\n--- \n\n### 内测版本\n\n暂无内测版本。\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/start/solon-example.md",
    "content": "# Solon 集成 Sa-Token 示例\n\n本篇介绍在 Solon 应用中如何集成 Sa-Token。\n\n整合示例在官方仓库的 `/sa-token-demo/sa-token-demo-solon` 文件夹下，如遇到难点可结合源码进行学习测试。\n\n> [!tip| label:Solon 是什么？] \n> Solon 是一个高效的国产应用开发框架：更快、更小、更简单。\n> \n> - 启动快 5 ～ 10 倍；\n> - qps 高 2～ 3 倍；\n> - 运行时内存节省 1/3 ~ 1/2；\n> - 打包可以缩到 1/2 ~ 1/10；\n> - 同时支持 jdk8、jdk11、jdk17、jdk20。\n> \n> 详情可参考：[https://solon.noear.org/](https://solon.noear.org/)\n\n---\n\n### 1、创建项目\n\n在 IDE 中新建一个 Solon 项目，例如：sa-token-demo-solon （可以借助 [Solon Initializr](https://solon.noear.org/start/) 生成） \n\n### 2、添加依赖\n\n在项目中添加依赖：\n\n<!---------------------------- tabs:start ---------------------------->\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证，在线文档：https://sa-token.cc -->\n<dependency>\n    <groupId>cn.dev33</groupId>\n    <artifactId>sa-token-solon-plugin</artifactId>\n    <version>${sa.top.version}</version>\n</dependency>\n```\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证，在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-solon-plugin:${sa.top.version}'\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n\nMaven依赖一直无法加载成功？[参考解决方案](https://sa-token.cc/doc.html#/start/maven-pull)\n\n更多内测版本了解：[Sa-Token最新版本](https://gitee.com/dromara/sa-token/blob/dev/sa-token-doc/start/new-version.md)\n\n\n\n### 3、设置配置文件\n\n你可以**零配置启动项目** ，但同时你也可以在 `app.yml` 中增加如下配置，定制性使用框架：\n\n<!---------------------------- tabs:start ---------------------------->\n\n<!------------- tab:app.yml 风格  ------------->\n\n```yaml\nserver:\n    # 端口\n    port: 8081\n    \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n\t# token 名称（同时也是 cookie 名称）\n\ttoken-name: satoken\n\t# token 有效期（单位：秒） 默认30天，-1 代表永久有效\n\ttimeout: 2592000\n\t# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\tactive-timeout: -1\n\t# 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\tis-concurrent: true\n\t# 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n\tis-share: false\n\t# token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n\ttoken-style: uuid\n\t# 是否输出操作日志 \n\tis-log: true\n```\n\n<!------------- tab:app.properties 风格  ------------->\n```properties\n# 端口\nserver.port=8081\n    \n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\n\n# token 名称（同时也是 cookie 名称）\nsa-token.token-name=satoken\n# token 有效期（单位：秒） 默认30天，-1 代表永久有效\nsa-token.timeout=2592000\n# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\nsa-token.active-timeout=-1\n# 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\nsa-token.is-concurrent=true\n# 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\nsa-token.is-share=false\n# token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\nsa-token.token-style=uuid\n# 是否输出操作日志 \nsa-token.is-log=true\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n\n### 4、创建启动类\n\n在项目中新建包 `com.pj` ，在此包内新建主类 `SaTokenDemoApp.java`，复制以下代码：\n\n```java\n@SolonMain\npublic class SaTokenDemoApp {\n    public static void main(String[] args) {\n        Solon.start(SaTokenDemoApp.class, args);\n        System.out.println(\"启动成功，Sa-Token 配置如下：\" + SaManager.getConfig());\n    }\n}\n```\n\n### 5、创建测试Controller\n\n```java\n@Mapping(\"/user/\")\n@Controller\npublic class UserController {\n\n    // 测试登录，浏览器访问： http://localhost:8081/user/doLogin?username=zhang&password=123456\n    @Mapping(\"doLogin\")\n    public String doLogin(String username, String password) {\n        // 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n        if(\"zhang\".equals(username) && \"123456\".equals(password)) {\n            StpUtil.login(10001);\n            return \"登录成功\";\n        }\n        return \"登录失败\";\n    }\n\n    // 查询登录状态，浏览器访问： http://localhost:8081/user/isLogin\n    @Mapping(\"isLogin\")\n    public String isLogin() {\n        return \"当前会话是否登录：\" + StpUtil.isLogin();\n    }\n    \n}\n```\n\n### 6、运行\n\n启动代码，从浏览器依次访问上述测试接口：\n\n<img src=\"/big-file/doc/start/test-do-login.png\" alt=\"运行结果\">\n\n\n<img src=\"/big-file/doc/start/test-is-login.png\" alt=\"运行结果\">\n\n\n### 详细了解\n\n通过这个示例，你已经对 Sa-Token 有了初步的了解，那么现在开始详细了解一下它都有哪些吧：\n\n[登录认证](/use/login-auth) (与 Springboot 处理类似)\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/start/webflux-example.md",
    "content": "# Spring WebFlux 集成 Sa-Token 示例\n\n**Reactor** 是一种非阻塞的响应式模型，本篇将以 **WebFlux** 为例，展示 Sa-Token 与 Reactor 响应式模型框架相整合的示例，\n**你可以用同样方式去对接其它 Reactor 模型框架（例如 SpringCloud Gateway）**\n\n整合示例在官方仓库的`/sa-token-demo/sa-token-demo-webflux`文件夹下，如遇到难点可结合源码进行测试学习\n\n\n> [!WARNING| label:小提示 ] \n> WebFlux 常用于微服务网关架构中，如果您的应用基于单体架构且非 Reactor 模型，可以先跳过本章  \n\n\n---\n\n### 1、创建项目\n在 IDE 中新建一个 SpringBoot 项目，例如：`sa-token-demo-webflux`\n\n\n### 2、添加依赖\n在项目中添加依赖：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.cc -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.cc\nimplementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n\n<!-------- tab:Gradle (Kotlin) 方式 -------->\n``` gradle\n// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.cc\nimplementation(\"cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}\")\n```\n- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。\n- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。\n<!---------------------------- tabs:end ------------------------------>\n\n\n\n\n\n### 3、创建启动类\n在项目中新建包 `com.pj` ，在此包内新建主类 `SaTokenDemoApplication.java`，输入以下代码：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Java -------->\n``` java\n@SpringBootApplication\npublic class SaTokenDemoApplication {\n\tpublic static void main(String[] args) throws JsonProcessingException {\n\t\tSpringApplication.run(SaTokenDemoApplication.class, args);\n\t\tSystem.out.println(\"启动成功，Sa-Token 配置如下：\" + SaManager.getConfig());\n\t}\n}\n```\n\n<!-------- tab:Kotlin -------->\n```kotlin\n@SpringBootApplication\nclass SaTokenDemoApplication\n\nfun main(args: Array<String>) {\n    runApplication<SaTokenDemoApplication>(*args)\n    println(SaManager.getConfig())\n}\n```\n<!---------------------------- tabs:end ------------------------------>\n\n\n### 4、创建全局过滤器\n新建`SaTokenConfigure.java`，注册 Sa-Token 的全局过滤器\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Java -------->\n``` java\n/**\n * [Sa-Token 权限认证] 全局配置类 \n */\n@Configuration\npublic class SaTokenConfigure {\n\t/**\n     * 注册 [Sa-Token全局过滤器] \n     */\n    @Bean\n    public SaReactorFilter getSaReactorFilter() {\n        return new SaReactorFilter()\n        \t\t// 指定 [拦截路由]\n        \t\t.addInclude(\"/**\")    /* 拦截所有path */\n        \t\t// 指定 [放行路由]\n        \t\t.addExclude(\"/favicon.ico\")\n        \t\t// 指定[认证函数]: 每次请求执行 \n        \t\t.setAuth(obj -> {\n        \t\t\tSystem.out.println(\"---------- sa全局认证\");\n                    // SaRouter.match(\"/test/test\", () -> StpUtil.checkLogin());\n        \t\t})\n        \t\t// 指定[异常处理函数]：每次[认证函数]发生异常时执行此函数 \n        \t\t.setError(e -> {\n        \t\t\tSystem.out.println(\"---------- sa全局异常 \");\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n        \t\t;\n    }\n}\n```\n\n<!-------- tab:Kotlin -------->\n```kotlin\n@Configuration\nclass SaTokenConfigure {\n    /**\n     * 注册 [Sa-Token全局过滤器]\n     */\n    @Bean\n    fun saReactorFilter(): SaReactorFilter = SaReactorFilter()\n        // 指定 [拦截路由]（此处为拦截所有path）\n        .addInclude(\"/**\")\n        // 指定 [放行路由]\n        .addExclude(\"/favicon.ico\")\n        // 指定[认证函数]: 每次请求执行\n        .setAuth {\n            println(\"---------- sa全局认证\")\n            // SaRouter.match(\"/test/test\", SaFunction { StpUtil.checkLogin() })\n        }\n        // 指定[异常处理函数]：每次[认证函数]发生异常时执行此函数\n        .setError { e: Throwable ->\n            println(\"---------- sa全局异常 \")\n            SaResult.error(e.message)\n        }\n}\n```\n<!---------------------------- tabs:end ------------------------------>\n\n你只需要按照此格式复制代码即可，有关过滤器的详细用法，会在之后的章节详细介绍。\n\n\n### 5、创建测试Controller\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Java -------->\n``` java\n@RestController\n@RequestMapping(\"/user/\")\npublic class UserController {\n\n\t// 测试登录，浏览器访问： http://localhost:8081/user/doLogin?username=zhang&password=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic String doLogin(String username, String password) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(username) && \"123456\".equals(password)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn \"登录成功\";\n\t\t}\n\t\treturn \"登录失败\";\n\t}\n\n\t// 查询登录状态，浏览器访问： http://localhost:8081/user/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic String isLogin() {\n\t\treturn \"当前会话是否登录：\" + StpUtil.isLogin();\n\t}\n\t\n}\n```\n\n<!-------- tab:Kotlin -------->\n```kotlin\n@RestController\n@RequestMapping(\"/user/\")\nclass UserController {\n    \n    @RequestMapping(\"doLogin\")\n    fun doLogin(username: String, password: String) =\n        // 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对\n        if (\"zhang\" == username && \"123456\" == password) {\n            StpUtil.login(10001)\n            \"登录成功\"\n        } else \"登录失败\"\n    \n    @RequestMapping(\"isLogin\")\n    fun isLogin() = \"当前会话是否登录：\" + StpUtil.isLogin()\n    \n}\n```\n\n<!---------------------------- tabs:end ------------------------------>\n### 6、运行\n启动代码，从浏览器依次访问上述测试接口：\n\n<img src=\"/big-file/doc/start/test-do-login.png\" alt=\"运行结果\">\n\n<img src=\"/big-file/doc/start/test-is-login.png\" alt=\"运行结果\">\n\n\n**注意事项：**\n\n更多使用示例请参考官方仓库demo\n\n\n\n"
  },
  {
    "path": "sa-token-doc/static/custom-docsify-plugins/doc-lock-by-gzh-plugin.js",
    "content": "// 章节锁定插件 \n\n// 声明 docsify 插件\nvar docLockPlugin = function(hook, vm) {\n\t\n\t// 钩子函数：解析之前执行\n\thook.beforeEach(function(content) {\n\t\treturn content;\n\t});\n\t\n\t// 钩子函数：每次路由切换时，解析内容之后执行 \n\thook.afterEach(function(html) {\n\t\treturn html;\n\t});\n\t\n\t// 钩子函数：每次路由切换时数据全部加载完成后调用，没有参数。\n\thook.doneEach(function() {\n\t\tisShowTanChuang(vm);\n\t});\n\t\n\t// 钩子函数：初始化并第一次加载完成数据后调用，没有参数。\n\thook.ready(function() {\n\t\t\n\t});\n\t\n\t\n\t\n\t\n\t\n\t// ======================================== 弹窗方法 \n\t\n\t// 检查成功后，多少天不再检查 \n\tconst dl_AllowDisparity = 1000 * 60 * 60 * 24 * 30 * 1;  // 1个月\n\t// 拦截 path ，如果填 /** 代表所有路径，填 /sso/* 代表 /sso/ 目录下所有路径\n\tconst dl_exeArray = [\n\t\t'/sso/*', '/oauth2/*', '/more/common-questions', '/up/*', '/micro/*', '/plugin/*'\n\t];\n\t// 排除 path \n\tconst dl_excludeArray = [\n\t\t'/sso/readme', '/oauth2/readme'\n\t];\n\t// 本次存储时，使用的标记 key\n\tconst dl_saveKey = 'dl_saveKey';\n\t\n\t\n\t// 判断当前是否应该弹出 \n\tfunction isShowTanChuang(vm) {\n\t\t// 非PC端不检查\n\t\t// if(document.body.offsetWidth < 800) {\n\t\t// \tconsole.log('small screen ... wj ');\n\t\t// \treturn;\n\t\t// }\n\t\t\n\t\t// 判断是否需要拦截 \n\t\tconst isExe = isExePath(vm.route.path, dl_exeArray, dl_excludeArray);\n\t\tif(!isExe) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 判断是否近期已经判断过了\n\t\ttry{\n\t\t\tconst flagTime = localStorage[dl_saveKey];\n\t\t\tif(flagTime) {\n\t\t\t\t// 记录 存储 的时间，和当前时间的差距\n\t\t\t\tconst disparity = new Date().getTime() - parseInt(flagTime);\n\t\t\t\t\n\t\t\t\t// 差距小于指定时间，不再检测 \n\t\t\t\tif(disparity < dl_AllowDisparity) {\n\t\t\t\t\tconsole.log('checked ... docLock ');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}catch(e){\n\t\t\tconsole.error(e);\n\t\t}\n\t\t\n\t\t// 本次打开页面的内存内已经弹出了的话，也不再弹了 \n\t\t// if(window.isYtcXsjfkasjdaaaa) {\n\t\t// \treturn;\n\t\t// }\n\t\t// window.isYtcXsjfkasjdaaaa = true;\n\t\t\n\t\t// 验证成功的回调\n\t\tconst okFn = function() {\n\t\t\tconsole.log('ok 了');\n\t\t\tlocalStorage.setItem(dl_saveKey, new Date().getTime() );\n\t\t\t$('body').css({'overflow': 'auto'});\n\t\t\tlayer.msg('感谢你的支持，Sa-Token 将努力变得更加完善！  ❤️ ❤️ ❤️ ');\n\t\t}\n\t\t// 点了返回的回调 \n\t\tconst backFu = function() {\n\t\t\t$('body').css({'overflow': 'auto'});\n\t\t\tlocation.href = '#/';\n\t\t}\n\t\t// 弹窗验证 \n\t\tshowDocLock(okFn, backFu);\n\t\t$('body').css({'overflow': 'hidden'});\n\t\t\n\t\t// 弹出弹框，邀请填写 \n\t\treturn;\n\t}\n\t\n\t\n\t// ======================================== 路径判断\n\t\n\t/**\n\t * 判断一个路径，是否会被成功拦截，返回 true 或 false \n\t * @param {Object} path   要判断的路径，例如：/sso/apidoc\n\t * @param {Object} exeArray   要拦截的路径数组，例如：['/sso/*', '/oauth2/*', '/more/common-questions'  ]，如果填 /** 代表所有路径，填 /sso/* 代表 /sso/ 目录下所有路径\n\t * @param {Object} excludeArray  要排除的路径数组，规则同上 \n\t */\n\tfunction isExePath( path, exeArray,  excludeArray) {\n\t\t // 参数验证和初始化\n\t\texeArray = exeArray || [];\n\t\texcludeArray = excludeArray || [];\n\t\t\n\t\t// 标准化路径，确保以 / 开头\n\t\tpath = normalizePath(path);\n\t\t\n\t\t// 先检查排除规则（优先级更高）\n\t\tfor (let pattern of excludeArray) {\n\t\t\tif (matchPattern(path, pattern)) {\n\t\t\t\treturn false; // 被排除，不拦截\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 再检查拦截规则\n\t\tfor (let pattern of exeArray) {\n\t\t\tif (matchPattern(path, pattern)) {\n\t\t\t\treturn true; // 需要拦截\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn false; // 默认不拦截\n\t}\n\t\n\t/**\n\t * 标准化路径\n\t */\n\tfunction normalizePath(path) {\n\t    if (!path) return '/';\n\t    if (!path.startsWith('/')) return '/' + path;\n\t    return path;\n\t}\n\t\n\t/**\n\t * 增强版模式匹配\n\t */\n\tfunction matchPattern(path, pattern) {\n\t    // 处理空值\n\t    if (!pattern) return false;\n\t    \n\t    pattern = pattern.trim();\n\t    \n\t    // /** 匹配所有\n\t    if (pattern === '/**' || pattern === '**') {\n\t        return true;\n\t    }\n\t    \n\t    // 处理前置和后置通配符\n\t    if (pattern === '*' || pattern === '/*') {\n\t        return true;\n\t    }\n\t    \n\t    // 精确匹配\n\t    if (!pattern.includes('*')) {\n\t        return path === pattern || path === normalizePath(pattern);\n\t    }\n\t    \n\t    // 转换模式为正则表达式\n\t    const regexStr = pattern\n\t        // 转义正则特殊字符\n\t        .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\t        // 处理 ** 通配符（匹配多级目录）\n\t        .replace(/\\/\\*\\*/g, '(/.*)?')\n\t        // 处理 * 通配符（匹配单级目录）\n\t        .replace(/\\*/g, '[^/]*')\n\t        // 确保匹配完整路径\n\t        .replace(/^\\//, '^/')\n\t        .replace(/$/, '$');\n\t    \n\t    try {\n\t        const regex = new RegExp(regexStr);\n\t        return regex.test(path);\n\t    } catch (e) {\n\t        console.error(`Invalid pattern: ${pattern}`, e);\n\t        return false;\n\t    }\n\t}\n\t\n\t\n\t\n}\n\n\n\n\n\n\n\n\n// =========================== AI 生成的弹窗代码\n\nfunction initTanChuangFun() {\n\t\n\t// 配置项\n\tconst CONFIG = {\n\t\tcorrectPassword: 'sa-token yyds', // 正确密码\n\t\tgzhImageUrl: './big-file/contact/lykj-gzh.jpg',\n\t\twechatImageUrl: './big-file/doc/zong/doc-lock-pre-wx.png'\n\t};\n\t\n\t// 弹窗HTML模板\n\tconst modalHTML = `\n\t\t<div class=\"modal-overlay\" id=\"passwordModal\">\n\t\t\t<div class=\"modal\">\n\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t<h2>🔒 本章节已锁定，输入密码后即可正常访问：</h2>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t<p class=\"error-message\" id=\"errorMessage\">密码错误，请重新输入！</p>\n\t\t\t\t\t\n\t\t\t\t\t<div class=\"password-form\">\n\t\t\t\t\t\t<input type=\"text\" class=\"password-input\" id=\"passwordInput\" placeholder=\"关注公众号可查看密码\" autocomplete=\"off\">\n\t\t\t\t\t\t\n\t\t\t\t\t\t<div class=\"form-actions\">\n\t\t\t\t\t\t\t<button class=\"form-btn btn-verify\" id=\"verifyBtn\">验证</button>\n\t\t\t\t\t\t\t<button class=\"form-btn btn-back\" id=\"backBtn\">回首页</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t\n\t\t\t\t\t<div class=\"password-help-section\">\n\t\t\t\t\t\t<div class=\"help-text\" style=\"text-align: center;\">\n\t\t\t\t\t\t\t关注公众号可查看密码：\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<div class=\"images-container\" style=\"text-align: center;\">\n\t\t\t\t\t\t\t<div class=\"qr-image-container\" style=\"border: 0;\">\n\t\t\t\t\t\t\t\t<img src=\"${CONFIG.gzhImageUrl}\" alt=\"QQ 群公告\" class=\"qr-image\" style=\"width: 200px; height: 200px; margin: 0 auto; \">\n\t\t\t\t\t\t\t\t<div class=\"image-label\">关注后点击 [私信]，点击菜单栏下方 [ 文档密码 ]</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t\n\t\t<div class=\"image-overlay\" id=\"imageOverlay\">\n\t\t\t<img src=\"\" alt=\"放大图片\" class=\"enlarged-image\" id=\"enlargedImage\">\n\t\t</div>\n\t`;\n\t\n\t// 初始化变量\n\tlet passwordModal, passwordInput, errorMessage, verifyBtn, backBtn;\n\tlet imageOverlay, enlargedImage;\n\t\n\t\n\tlet okCallFn = null;\n\tlet backCallFn = null;\n\t\n\t/**\n\t * 初始化弹窗\n\t * 将弹窗HTML插入到页面中，并绑定事件\n\t */\n\tfunction initModal() {\n\t\t// 插入弹窗HTML到页面\n\t\tdocument.body.insertAdjacentHTML('beforeend', modalHTML);\n\t\t\n\t\t// 获取DOM元素\n\t\tpasswordModal = document.getElementById('passwordModal');\n\t\tpasswordInput = document.getElementById('passwordInput');\n\t\terrorMessage = document.getElementById('errorMessage');\n\t\tverifyBtn = document.getElementById('verifyBtn');\n\t\tbackBtn = document.getElementById('backBtn');\n\t\timageOverlay = document.getElementById('imageOverlay');\n\t\tenlargedImage = document.getElementById('enlargedImage');\n\t\t\n\t\t// 绑定事件\n\t\tbindEvents();\n\t}\n\t\n\t/**\n\t * 绑定所有事件\n\t */\n\tfunction bindEvents() {\n\t\t// 触发按钮点击事件\n\t\t// document.getElementById('accessBtn').addEventListener('click', openModal);\n\t\t\n\t\t// 验证按钮点击事件\n\t\tverifyBtn.addEventListener('click', validatePassword);\n\t\t\n\t\t// 返回按钮点击事件\n\t\tbackBtn.addEventListener('click', function(){\n\t\t\tcloseModal();\n\t\t\tbackCallFn();\n\t\t});\n\t\t\n\t\t// 输入框回车事件\n\t\tpasswordInput.addEventListener('keypress', function(e) {\n\t\t\tif (e.key === 'Enter') {\n\t\t\t\tvalidatePassword();\n\t\t\t}\n\t\t});\n\t\t\n\t\t// 只在非移动端绑定图片点击事件\n\t\tif (!isMobileDevice()) {\n\t\t\t// 图片点击放大事件\n\t\t\tdocument.querySelectorAll('.qr-image').forEach(img => {\n\t\t\t\timg.addEventListener('click', function() {\n\t\t\t\t\tenlargeImage(this.src);\n\t\t\t\t});\n\t\t\t});\n\t\t\t\n\t\t\t// 放大图片关闭事件\n\t\t\timageOverlay.addEventListener('click', function(e) {\n\t\t\t\tif (e.target === this || e.target === enlargedImage) {\n\t\t\t\t\tcloseImageOverlay();\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t\t// ESC键关闭放大图片\n\t\t\tdocument.addEventListener('keydown', function(e) {\n\t\t\t\tif (e.key === 'Escape' && imageOverlay.classList.contains('active')) {\n\t\t\t\t\tcloseImageOverlay();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\t\n\t/**\n\t * 检测是否为移动设备\n\t * @returns {boolean} 是否为移动设备\n\t */\n\tfunction isMobileDevice() {\n\t\treturn window.innerWidth <= 768;\n\t}\n\t\n\t/**\n\t * 打开密码弹窗\n\t */\n\tfunction openModal() {\n\t\tpasswordModal.classList.add('active');\n\t\tpasswordInput.focus();\n\t\terrorMessage.classList.remove('show');\n\t\tpasswordInput.value = '';\n\t}\n\t\n\t/**\n\t * 关闭密码弹窗\n\t */\n\tfunction closeModal() {\n\t\tpasswordModal.classList.remove('active');\n\t}\n\t\n\t/**\n\t * 密码验证函数\n\t * 宽松验证策略：允许左右空格，中间空格可省略\n\t */\n\tfunction validatePassword() {\n\t\tlet enteredPassword = passwordInput.value.trim(); // 去除左右空格\n\t\t\n\t\t// 标准化：移除所有空格\n\t\tconst normalizedEntered = enteredPassword.replace(/\\s+/g, '');\n\t\tconst normalizedCorrect = CONFIG.correctPassword.replace(/\\s+/g, '');\n\t\t\n\t\tif (normalizedEntered === normalizedCorrect) {\n\t\t\t// 密码正确，解锁章节\n\t\t\tunlockChapter();\n\t\t} else {\n\t\t\t// 密码错误，显示错误信息\n\t\t\tshowError();\n\t\t}\n\t}\n\t\n\t/**\n\t * 显示密码错误提示\n\t */\n\tfunction showError() {\n\t\terrorMessage.classList.add('show');\n\t\tpasswordInput.value = '';\n\t\tpasswordInput.focus();\n\t\t\n\t\t// 添加抖动效果\n\t\tpasswordInput.classList.add('shake');\n\t\tsetTimeout(() => {\n\t\t\tpasswordInput.classList.remove('shake');\n\t\t}, 500);\n\t}\n\t\n\t/**\n\t * 解锁章节\n\t */\n\tfunction unlockChapter() {\n\t\tcloseModal();\n\t\tokCallFn();\n\t\t\n\t\t\n\t\t// // 更新章节内容\n\t\t// const lockedSection = document.querySelector('.locked-section');\n\t\t// const tocLockedItems = document.querySelectorAll('.toc a.locked');\n\t\t\n\t\t// // 更新章节显示\n\t\t// lockedSection.innerHTML = `\n\t\t// \t<h3><i class=\"fas fa-unlock-alt\" style=\"color:#2ecc71;\"></i> 章节已解锁</h3>\n\t\t// \t<p>感谢您加入我们的社区！现在您可以查看高级配置指南的全部内容。</p>\n\t\t// \t<div style=\"text-align: left; margin-top: 20px;\">\n\t\t// \t\t<h4>高级配置内容示例：</h4>\n\t\t// \t\t<p>1. 自定义插件开发：详细讲解如何为项目开发自定义插件，包括插件结构、API接口和最佳实践。</p>\n\t\t// \t\t<p>2. 性能调优指南：深入分析项目性能瓶颈，并提供多种优化方案和调优技巧。</p>\n\t\t// \t\t<p>3. 高级集成方案：介绍如何将项目与其他流行框架和工具进行深度集成。</p>\n\t\t// \t\t<p>4. 企业级部署：针对生产环境的企业级部署方案，包括高可用、负载均衡和监控配置。</p>\n\t\t// \t</div>\n\t\t// \t<p style=\"margin-top: 20px; color: #27ae60; font-weight: 600;\">\n\t\t// \t\t<i class=\"fas fa-check-circle\"></i> 您现在可以访问所有高级章节内容了！\n\t\t// \t</p>\n\t\t// `;\n\t\t\n\t\t// // 更新目录状态\n\t\t// tocLockedItems.forEach(item => {\n\t\t// \tif (item.textContent.includes('高级配置指南')) {\n\t\t// \t\titem.classList.remove('locked');\n\t\t// \t\titem.innerHTML = '<i class=\"fas fa-unlock-alt\" style=\"color:#2ecc71;\"></i> 高级配置指南';\n\t\t// \t}\n\t\t// });\n\t\t\n\t\t// // 显示成功通知\n\t\t// showNotification('章节解锁成功！您现在可以访问高级配置指南。');\n\t}\n\t\n\t/**\n\t * 放大图片\n\t * @param {string} src - 图片地址\n\t */\n\tfunction enlargeImage(src) {\n\t\tenlargedImage.src = src;\n\t\timageOverlay.classList.add('active');\n\t}\n\t\n\t/**\n\t * 关闭图片放大层\n\t */\n\tfunction closeImageOverlay() {\n\t\timageOverlay.classList.remove('active');\n\t}\n\t\n\t/**\n\t * 显示通知\n\t * @param {string} message - 通知内容\n\t */\n\tfunction showNotification(message) {\n\t\tconst notification = document.createElement('div');\n\t\tnotification.style.cssText = `\n\t\t\tposition: fixed;\n\t\t\ttop: 20px;\n\t\t\tright: 20px;\n\t\t\tbackground-color: #2ecc71;\n\t\t\tcolor: white;\n\t\t\tpadding: 15px 25px;\n\t\t\tborder-radius: 4px;\n\t\t\tbox-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);\n\t\t\tz-index: 1001;\n\t\t\tfont-weight: 600;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\ttransform: translateX(150%);\n\t\t\ttransition: transform 0.5s ease;\n\t\t`;\n\t\t\n\t\tnotification.innerHTML = `<i class=\"fas fa-check-circle\"></i><span>${message}</span>`;\n\t\tdocument.body.appendChild(notification);\n\t\t\n\t\t// 显示通知\n\t\tsetTimeout(() => {\n\t\t\tnotification.style.transform = 'translateX(0)';\n\t\t}, 10);\n\t\t\n\t\t// 3秒后隐藏\n\t\tsetTimeout(() => {\n\t\t\tnotification.style.transform = 'translateX(150%)';\n\t\t\tsetTimeout(() => {\n\t\t\t\tdocument.body.removeChild(notification);\n\t\t\t}, 500);\n\t\t}, 3000);\n\t}\n\t\n\t/**\n\t * 添加抖动动画样式\n\t */\n\tfunction addShakeAnimation() {\n\t\tconst style = document.createElement('style');\n\t\tstyle.textContent = `\n\t\t\t@keyframes shake {\n\t\t\t\t0%, 100% { transform: translateX(0); }\n\t\t\t\t10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }\n\t\t\t\t20%, 40%, 60%, 80% { transform: translateX(5px); }\n\t\t\t}\n\t\t\t.shake {\n\t\t\t\tanimation: shake 0.5s;\n\t\t\t\tborder-color: #e74c3c !important;\n\t\t\t}\n\t\t`;\n\t\tdocument.head.appendChild(style);\n\t}\n\t\n\t// 初始化\n\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\tinitModal();\n\t\taddShakeAnimation();\n\t});\n\t\n\t// 显示弹窗： 验证成功的回调、点击返回的回调 \n\twindow.showDocLock = function(okFn, backFn) {\n\t\tokCallFn = okFn;\n\t\tbackCallFn = backFn;\n\t\t// 打开 \n\t\topenModal();\n\t}\n\t\n};\ninitTanChuangFun();"
  },
  {
    "path": "sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.css",
    "content": "/* 弹窗遮罩层样式 */\n.modal-overlay {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tbackground-color: rgba(0, 0, 0, 0.7);\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tz-index: 1000;\n\topacity: 0;\n\tvisibility: hidden;\n\ttransition: opacity 0.3s, visibility 0.3s;\n}\n\n.modal-overlay.active {\n\topacity: 1;\n\tvisibility: visible;\n}\n\n/* 弹窗主体样式 */\n.modal {\n\tbackground: white;\n\tborder-radius: 4px; /* 弹窗圆角4px */\n\twidth: 90%;\n\tmax-width: 500px;\n\tpadding: 25px 30px;\n\tbox-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);\n\ttransform: translateY(-20px);\n\ttransition: transform 0.3s;\n}\n\n.modal-overlay.active .modal {\n\ttransform: translateY(0);\n}\n\n/* 弹窗标题样式 */\n.modal-header {\n\tdisplay: flex;\n\talign-items: center;\n\tmargin-bottom: 20px;\n\t/* color: green; */\n\t/* color: #2d8cf0; */\n\tcolor: green;\n}\n\n.modal-header i {\n\tfont-size: 1.8rem;\n\tmargin-right: 12px;\n}\n\n.modal-header h2 {\n\tfont-size: 16px;\n\t/* color: #e74c3c; */\n\tmargin: 0;\n}\n\n/* 密码输入区域样式 */\n.password-form {\n\tmargin-bottom: 20px;\n}\n\n.password-input {\n\twidth: 100%;\n\tpadding: 14px;\n\tborder: 2px solid #ddd;\n\tborder-radius: 2px; /* 输入框圆角2px */\n\tfont-size: 1rem;\n\ttransition: border-color 0.3s;\n\tmargin-bottom: 15px;\n}\n\n.password-input:focus {\n\tborder-color: #2d8cf0;\n\toutline: none;\n}\n\n/* 按钮区域样式 */\n.form-actions {\n\tdisplay: flex;\n\tgap: 15px;\n\tjustify-content: flex-start;\n}\n\n.form-btn {\n\tcolor: white;\n\tborder: none;\n\tpadding: 12px 24px;\n\tborder-radius: 3px; /* 按钮圆角2px */\n\tfont-weight: 400;\n\tcursor: pointer;\n\tfont-size: 1rem;\n\ttransition: all 0.3s;\n\twhite-space: nowrap;\n\twidth: calc(50% - 5px);\n}\n\n.btn-verify {\n\tbackground-color: #2d8cf0;\n}\n\n.btn-verify:hover {\n\tbackground-color: #1c7ae0;\n}\n\n.btn-back {\n\tbackground-color: #aaa;\n}\n\n.btn-back:hover {\n\tbackground-color: #888;\n}\n\n/* 错误提示样式 */\n.error-message {\n\tcolor: #e74c3c;\n\tmargin-bottom: 15px;\n\ttext-align: center;\n\tfont-weight: 600;\n\tdisplay: none;\n\tbackground-color: #ffebee;\n\tpadding: 10px;\n\tborder-radius: 2px;\n\tborder-left: 3px solid #e74c3c;\n}\n\n.error-message.show {\n\tdisplay: block;\n}\n\n/* 提示区域样式 */\n.password-help-section {\n\tmargin-top: 25px;\n\tborder-top: 1px solid #eee;\n\tpadding-top: 20px;\n}\n\n.help-text {\n\ttext-align: left;\n\tmargin-bottom: 20px;\n\tcolor: #666;\n\tfont-weight: 400;\n\tfont-size: 16px;\n}\n\n.help-text a {\n\tcolor: #0c6ae0;\n\ttext-decoration: none;\n\ttransition: color 0.2s;\n}\n\n.help-text a:hover {\n\tcolor: blue;\n\ttext-decoration: underline;\n}\n\n/* 二维码图片区域样式 */\n.images-container {\n\tdisplay: flex;\n\tgap: 15px;\n\tjustify-content: center;\n\talign-items: center;\n}\n\n.qr-image-container {\n\tflex: 1;\n\tposition: relative;\n\toverflow: hidden;\n\tborder-radius: 0px;\n\t/* box-shadow: 0 0px 8px rgba(0, 0, 0, 0.1); */\n\tborder: 1px #ddd solid;\n\tcursor: pointer;\n\ttransition: transform 0.3s;\n\tpadding-bottom: 5px;\n}\n\n.qr-image {\n\twidth: 100%;\n\theight: 110px; /* 3:2比例 */\n\tobject-fit: cover;\n\tdisplay: block;\n\ttransition: transform 0.3s;\n}\n\n.qr-image-container:hover {\n\ttransform: translateY(-5px);\n}\n\n.qr-image-container:hover .qr-image {\n\ttransform: scale(1.05);\n}\n\n.image-label {\n\ttext-align: center;\n\tfont-size: 0.85rem;\n\tcolor: #7f8c8d;\n\tmargin-top: 8px;\n\tfont-weight: 500;\n}\n\n/* 图片放大效果样式 */\n.image-overlay {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: rgba(0, 0, 0, 0.9);\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tz-index: 2000;\n\tcursor: pointer;\n\topacity: 0;\n\tvisibility: hidden;\n\ttransition: opacity 0.4s ease, visibility 0.4s ease;\n}\n\n.image-overlay.active {\n\topacity: 1;\n\tvisibility: visible;\n}\n\n.enlarged-image {\n\tmax-width: 80%;\n\tmax-height: 80%;\n\tborder-radius: 8px;\n\tbox-shadow: 0 15px 40px rgba(0, 0, 0, 0.5);\n\ttransform: scale(0.8);\n\ttransition: transform 0.4s ease;\n}\n\n.image-overlay.active .enlarged-image {\n\ttransform: scale(1);\n}\n\n/* ============================ */\n/* 移动端适配样式 */\n/* ============================ */\n\n/* 移动端：屏幕宽度小于等于768px时应用 */\n@media (max-width: 768px) {\n\t/* 弹窗宽度调整，左右留出边距 */\n\t.modal {\n\t\twidth: calc(100% - 30px); /* 左右各15px边距 */\n\t\tmax-width: 100%;\n\t\tpadding: 20px;\n\t\tmargin: 0 15px;\n\t}\n\t\n\t/* 弹窗标题调整 */\n\t.modal-header {\n\t\tmargin-bottom: 15px;\n\t}\n\t\n\t.modal-header h2 {\n\t\tfont-size: 1.1rem;\n\t\tline-height: 1.3;\n\t}\n\t\n\t.modal-header i {\n\t\tfont-size: 1.5rem;\n\t\tmargin-right: 10px;\n\t}\n\t\n\t/* 密码输入框调整 */\n\t.password-input {\n\t\tpadding: 12px;\n\t\tfont-size: 0.95rem;\n\t\tmargin-bottom: 10px;\n\t}\n\t\n\t/* 按钮区域调整 */\n\t.form-actions {\n\t\tflex-direction: column; /* 按钮垂直排列 */\n\t\tgap: 10px;\n\t\tjustify-content: stretch;\n\t}\n\t\n\t.form-btn {\n\t\twidth: 100%;\n\t\tpadding: 14px;\n\t\tfont-size: 1rem;\n\t}\n\t\n\t/* 错误提示调整 */\n\t.error-message {\n\t\tfont-size: 0.9rem;\n\t\tpadding: 8px;\n\t\tmargin-bottom: 10px;\n\t}\n\t\n\t/* 提示区域调整 */\n\t.password-help-section {\n\t\tmargin-top: 15px;\n\t\tpadding-top: 15px;\n\t}\n\t\n\t.help-text {\n\t\tfont-size: 0.95rem;\n\t\tmargin-bottom: 15px;\n\t}\n\t\n\t/* 移动端隐藏图片区域 */\n\t.images-container {\n\t\tdisplay: none; /* 移动端隐藏图片 */\n\t}\n\t\n\t/* 图片放大效果在移动端隐藏 */\n\t.image-overlay {\n\t\tdisplay: none;\n\t}\n\t\n\t/* 弹窗内容垂直居中优化 */\n\t.modal {\n\t\tmax-height: 80vh;\n\t\toverflow-y: auto;\n\t}\n}\n\n/* 超小屏幕适配：屏幕宽度小于等于480px时应用 */\n@media (max-width: 480px) {\n\t.modal {\n\t\twidth: calc(100% - 20px); /* 左右各10px边距 */\n\t\tmargin: 0 10px;\n\t\tpadding: 15px;\n\t}\n\t\n\t.modal-header h2 {\n\t\tfont-size: 1rem;\n\t}\n\t\n\t.help-text {\n\t\tfont-size: 0.9rem;\n\t\tline-height: 1.4;\n\t}\n\t\n\t.password-input {\n\t\tfont-size: 0.9rem;\n\t}\n\t\n\t.form-btn {\n\t\tfont-size: 0.95rem;\n\t}\n}"
  },
  {
    "path": "sa-token-doc/static/custom-docsify-plugins/doc-lock-plugin.js",
    "content": "// 章节锁定插件 \n\n// 声明 docsify 插件\nvar docLockPlugin = function(hook, vm) {\n\t\n\t// 钩子函数：解析之前执行\n\thook.beforeEach(function(content) {\n\t\treturn content;\n\t});\n\t\n\t// 钩子函数：每次路由切换时，解析内容之后执行 \n\thook.afterEach(function(html) {\n\t\treturn html;\n\t});\n\t\n\t// 钩子函数：每次路由切换时数据全部加载完成后调用，没有参数。\n\thook.doneEach(function() {\n\t\t// isShowTanChuang(vm);\n\t});\n\t\n\t// 钩子函数：初始化并第一次加载完成数据后调用，没有参数。\n\thook.ready(function() {\n\t\t\n\t});\n\t\n\t\n\t\n\t\n\t\n\t// ======================================== 弹窗方法 \n\t\n\t// 检查成功后，多少天不再检查 \n\tconst dl_AllowDisparity = 1000 * 60 * 60 * 24 * 30 * 1;  // 1个月\n\t// 拦截 path ，如果填 /** 代表所有路径，填 /sso/* 代表 /sso/ 目录下所有路径\n\tconst dl_exeArray = [\n\t\t'/sso/*', '/oauth2/*', '/more/common-questions', '/up/*', '/micro/*', '/plugin/*'\n\t];\n\t// 排除 path \n\tconst dl_excludeArray = [\n\t\t'/sso/readme', '/oauth2/readme'\n\t];\n\t// 本次存储时，使用的标记 key\n\tconst dl_saveKey = 'dl_saveKey';\n\t\n\t\n\t// 判断当前是否应该弹出 \n\tfunction isShowTanChuang(vm) {\n\t\t// 非PC端不检查\n\t\t// if(document.body.offsetWidth < 800) {\n\t\t// \tconsole.log('small screen ... wj ');\n\t\t// \treturn;\n\t\t// }\n\t\t\n\t\t// 判断是否需要拦截 \n\t\tconst isExe = isExePath(vm.route.path, dl_exeArray, dl_excludeArray);\n\t\tif(!isExe) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 判断是否近期已经判断过了\n\t\ttry{\n\t\t\tconst flagTime = localStorage[dl_saveKey];\n\t\t\tif(flagTime) {\n\t\t\t\t// 记录 存储 的时间，和当前时间的差距\n\t\t\t\tconst disparity = new Date().getTime() - parseInt(flagTime);\n\t\t\t\t\n\t\t\t\t// 差距小于指定时间，不再检测 \n\t\t\t\tif(disparity < dl_AllowDisparity) {\n\t\t\t\t\tconsole.log('checked ... docLock ');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}catch(e){\n\t\t\tconsole.error(e);\n\t\t}\n\t\t\n\t\t// 本次打开页面的内存内已经弹出了的话，也不再弹了 \n\t\t// if(window.isYtcXsjfkasjdaaaa) {\n\t\t// \treturn;\n\t\t// }\n\t\t// window.isYtcXsjfkasjdaaaa = true;\n\t\t\n\t\t// 验证成功的回调\n\t\tconst okFn = function() {\n\t\t\tconsole.log('ok 了');\n\t\t\tlocalStorage.setItem(dl_saveKey, new Date().getTime() );\n\t\t\t$('body').css({'overflow': 'auto'});\n\t\t\tlayer.msg('感谢你的支持，Sa-Token 将努力变得更加完善！  ❤️ ❤️ ❤️ ');\n\t\t}\n\t\t// 点了返回的回调 \n\t\tconst backFu = function() {\n\t\t\t$('body').css({'overflow': 'auto'});\n\t\t\tlocation.href = '#/';\n\t\t}\n\t\t// 弹窗验证 \n\t\tshowDocLock(okFn, backFu);\n\t\t$('body').css({'overflow': 'hidden'});\n\t\t\n\t\t// 弹出弹框，邀请填写 \n\t\treturn;\n\t}\n\t\n\t\n\t// ======================================== 路径判断\n\t\n\t/**\n\t * 判断一个路径，是否会被成功拦截，返回 true 或 false \n\t * @param {Object} path   要判断的路径，例如：/sso/apidoc\n\t * @param {Object} exeArray   要拦截的路径数组，例如：['/sso/*', '/oauth2/*', '/more/common-questions'  ]，如果填 /** 代表所有路径，填 /sso/* 代表 /sso/ 目录下所有路径\n\t * @param {Object} excludeArray  要排除的路径数组，规则同上 \n\t */\n\tfunction isExePath( path, exeArray,  excludeArray) {\n\t\t // 参数验证和初始化\n\t\texeArray = exeArray || [];\n\t\texcludeArray = excludeArray || [];\n\t\t\n\t\t// 标准化路径，确保以 / 开头\n\t\tpath = normalizePath(path);\n\t\t\n\t\t// 先检查排除规则（优先级更高）\n\t\tfor (let pattern of excludeArray) {\n\t\t\tif (matchPattern(path, pattern)) {\n\t\t\t\treturn false; // 被排除，不拦截\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 再检查拦截规则\n\t\tfor (let pattern of exeArray) {\n\t\t\tif (matchPattern(path, pattern)) {\n\t\t\t\treturn true; // 需要拦截\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn false; // 默认不拦截\n\t}\n\t\n\t/**\n\t * 标准化路径\n\t */\n\tfunction normalizePath(path) {\n\t    if (!path) return '/';\n\t    if (!path.startsWith('/')) return '/' + path;\n\t    return path;\n\t}\n\t\n\t/**\n\t * 增强版模式匹配\n\t */\n\tfunction matchPattern(path, pattern) {\n\t    // 处理空值\n\t    if (!pattern) return false;\n\t    \n\t    pattern = pattern.trim();\n\t    \n\t    // /** 匹配所有\n\t    if (pattern === '/**' || pattern === '**') {\n\t        return true;\n\t    }\n\t    \n\t    // 处理前置和后置通配符\n\t    if (pattern === '*' || pattern === '/*') {\n\t        return true;\n\t    }\n\t    \n\t    // 精确匹配\n\t    if (!pattern.includes('*')) {\n\t        return path === pattern || path === normalizePath(pattern);\n\t    }\n\t    \n\t    // 转换模式为正则表达式\n\t    const regexStr = pattern\n\t        // 转义正则特殊字符\n\t        .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\t        // 处理 ** 通配符（匹配多级目录）\n\t        .replace(/\\/\\*\\*/g, '(/.*)?')\n\t        // 处理 * 通配符（匹配单级目录）\n\t        .replace(/\\*/g, '[^/]*')\n\t        // 确保匹配完整路径\n\t        .replace(/^\\//, '^/')\n\t        .replace(/$/, '$');\n\t    \n\t    try {\n\t        const regex = new RegExp(regexStr);\n\t        return regex.test(path);\n\t    } catch (e) {\n\t        console.error(`Invalid pattern: ${pattern}`, e);\n\t        return false;\n\t    }\n\t}\n\t\n\t\n\t\n}\n\n\n\n\n\n\n\n\n// =========================== AI 生成的弹窗代码\n\nfunction initTanChuangFun() {\n\t\n\t// 配置项\n\tconst CONFIG = {\n\t\tcorrectPassword: 'sa-token yyds', // 正确密码\n\t\tqqImageUrl: './big-file/doc/zong/doc-lock-pre-qq.png',\n\t\twechatImageUrl: './big-file/doc/zong/doc-lock-pre-wx.png'\n\t};\n\t\n\t// 弹窗HTML模板\n\tconst modalHTML = `\n\t\t<div class=\"modal-overlay\" id=\"passwordModal\">\n\t\t\t<div class=\"modal\">\n\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t<h2>🔒 本章节已锁定，输入密码后即可正常访问：</h2>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t<p class=\"error-message\" id=\"errorMessage\">密码错误，请重新输入！</p>\n\t\t\t\t\t\n\t\t\t\t\t<div class=\"password-form\">\n\t\t\t\t\t\t<input type=\"text\" class=\"password-input\" id=\"passwordInput\" placeholder=\"加群可获得访问密码\" autocomplete=\"off\">\n\t\t\t\t\t\t\n\t\t\t\t\t\t<div class=\"form-actions\">\n\t\t\t\t\t\t\t<button class=\"form-btn btn-verify\" id=\"verifyBtn\">验证</button>\n\t\t\t\t\t\t\t<button class=\"form-btn btn-back\" id=\"backBtn\">回首页</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t\n\t\t\t\t\t<div class=\"password-help-section\">\n\t\t\t\t\t\t<div class=\"help-text\">\n\t\t\t\t\t\t\t加入 QQ群 或 微信群 后可在群公告查看密码：<a href=\"#/more/join-group\" target=\"_black\">加群链接</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\n\t\t\t\t\t\t<div class=\"images-container\">\n\t\t\t\t\t\t\t<div class=\"qr-image-container\">\n\t\t\t\t\t\t\t\t<img src=\"${CONFIG.qqImageUrl}\" alt=\"QQ 群公告\" class=\"qr-image\">\n\t\t\t\t\t\t\t\t<div class=\"image-label\">QQ 群公告</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"qr-image-container\">\n\t\t\t\t\t\t\t\t<img src=\"${CONFIG.wechatImageUrl}\" alt=\"微信群公告\" class=\"qr-image\">\n\t\t\t\t\t\t\t\t<div class=\"image-label\">微信群公告</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t\n\t\t<div class=\"image-overlay\" id=\"imageOverlay\">\n\t\t\t<img src=\"\" alt=\"放大图片\" class=\"enlarged-image\" id=\"enlargedImage\">\n\t\t</div>\n\t`;\n\t\n\t// 初始化变量\n\tlet passwordModal, passwordInput, errorMessage, verifyBtn, backBtn;\n\tlet imageOverlay, enlargedImage;\n\t\n\t\n\tlet okCallFn = null;\n\tlet backCallFn = null;\n\t\n\t/**\n\t * 初始化弹窗\n\t * 将弹窗HTML插入到页面中，并绑定事件\n\t */\n\tfunction initModal() {\n\t\t// 插入弹窗HTML到页面\n\t\tdocument.body.insertAdjacentHTML('beforeend', modalHTML);\n\t\t\n\t\t// 获取DOM元素\n\t\tpasswordModal = document.getElementById('passwordModal');\n\t\tpasswordInput = document.getElementById('passwordInput');\n\t\terrorMessage = document.getElementById('errorMessage');\n\t\tverifyBtn = document.getElementById('verifyBtn');\n\t\tbackBtn = document.getElementById('backBtn');\n\t\timageOverlay = document.getElementById('imageOverlay');\n\t\tenlargedImage = document.getElementById('enlargedImage');\n\t\t\n\t\t// 绑定事件\n\t\tbindEvents();\n\t}\n\t\n\t/**\n\t * 绑定所有事件\n\t */\n\tfunction bindEvents() {\n\t\t// 触发按钮点击事件\n\t\t// document.getElementById('accessBtn').addEventListener('click', openModal);\n\t\t\n\t\t// 验证按钮点击事件\n\t\tverifyBtn.addEventListener('click', validatePassword);\n\t\t\n\t\t// 返回按钮点击事件\n\t\tbackBtn.addEventListener('click', function(){\n\t\t\tcloseModal();\n\t\t\tbackCallFn();\n\t\t});\n\t\t\n\t\t// 输入框回车事件\n\t\tpasswordInput.addEventListener('keypress', function(e) {\n\t\t\tif (e.key === 'Enter') {\n\t\t\t\tvalidatePassword();\n\t\t\t}\n\t\t});\n\t\t\n\t\t// 只在非移动端绑定图片点击事件\n\t\tif (!isMobileDevice()) {\n\t\t\t// 图片点击放大事件\n\t\t\tdocument.querySelectorAll('.qr-image').forEach(img => {\n\t\t\t\timg.addEventListener('click', function() {\n\t\t\t\t\tenlargeImage(this.src);\n\t\t\t\t});\n\t\t\t});\n\t\t\t\n\t\t\t// 放大图片关闭事件\n\t\t\timageOverlay.addEventListener('click', function(e) {\n\t\t\t\tif (e.target === this || e.target === enlargedImage) {\n\t\t\t\t\tcloseImageOverlay();\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t\t// ESC键关闭放大图片\n\t\t\tdocument.addEventListener('keydown', function(e) {\n\t\t\t\tif (e.key === 'Escape' && imageOverlay.classList.contains('active')) {\n\t\t\t\t\tcloseImageOverlay();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\t\n\t/**\n\t * 检测是否为移动设备\n\t * @returns {boolean} 是否为移动设备\n\t */\n\tfunction isMobileDevice() {\n\t\treturn window.innerWidth <= 768;\n\t}\n\t\n\t/**\n\t * 打开密码弹窗\n\t */\n\tfunction openModal() {\n\t\tpasswordModal.classList.add('active');\n\t\tpasswordInput.focus();\n\t\terrorMessage.classList.remove('show');\n\t\tpasswordInput.value = '';\n\t}\n\t\n\t/**\n\t * 关闭密码弹窗\n\t */\n\tfunction closeModal() {\n\t\tpasswordModal.classList.remove('active');\n\t}\n\t\n\t/**\n\t * 密码验证函数\n\t * 宽松验证策略：允许左右空格，中间空格可省略\n\t */\n\tfunction validatePassword() {\n\t\tlet enteredPassword = passwordInput.value.trim(); // 去除左右空格\n\t\t\n\t\t// 标准化：移除所有空格\n\t\tconst normalizedEntered = enteredPassword.replace(/\\s+/g, '');\n\t\tconst normalizedCorrect = CONFIG.correctPassword.replace(/\\s+/g, '');\n\t\t\n\t\tif (normalizedEntered === normalizedCorrect) {\n\t\t\t// 密码正确，解锁章节\n\t\t\tunlockChapter();\n\t\t} else {\n\t\t\t// 密码错误，显示错误信息\n\t\t\tshowError();\n\t\t}\n\t}\n\t\n\t/**\n\t * 显示密码错误提示\n\t */\n\tfunction showError() {\n\t\terrorMessage.classList.add('show');\n\t\tpasswordInput.value = '';\n\t\tpasswordInput.focus();\n\t\t\n\t\t// 添加抖动效果\n\t\tpasswordInput.classList.add('shake');\n\t\tsetTimeout(() => {\n\t\t\tpasswordInput.classList.remove('shake');\n\t\t}, 500);\n\t}\n\t\n\t/**\n\t * 解锁章节\n\t */\n\tfunction unlockChapter() {\n\t\tcloseModal();\n\t\tokCallFn();\n\t\t\n\t\t\n\t\t// // 更新章节内容\n\t\t// const lockedSection = document.querySelector('.locked-section');\n\t\t// const tocLockedItems = document.querySelectorAll('.toc a.locked');\n\t\t\n\t\t// // 更新章节显示\n\t\t// lockedSection.innerHTML = `\n\t\t// \t<h3><i class=\"fas fa-unlock-alt\" style=\"color:#2ecc71;\"></i> 章节已解锁</h3>\n\t\t// \t<p>感谢您加入我们的社区！现在您可以查看高级配置指南的全部内容。</p>\n\t\t// \t<div style=\"text-align: left; margin-top: 20px;\">\n\t\t// \t\t<h4>高级配置内容示例：</h4>\n\t\t// \t\t<p>1. 自定义插件开发：详细讲解如何为项目开发自定义插件，包括插件结构、API接口和最佳实践。</p>\n\t\t// \t\t<p>2. 性能调优指南：深入分析项目性能瓶颈，并提供多种优化方案和调优技巧。</p>\n\t\t// \t\t<p>3. 高级集成方案：介绍如何将项目与其他流行框架和工具进行深度集成。</p>\n\t\t// \t\t<p>4. 企业级部署：针对生产环境的企业级部署方案，包括高可用、负载均衡和监控配置。</p>\n\t\t// \t</div>\n\t\t// \t<p style=\"margin-top: 20px; color: #27ae60; font-weight: 600;\">\n\t\t// \t\t<i class=\"fas fa-check-circle\"></i> 您现在可以访问所有高级章节内容了！\n\t\t// \t</p>\n\t\t// `;\n\t\t\n\t\t// // 更新目录状态\n\t\t// tocLockedItems.forEach(item => {\n\t\t// \tif (item.textContent.includes('高级配置指南')) {\n\t\t// \t\titem.classList.remove('locked');\n\t\t// \t\titem.innerHTML = '<i class=\"fas fa-unlock-alt\" style=\"color:#2ecc71;\"></i> 高级配置指南';\n\t\t// \t}\n\t\t// });\n\t\t\n\t\t// // 显示成功通知\n\t\t// showNotification('章节解锁成功！您现在可以访问高级配置指南。');\n\t}\n\t\n\t/**\n\t * 放大图片\n\t * @param {string} src - 图片地址\n\t */\n\tfunction enlargeImage(src) {\n\t\tenlargedImage.src = src;\n\t\timageOverlay.classList.add('active');\n\t}\n\t\n\t/**\n\t * 关闭图片放大层\n\t */\n\tfunction closeImageOverlay() {\n\t\timageOverlay.classList.remove('active');\n\t}\n\t\n\t/**\n\t * 显示通知\n\t * @param {string} message - 通知内容\n\t */\n\tfunction showNotification(message) {\n\t\tconst notification = document.createElement('div');\n\t\tnotification.style.cssText = `\n\t\t\tposition: fixed;\n\t\t\ttop: 20px;\n\t\t\tright: 20px;\n\t\t\tbackground-color: #2ecc71;\n\t\t\tcolor: white;\n\t\t\tpadding: 15px 25px;\n\t\t\tborder-radius: 4px;\n\t\t\tbox-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);\n\t\t\tz-index: 1001;\n\t\t\tfont-weight: 600;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 10px;\n\t\t\ttransform: translateX(150%);\n\t\t\ttransition: transform 0.5s ease;\n\t\t`;\n\t\t\n\t\tnotification.innerHTML = `<i class=\"fas fa-check-circle\"></i><span>${message}</span>`;\n\t\tdocument.body.appendChild(notification);\n\t\t\n\t\t// 显示通知\n\t\tsetTimeout(() => {\n\t\t\tnotification.style.transform = 'translateX(0)';\n\t\t}, 10);\n\t\t\n\t\t// 3秒后隐藏\n\t\tsetTimeout(() => {\n\t\t\tnotification.style.transform = 'translateX(150%)';\n\t\t\tsetTimeout(() => {\n\t\t\t\tdocument.body.removeChild(notification);\n\t\t\t}, 500);\n\t\t}, 3000);\n\t}\n\t\n\t/**\n\t * 添加抖动动画样式\n\t */\n\tfunction addShakeAnimation() {\n\t\tconst style = document.createElement('style');\n\t\tstyle.textContent = `\n\t\t\t@keyframes shake {\n\t\t\t\t0%, 100% { transform: translateX(0); }\n\t\t\t\t10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }\n\t\t\t\t20%, 40%, 60%, 80% { transform: translateX(5px); }\n\t\t\t}\n\t\t\t.shake {\n\t\t\t\tanimation: shake 0.5s;\n\t\t\t\tborder-color: #e74c3c !important;\n\t\t\t}\n\t\t`;\n\t\tdocument.head.appendChild(style);\n\t}\n\t\n\t// 初始化\n\tdocument.addEventListener('DOMContentLoaded', function() {\n\t\tinitModal();\n\t\taddShakeAnimation();\n\t});\n\t\n\t// 显示弹窗： 验证成功的回调、点击返回的回调 \n\twindow.showDocLock = function(okFn, backFn) {\n\t\tokCallFn = okFn;\n\t\tbackCallFn = backFn;\n\t\t// 打开 \n\t\topenModal();\n\t}\n\t\n};\ninitTanChuangFun();"
  },
  {
    "path": "sa-token-doc/static/doc.css",
    "content": "/* 调整一下左侧树的样式 */\nbody{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;}\n\n#main {padding-bottom: 100px;}\n#main h2 {font-size: 1.6rem;}\n#main h3 {font-size: 1.25rem;}\n\n.main-box .markdown-section{ /* padding: 38px 20px; */ max-width: 100%; /* margin-left: 12%; */}\n.main-box .markdown-section h4{font-size: 1rem;}\n\n.main-box red, .main-box red *{ color: red !important; }\n.main-box green, .main-box green *{ color: green !important; }\n.main-box blue, .main-box blue *{ color: blue;!important; }\n.main-box purple, .main-box purple *{ color: purple;!important; }\n.main-box question, .main-box question *{ color: purple;!important; }\n\n\n\n/* ------- 多设备适配 start ------- */\n.sub-nav-draw-box{ display: none; }\nbody{\n\t--doc-left-width: 300px;\n\t--doc-context-width: 1000px;\n\t--doc-right-width: 300px;\n}\n\n/* 大于 1100px，就显示左中右结构 */\n@media screen and (min-width: 1100px) {\n\t.doc-right-bj-box{ display: block; }\n\t.main-box .content{left: 0;}\n\t.main-box .markdown-section{width: var(--doc-context-width); padding: 38px 20px; border: 0px green solid;}\n\t.main-box .doc-right-bj-box{left: calc(50% + (var(--doc-context-width) / 2) + 10px);}\n\t.main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar{ \n\t\tposition: fixed;\n\t\ttop: 120px;\n\t\tleft: calc(50% + (var(--doc-context-width) / 2) + 10px);\n\t\twidth: var(--doc-right-width) !important;\n\t\tborder: 0px #000 solid;\n\t\tline-height: 1.4em;\n\t\twidth: calc(300px - 25px);\n\t\tmax-height: 50vh;\n\t\toverflow: auto;\n\t}\n\t.main-box .sidebar{width: var(--doc-left-width);}\n\t.main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar::-webkit-scrollbar{ width: 0px; }\n\t.main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar li.active a{ color: #42B983; }\n\t.main-box .sidebar-nav>ul>li>ul>li>.app-sub-sidebar li a{ font-size: 12px; color: #888; }\n\t/* .main-box .app-sub-sidebar{display: none;} */\n}\n/* 小于 1100px时 */\n@media screen and (max-width: 1100px) {\n\t.doc-right-bj-box{ display: none; }\n}\n\n\n\n/* 大于 1600px */\n@media screen and (min-width: 1600px) {\n\tbody{\n\t\t--doc-left-width: 300px;\n\t\t--doc-context-width: 1000px;\n\t\t--doc-right-width: 300px;\n\t}\n}\n/* 小于 1100px - 1600px 之间 */\n@media screen and (max-width: 1600px) {\n\tbody{\n\t\t--doc-left-width: 200px;\n\t\t--doc-context-width: calc( 100vw - 400px - 50px);\n\t\t--doc-right-width: calc(200px - 20px);\n\t\t\n\t\t/* 窄屏幕时，广告更换为上下显示 */\n\t\t.main-box .top-ad-box .mad-img{ float: none; margin-right: 10px;  border: 0px; }\n\t\t.main-box .top-ad-box .mad-text{ float: none; display: block; width: 100%; margin-top: 10px; color: #000; word-break: normal; }\n\t}\n}\n/* 小于 1100px时 */\n@media screen and (max-width: 1100px) {\n\t.doc-right-bj-box{ display: none; }\n}\n/* 小于 800px时 */\n/* @media screen and (max-width: 800px) {\n\t.doc-right-bj-box{ display: none; }\n} */\n\n\n/* 媒体查询 */\n@media screen and (max-width: 800px) {\n\t.nav-left .logo-box .logo-text,\n\t.nav-left .logo-box sub{display: none;}\n\t/* .main-box .markdown-section{max-width: 1000px; margin-left: auto; margin-top: 40px;} */\n}\n/* 手机端不显示广告，和一些其它东西 */\n@media (max-width: 576px) {.wwads-cn,.p-none{display:none!important}} \n\n/* ------- 多设备适配 end ------- */\n\n\n/* 右侧盒子 */\n.doc-right-bj-box{\n\twidth: var(--doc-right-width);\n\tpadding: 10px;\n\tposition: fixed;\n\tmargin-top: 10px;\n\ttop: 60px;\n\tborder: 0px #000 solid;\n\tfont-size: 12px;\n}\n.doc-right-bj-box-title{ font-size: 14px; color: #888; padding-bottom: 8px; border-bottom: 1px #aaa solid; }\n.doc-right-more-item{ position: absolute; border: 0px #000 solid; color: #000; width: 100%;}\n\n\n\n\n/* ------- 头部样式 ------- */\n.doc-header{position: fixed; top: 0; z-index: 1000; width: 100%; height: 60px; line-height: 60px;}\n.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);}\n\n/* 左边导航 */\n.nav-left{display: inline-block; float: left;}\n.logo-box {display: inline-block; cursor: pointer; color: #000; padding-left: 24px; height: 60px; line-height: 60px;}\n.logo-box img {width: 50px; height: 50px; vertical-align: middle; position: relative; top: -1px; margin-right: 5px;}\n.logo-box .logo-text {display: inline-block; margin: 0; padding: 0; color: #000; vertical-align: middle; font-size: 26px;font-weight: 500;}\n.logo-box sub{margin-left: 5px; color: #666;}\n\n/* 右边导航 */\n.doc-header .nav-right{margin: 0;  float: right; padding-right: 3em; margin-right: 20px !important;}\n.doc-header .nav-right>*{padding: 0px; margin: 0 10px;}\n.doc-header .nav-right>*:last-child{position: relative; z-index: 1002;}\n.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;}\n.doc-header .nav-right>select:hover{box-shadow: 0 0 10px #aaa;}\n\n.github-corner{z-index: 1001 !important;}\n.doc-header .nav-right .wzi{font-size: 14px; line-height: 61px; transition: color 0.2s; padding-bottom: 4px;}\n.doc-header .nav-right .wzi:hover{border-bottom: 2px var(--a-color) solid;}\n\n.nav-right a{color: #34495E;}\n\n/* 搜索框 */\n.sear-box{display: inline-block; width: 180px; margin-right: 20px; line-height: 26px; text-align: left;}\n.sear-box{/* position: fixed; */ }\n.sear-box .search{margin-bottom: 0px; padding: 0; border: 0;}\n.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;}\n.sear-box .search input{border: 1px solid #e3e3e3; color: #345; border-radius: 15px; line-height: 30px; padding-left: 30px; transition: all 0.2s;}\n.sear-box .search input{background: #fff url(./search-icon.svg) 10px 8px no-repeat; background-size: 14px;}\n.clear-button{display: none !important;}\n\n/* 工具栏超链接 展开、收缩div */\n.zk-box{display: inline-block;}\n/* 外层盒 */\n.zk-box .zk-context{max-height: 0px; position: absolute; overflow: hidden;}\n.zk-box:hover .zk-context{max-height: 400px;}\n/* 内层盒 */\n.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;}\n.zk-box:hover .zk-context>div{opacity: 1;}\n/* 小链接 */\n.zk-box .zk-context a{font-size: 14px; display: block; line-height: 32px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}\n.zk-box .zk-context a{text-align: left; padding: 0 1.5em 0 1em;}\n.zk-box .zk-context .zk-fengexian{border-bottom: 1px #d9d9d9 solid; margin: 10px 0;}\n\n/* 下三角小图标 */\n.zk-icon{display: inline-block; width: 0px; height: 0px; position: relative;top: 3px; margin-left: 4px;}\n.zk-icon{border-style: solid; border-width: 5px; border-color: #aaa transparent transparent transparent; }\n\n/* 版本选择按钮 */\n.select-version{background-color: transparent !important;}\n\n\n/* ------- 调整一下左侧树的字体样式 ------- */\n.main-box .sidebar{padding-top: 25px; margin-top: 60px;}\n.sidebar .sidebar-nav>ul>li>p{/* font-size: 1.2em; */ margin-top: 10px;}\n.sidebar .sidebar-nav>ul>li> strong{/* font-size: 1.2em; */ margin-top: 10px;}\n/* .sidebar ul li a{color: #222;} */\n.sidebar .sidebar-nav>ul>li>ul>li>a{/* color: #222; */font-size: 14px; /* font-weight: 700; */}\n.main-box .sidebar-nav ul li{margin-top: 0; margin-bottom: 0;}\n.main-box .sidebar ul li a{color: #00323c;}\n\n/* 做到悬浮出现下划线的效果 */\n.main-box .sidebar>.sidebar-nav>ul{padding-left: 6px;}\n.main-box .sidebar li a:hover{color: #42b983;}\n/* .main-box .sidebar li{white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin: 5px 0;}\n.main-box .sidebar li a{display: inline; line-height: 30px; padding: 5px 0 2px;}\n.main-box .sidebar li a:hover{text-decoration: none; color: #42b983; border-bottom: 1px #42b983 solid;}\n.main-box .sidebar li.active>a{border: 0px;}\n.main-box .sidebar li.active>a:after{content: ''; position: absolute; height: 30px; right: 0; border-right: 3px #42b983 solid;} */\n\n.sidebar .sidebar-nav>ul>li>ul>li.active-rep>a{ color: #42B983; font-weight: 700; }\n\n\n\n/* .main-box .sidebar .app-sub-sidebar li:before{float: none;} */\n\n/* ============== code代码样式优化 ================ */\n\n.main-box .markdown-section code, .main-box .markdown-section pre{background-color: rgba(0, 0, 0, 0.04);}\n\n/* 背景变黑 */\n.main-box [data-lang]{padding: 0px !important; border-radius: 2px;overflow-x: auto; overflow-y: hidden;}\n.main-box [v-pre] code{border: 0px red solid; border-radius: 0px; /* background-color: #282828; */ background-color: #191919; color: #FFF;}\n.main-box [v-pre] code{padding: 1.5em 1.3em; margin-left: 40px !important;}\n/* .main-box h2{margin-top: 70px;} */\n\n/* 代码行号盒子样式 */\n.code-line-box {list-style-type: none; border-right: 1px solid #000; position: absolute; top: 0; left: 0; width: 40px;  user-select: none;}\n.code-line-box {padding: calc(1.5em + 1px)  0px !important; padding-bottom: calc(1.5em + 20px) !important; margin: 0px !important;}\n.code-line-box {line-height: inherit !important; background-color: #191919; color: #aaa;font-weight: 400;font-size: 0.85em;text-align: center;}\n\n/* xml语言样式优化 */\n/* .lang-xml .token.comment{color: #CDAB53;} */\n.lang-xml .token.tag *{color: #db2d20;}\n.lang-xml .token.attr-value{color: #A6E22E;}\n\n/* html语言样式优化 */\n.lang-html .token.comment{color: #CDAB53;}\n.lang-html .token.tag *{color: #db2d20;}\n.lang-html .token.tag .attr-name,\n.lang-html .token.tag .attr-name *{color: #A6E22E; opacity: 0.9;}\n.lang-html .token.tag .attr-value,\n.lang-html .token.tag .attr-value *{color: #E6DB74; opacity: 0.9;}\n.lang-html .token.annotation.punctuation{color: #ddd;}\n.lang-html .token.punctuation{color: #ddd;}\n\n/* java语言样式优化 */\n.main-box .lang-java{color: #01a252 !important;; opacity: 1;}\n.lang-java .token.keyword{color: #db2d20;}\n.lang-java .token.namespace,.lang-java .token.namespace *{color: #01A252; opacity: 1;}\n.lang-java .token.class-name,.lang-java .cm-variable{color: #55b5db; opacity: 1;}\n/* .lang-java .token.comment{color: #CDAB53;} */\n.lang-java .token.annotation.punctuation{color: #ddd;}\n.lang-java .token.punctuation{color: #ddd;}\n\n/* cmd语言样式优化 */\n.main-box .lang-cmd{color: #01A252 !important; opacity: 1;}\n\n/* url语言样式优化 */\n.main-box .lang-url{color: #E96917 !important; opacity: 1;}\n\n/* js语言样式优化 */\n.main-box .lang-js{color: #01a252 !important;}\n/* .lang-js .token.comment{color: #CDAB53;} */\n/* .lang-js .token.string{color: #fded02;} */\n.lang-js .token.string{color: #ddd;}\n.lang-js .token.punctuation{color: #ddd;}\n\n/* yaml 和 properties 语言优化 */\n.lang-yaml .token.punctuation{color: #eee;}\n.lang-properties .token.attr-name{color: #22a2c9;}\n\n\n/* ------- markdown 内容样式优化 ------- */\n\n/* GitHub折线图最大宽度 */\n[alt=github-chart]{max-width: 897px;}\n/* 大屏幕时，某些图片限制一下宽度 */\n@media screen and (min-width: 800px) {\n\t[title=s-w],[title=s-w-sh]{max-width: 80%;}\n\t.s-w,.s-w-sh{ max-width: 80%; }\n}\n.s-w-sh, [title=s-w-sh]{display: inline-block; border: 1px #eee solid;}\n.w-100, [title=w-100]{display: inline-block; border: 1px #eee solid; max-width: 100%;}\n\n/* 鼠标悬浮时切换img */\n.hover-change-img {border: 1px #eee solid; max-width: 100%; }\n.hover-change-img:hover img:first-child{ display: none; }\n.hover-change-img img:last-child{ display: none; }\n.hover-change-img img:first-child{ display: inline-block; }\n.hover-change-img:hover img:last-child{ display: inline-block; }\n\n/* 公众号table */\n.gzh-table{ /* table-layout:fixed !important; */}\n/* .gzh-table,.gzh-table tr,.gzh-table td{display: block !important;} */\n/* .gzh-table tbody{display: block !important; width: 100% !important;} */\n#main .gzh-table tr{background-color: #FFF;}\n.gzh-table td{padding: 20px !important; width: 20%; border: 0;}\n.gzh-table td b{display: block; margin-bottom: 10px; }\n\n/* tab选项卡优化 */\n/* .docsify-tabs--classic{background-color: rgba(255, 255, 255, 0.2);} */\n.docsify-tabs__tab{outline: 0; cursor: pointer;}\n.docsify-tabs--classic .docsify-tabs__tab--active{box-shadow: 0 0 0;}\n/* tab卡片插件样式优化 */\n.main-box{\n\t--docsifytabs-border-color: #ddd;\n\t--docsifytabs-tab-color: #777;\n}\n\n\n/* 调整表格的响应式 */\n#main table{margin-left: 0px;}\n@media screen and (min-width: 800px) {\n\t#main table tr th{min-width: 100px;}\n}\n\n/* 提示框加上灰色背景 */\n.main-box .markdown-section blockquote{padding: 1px 24px 1px 30px; background-color: #f8f8f8;}\n\n/* 行级代码样式 */\nblockquote code {font-weight: 400;}\n\n/* 赞助列表 */\n.zanzhu-box{margin-top: -10px;}\n.zanzhu-box table tr td:nth-child(2){color: red;}\n#main .zanzhu-box table tr td:first-child a{border-color: rgba(0,0,0,0); color: inherit;}\n#main .zanzhu-box table tr td:first-child a:hover{border-color: var(--a-hover-color); color: var(--a-hover-color);}\n\n/* 展开和收起 */\n#main .zanzhu-box{/* height: 500px; */ overflow-y: hidden; transition: all 1.5s;}\n#main .zanzhu-box table{display: table;}\n.zhankai-btn-box{margin-top: 10px;}\n.zk-btn--1,.zk-btn--2{cursor: pointer;}\n.zk-btn--2{display: none;}\n\n/* 角标位置修复 */\n.badge-box a:nth-child(-n+2) img{position: relative; top: 1px;}\n\nbody {\n  --a-color: #01a252;\n  --a-hover-color: #0969da;\n}\n\n/* 超链接样式 */\n#main *:not(h1,h2,h3,h4,h5,h6) a{font-weight: 400; text-decoration: none; font-family: \"思源黑体\";}\n#main *:not(h1,h2,h3,h4,h5,h6) a{color: var(--a-color); border-bottom: 1px var(--a-color) solid;}\n#main *:not(h1,h2,h3,h4,h5,h6) a:hover{color: var(--a-hover-color); border-bottom: 1px var(--a-hover-color) solid;}\n\n#main .un-dec-a-pre+p a,\n#main p[align=center] a{border-bottom:0px;}\n\n\n/* toc目录树 */\n.toc-box>li{margin-bottom: 15px;}\n.toc-box .toc-h2{list-style-type: none; font-size: 18px; margin-top: 20px;}\n.toc-box .toc-h3,.toc-box .toc-h4{margin-left: 1em;}\n.toc-box .toc-h5,.toc-box .toc-h6{margin-left: 2em;}\n#main .toc-box .toc-h2 a span{color: #34495e;}\n#main .toc-box a{border-color: rgba(0,0,0,0); transition: 0s;}\n#main .toc-box a span{color: inherit;}\n\n\n/* 加载图片的按钮 */\n.show-img{\n\tbackground-color: #FFF;\n\tpadding: 8px 15px;\n\tborder: 1px #42b983 solid;\n\tcolor: #42b983;\n\tcursor: pointer;\n\tborder-radius: 2px;\n\ttransition: all 0.2s;\n}\n.show-img:hover{\n\tbackground-color: #eaf6eb;\n}\n.show-to-img{cursor: pointer;}\n\n/* 导航栏悬浮时出现下滑条条 */\n/* .doc-header .nav-right .wzi::after {\n    content: '';\n    width: 0%;\n\tfloat: left;\n    display: inline-block;\n\ttext-align: center;\n\tmargin-top: -15px;\n    border-bottom: 2px var(--a-color) solid;\n\ttransition: all 0.2s;\n}\n.doc-header .nav-right .wzi:hover::after {width: 100%;} */\n\n\n/* 保证点开图片时在最上面 */\n.medium-zoom-image.medium-zoom-image--opened{\n\tz-index: 10000;\n}\n\n\n/* -------------  答题按钮 ------------- */\n#main .dt-btn,#main .case-btn{\n\tbackground-color: #e7ecf3;\n\tcolor: #385481;\n\tdisplay: inline-block;\n\tborder: 1px #d7dce3 solid !important;\n\tborder-radius: 1px;\n\t/* border-bottom-width: 0px !important; */\n\t\n\tmargin-top: 10px;\n\twidth: 100%;\n\tpadding: 8px 14px;\n\tfont-size: 14px;\n\ttransition: all 0.15s;\n\ttext-decoration: none !important;\n\tfont-weight: 400;\n\t\n\t/* 背景 */\n\tbackground-image: url(icon/dati.svg);\n\tbackground-repeat: no-repeat;\n\tbackground-size: 20px 20px;\n\tbackground-position: 1em 12px;\n\ttext-indent: 2em;\n}\n/* 代码示例按钮 */\n#main .case-btn{\n\tbackground-color: #feedeb;\n\tcolor: #dd4949;\n\tborder: 1px #decdcb solid !important;\n\tbackground-size: 18px 18px;\n\tbackground-image: url(icon/code.svg);\n}\n#main .case-btn.case-btn-video{ \n\tbackground-color: #ECF5FF; \n\tcolor: #1979DA; \n\tborder: 1px #49A9DA solid !important;\n\tbackground-image: url(icon/video.svg);\n}\n#main .dt-btn{display: none;}\n\n\n/* -------------  背景色相关 ------------- */\n/* 侧边栏需要透明 */\n.sidebar-toggle{background-color: transparent !important;}\n.sidebar{background-color: transparent !important;}\n\n/* 变色的动画 */\n.doc-header{transition: background-color 0.3s !important;}\n\n/* 调色按钮 */\n.theme-btn{width: 25px; height: 25px; line-height: 60px; vertical-align: middle; position: relative; top: -1px;}\n.theme-box{width: 156px; text-align: left; line-height: 20px; margin-top: -20px;}\n.theme-box span{\n\tdisplay: inline-block;\n\twidth: 20px; \n\theight: 20px; \n\tmargin: 1px 2px; \n\tborder: 1px #ccc solid; \n\tcursor: pointer;\n\tborder-radius: 1px;\n}\n\n/* -------------  details标签 ------------- */\n.main-box details{\n\tborder: 1px #42B983 solid;\n\tbackground-color: #f4fdef;\n\toverflow: hidden;\n\tmax-height: 44px;\n\tmargin-bottom: 1em;\n\t/* transition: all 1s; */\n}\n.main-box details[open]{ /* max-height: 1000px; */ overflow: auto; animation: slideDown 0.6s linear both;}\n@keyframes slideDown {\n    0% { max-height: 44px; overflow: hidden; }\n    99% { max-height: 1500px; overflow: hidden; }\n    100% { max-height: 1500px; overflow: auto; }\n}\n.main-box details summary{\n\tpadding: 11px 14px;\n\tbackground-color: #f4fdef;\n\tcolor: #01a252;\n\tcursor: pointer;\n}\n.main-box details pre{\n\tmargin-left: 1em;\n\tmargin-right: 1em;\n}\n.main-box details table{margin-left: 1em !important; margin-right: 1em; width: auto;}\n.main-box details p{padding: 0 14px;}\n\n/* 广告盒子 */\n.ad-title{ font-size: 14px; color: #aaa; padding-bottom: 8px; margin-bottom: 14px; border-bottom: 1px #aaa solid; }\n.ad-tips{margin-bottom: 5px;}\n.ad-close{float: right;}\n.ad-close:hover{cursor: pointer; text-decoration: underline; color: red;}\n\n.main-box .top-ad-box{font-size: 12px;}\n.main-box .top-ad-box a{border-bottom: 0px; text-decoration: none;}\n.main-box .top-ad-box a:hover{border-bottom: 0px;}\n.main-box .top-ad-box a img{border: 1px #eee solid; width: 100%; /* max-height: 80px; */ border-radius: 2px; transition: all 0.1s !important;}\n.main-box .top-ad-box a img:hover{box-shadow: 0 0 20px #ddd;}\n\n.mad-bg-box{ padding: 10px; background-color: #F4F8FA; /* border: 1px #eee solid; */ overflow: hidden; }\n.mad-wh-box{ width: 100%; }\n.main-box .top-ad-box .mad-img{ width: 130px; float: left; margin-right: 10px;  border: 0px; }\n.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; }\n\n.main-box .top-ad-box2 a img{ width: 48.5%; margin-bottom: 2px; }\n.main-box .top-ad-box2 a:nth-child(2n+1) img{margin-right: 2px; }\n\n\n/* 帮助按钮 */\n.help-btn{transition: all 0.5s; text-align: center; border: 1px #42b983 solid; background-color: rgba(255, 255, 255, 0.5); cursor: pointer; font-size: 13px; color: #42b983; line-height: 40px;}\n.help-btn:hover{box-shadow: 0 0 20px #D1EEE1 !important;}\n.xiaozhushou-intro p{line-height: 14px;}\n\n/* ew-wa */\n.ew-wa{ margin-top: 14px; line-height: 18px; color: #aaa; }\n.ew-wa a{ margin-right: 5px; text-decoration: none; color: #8693A7; }\n.ew-wa a:hover{text-decoration: underline; color: #44f; }\n\n/* 按钮发光动画 */\n/* .help-btn{animation: helpbtnanimation 3s infinite;}\n@keyframes helpbtnanimation{\n    0%{box-shadow: 0 0 1px #42B983;}\n    50%{box-shadow: 0 0 20px #42B983;}\n    100%{box-shadow: 0 0 20px #FFF;}\n} */\n\n/* ********** 赞助者名单 ******** */\n.zanzhu-table{text-align: left;}\n/* 赞助排序盒子 */\n.zanzhu-sort-box{font-size: 14px; margin-bottom: 10px;}\n.zanzhu-sort-box .zanzhu-sort-btn{text-decoration: none; color: #999; cursor: pointer;}\n.zanzhu-sort-box .zanzhu-sort-btn:hover{text-decoration: underline; color: #557;}\n.zanzhu-sort-box .zanzhu-sort-btn.zz-sort-native{text-decoration: underline; color: #557;}\n/* 底部按钮盒子 */\n.zz-btn-box{color: #666; font-size: 14px;}\n.zz-btn-box button{padding: 5px 10px; cursor: pointer; border: 1px #ccc solid; color: #999; background-color: #FFF;}\n.zz-btn-box button:hover{box-shadow: 0 0 10px #ddd;}\n\n.syzz-show-btn{border: 1px #ccc solid; padding: 5px 10px; background-color: #FFF; color: #666; cursor: pointer;}\n.syzz-show-btn:hover{box-shadow: 0 0 10px #ddd;}\n\n/* ********** 团队成员名单 ******** */\n.markdown-section .team-table{ display: table; text-align: left; }\n.team-table img{ width: 45px; height: 45px; }\n\n/* ajax加载时的转圈圈样式 */\n.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }\n\n\n/* 万维广告 */\n.wwads-cn{margin-top: 0px !important;}\n.wwads-cn>a>img{width: 80px !important;}\n\n/* 提示框 */\n.main-box .alert{ border-radius: 0px !important; }\n/* .main-box .alert ul,.main-box .alert ol{ margin-top: -5px; margin-bottom: -10px; } */\n.main-box .alert.tip .title .icon.icon-tip{\n\tbackground-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\");\n}\n.main-box .alert.flat.note{background-color: #E8F4FF;}\n.main-box .alert.flat.tip{background-color: #F0F9EB;}\n.main-box .alert.flat.warning{background-color: #FDF6EC;}\n"
  },
  {
    "path": "sa-token-doc/static/docsify-plugin.js",
    "content": "// 声明 docsify 插件\nvar myDocsifyPlugin = function(hook, vm) {\n\t\n\t// 钩子函数：解析之前执行\n\thook.beforeEach(function(content) {\n\t\ttry{\n\t\t\t// 功能 1，替换全局变量 \n\t\t\tcontent = content.replace(/\\$\\{sa.top.version\\}/g, window.saTokenTopVersion);\n\t\t\t\n\t\t\t// 添加 [toc] 标记\n\t\t\tcontent = content.replace(/\\[\\[toc\\]\\]/g, '<div class=\"toc-box\"></div>');\n\t\t\t\n\t\t}catch(e){\n\t\t\t// \n\t\t}\n\t\treturn content;\n\t});\n\t\n\t// 钩子函数：每次路由切换时，解析内容之后执行 \n\thook.afterEach(function(html) {\n\t\t\n\t\t// 功能 2，文章底部添加仓库地址  \n\t\tvar url = 'https://gitee.com/dromara/sa-token/tree/dev/sa-token-doc/' + vm.route.file;\n\t\tvar url2 = 'https://github.com/dromara/sa-token/tree/dev/sa-token-doc/' + vm.route.file;\n\t\tvar footer = [\n\t\t\t'<br/><br/><br/><br/><br/><br/><br/><hr/>',\n\t\t\t'<footer>',\n\t\t\t'<span>发现错误？ 您可以在 <a href=\"' + url + '\" target=\"_blank\">Gitee</a> 或 <a href=\"' + url2 +\n\t\t\t'\" target=\"_blank\">GitHub</a> 帮助我们完善此页文档！</span>',\n\t\t\t'或 <a href=\"#/more/join-group\">加入讨论群</a> 交流反馈。',\n\t\t\t'<br/><br/>',\n\t\t\t'<a href=\"https://beian.miit.gov.cn/\" target=\"_blank\" style=\"color:#aaa; border-color: #aaa;\">鲁ICP备18046274号-4</a>',\n\t\t\t'</footer>'\n\t\t].join('');\n\t\treturn html + footer;\n\t});\n\t\n\t// 钩子函数：每次路由切换时数据全部加载完成后调用，没有参数。\n\thook.doneEach(function() {\n\t\t\n\t\t// 功能3，给代码盒子，添加行数样式 \n\t\t$('pre code').each(function(){\n\t\t\tvar lines = $(this).text().split('\\n').length;\n\t\t\tvar $numbering = $('<ul/>').addClass('code-line-box');\n\t\t\t$(this)\n\t\t\t\t.addClass('has-numbering')\n\t\t\t\t.parent()\n\t\t\t\t.append($numbering);\n\t\t\tfor(i=1;i<=lines;i++){\n\t\t\t\t$numbering.append($('<li/>').text(i));\n\t\t\t}\n\t\t});\n\t\t\n\t\t// 功能4，添加 toc 目录 \n\t\tvar dStr = \"\";\n\t\t$('#main h2, #main h3, #main h4, #main h5, #main h6').each(function() {\n\t\t\t$('.toc-box').append('<li class=\"toc-' + this.localName + '\">' + this.innerHTML + '</li>');\n\t\t});\n\t\t\n\t\t// 功能5，统计赞助人数\n\t\t// if($('.zanzhu-count').length && $('.zanzhu-box table').length) {\n\t\t// \t$('.zanzhu-count').html($('.zanzhu-box table tr').length);\n\t\t// }\n\t\t\n\t\t// 功能5，渲染赞助数据 \n\t\tif($('.zanzhu-table').length) {\n\t\t\t// $('.zanzhu-count').html($('.zanzhu-box table tr').length);\n\t\t\t// console.log(123);\n\t\t\trenderDonateTable();\n\t\t\tonZanzhuSortClick();\n\t\t}\n\t\t\n\t\t// 功能6：标题下面的广告 \n\t\t// if(vm.route.path !== '/' && $(window).width() >= 800) {\n\t\t// \tvar ad = `<p class=\"top-ad-box\">\n\t\t// \t\t<span class=\"ad-tips\">推广信息：</span>\n\t\t// \t\t<span class=\"ad-tips ad-close\">关闭</span>\n\t\t// \t\t<a href=\"http://sa-pro.yun94.cn?from=satop\" target=\"_blank\">\n\t\t// \t\t\t<img src=\"https://oss.dev33.cn/sa-token/ad/sa-sso-pro-s3.png\" />\n\t\t// \t\t</a>\n\t\t// \t</p>`;\n\t\t\t\t\n\t\t// \t// 没有下划线就先补个下划线\n\t\t// \t// if($('#main h1').next().prop('tagName') !== 'HR') {\n\t\t// \t// \t$('#main h1').after('<hr/>');\n\t\t// \t// }\n\t\t\t\n\t\t// \t// 如果一周内用户点击过关闭广告，则不再展现\n\t\t// \tlet allowJg = 1000 * 60 * 60 * 24 * 7;\n\t\t// \t// allowJg = 1000 * 10;\n\t\t// \ttry{\n\t\t// \t\tconst closeAdTime = localStorage.closeAdTime;\n\t\t// \t\tif(closeAdTime) {\n\t\t// \t\t\t// 点击广告关闭的时间，和当前时间的差距\n\t\t// \t\t\tconst closeAdJg = new Date().getTime() - parseInt(closeAdTime);\n\t\t\t\t\t\n\t\t// \t\t\t// 差距小于七天，不再展示 \n\t\t// \t\t\tif(closeAdJg < allowJg) {\n\t\t// \t\t\t\tconsole.log('not show ad ...');\n\t\t// \t\t\t\treturn;\n\t\t// \t\t\t}\n\t\t// \t\t}\n\t\t// \t}catch(e){\n\t\t// \t\tconsole.error(e);\n\t\t// \t}\n\t\t\t\n\t\t\t\n\t\t// \t// 添加广告\n\t\t// \t// $('#main h1').after(ad);\n\t\t// \t$('.ssp-ad-box').append(ad)\n\t\t\t\n\t\t// \t// 添加关闭事件\n\t\t// \t$('.top-ad-box .ad-close').click(function(){\n\t\t// \t\tconsole.log('关闭广告');\n\t\t// \t\t// $('.top-ad-box').slideUp(); // 折叠收起\n\t\t// \t\tlayer.confirm('关闭后，一周内不再展现此信息', function(){\n\t\t// \t\t\t$(\".top-ad-box\").fadeOut(1000); // 淡出效果\n\t\t// \t\t\tlayer.msg('关闭成功');\n\t\t// \t\t\tlocalStorage.closeAdTime = new Date().getTime();\n\t\t// \t\t})\n\t\t// \t})\n\t\t// }\n\t\t\n\t});\n\t\n\t// 钩子函数：初始化并第一次加载完成数据后调用，没有参数。\n\thook.ready(function() {\n\t\t// 将搜索框转移到右上角 \n\t\tdocument.querySelector(\".sear-box\").innerHTML = '';\n\t\tdocument.querySelector(\".sear-box\").append(document.querySelector(\".search\"));\n\t\tdocument.querySelector(\".search input\").placeholder = '搜索…';\n\t\t\n\t\t// 点击input时，展开 \n\t\t$('.sear-box input').click(function() {\n\t\t\tif($('.search input').val() != '') {\n\t\t\t\t$('.results-panel').addClass('show');\n\t\t\t}\n\t\t});\n\t\t// 失去焦点时，收缩 \n\t\t$('.sear-box input').blur(function() {\n\t\t\tsetTimeout(function() {\n\t\t\t\t$('.results-panel').removeClass('show');\n\t\t\t}, 200);\n\t\t})\n\t\t// 选择一项时，收缩 \n\t\t$('.sear-box').on('click', '.matching-post', function() {\n\t\t\tconsole.log('click……');\n\t\t\t// $('.search input').val('');\n\t\t\t$('.results-panel').removeClass('show');\n\t\t});\n\t\t\n\t\t// 点击按钮，加载图片\n\t\t$(document).on('click', '.show-img', function(){\n\t\t\tvar src = $(this).attr('img-src');\n\t\t\tvar img = '<img class=\"show-to-img\" src=\"' + src + '\" />';\n\t\t\t$(this).after(img);\n\t\t\t$(this).remove();\n\t\t})\n\t\t\n\t\t// 点击按钮，加载图片\n\t\t$(document).on('click', '.show-to-img', function(){\n\t\t\topen(this.src);\n\t\t})\n\t\t\n\t});\n\t\n}"
  },
  {
    "path": "sa-token-doc/static/docsify-plugins/docsify-betterembed-1.1.1.js",
    "content": "const PMEregexGetImport=/<!-- embedImport:start:(.*?) -->(.*?)<!-- embedImport:end:(.*?) -->/gs,PMEregexReplaceImport=e=>new RegExp(`<!-- embedImport:start:${e} -->(.*?)<!-- embedImport:end:${e} -->`,\"gs\"),PMEregexGetImportName=/<!-- embedImport:start:(.*?) -->/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,`\n<!-- embedImport:start:${t} -->\n${r}\n<!-- embedImport:end:${t} -->\n`)}),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(`<!-- embed:start:${o} -->`)[1].split(`<!-- embed:end:${o} -->`)[0];a=a.replace(PMEregexReplaceImport(o),e)}),a})}window.$docsify=window.$docsify||{},$docsify.plugins=[partialMarkdownEmbed,...$docsify.plugins||[]];"
  },
  {
    "path": "sa-token-doc/static/docsify-plugins/docsify-plugin-flexible-alerts.min-1.1.1.js",
    "content": "/*!\n * docsify-plugin-flexible-alerts\n * v1.1.1\n * https://github.com/fzankl/docsify-plugin-flexible-alerts#readme\n * (c) 2022 Fabian Zankl\n * MIT license\n */\n!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]+?(?:<p>)?\\[!(\\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='<span class=\"icon '.concat(u,'\"></span>'),h='<p class=\"title\">'.concat(s?w:\"\").concat(g?m:\"\",\"</p>\");return'<div class=\"alert '.concat(d,\" \").concat(f,'\">\\n            ').concat(s||g?h:\"\",\"\\n            <p>\").concat(n,\"</p>\\n          </div>\")})))}))}),window.$docsify.plugins)}()}();\n//# sourceMappingURL=docsify-plugin-flexible-alerts.min.js.map"
  },
  {
    "path": "sa-token-doc/static/docsify-plugins/progress.update.js",
    "content": "// 显示文档阅读进度的进度条 \n// \n// 修改于：https://github.com/HerbertHe/docsify-progress\n// \n// 1、将最外层盒子的 z-index 值从 999 修改为 9999999999\n\nfunction plugin(hook, vm) {\n    let marginTop\n    hook.mounted(function () {\n        const content = document.getElementsByClassName(\"content\")[0]\n        marginTop = parseFloat(\n            window.getComputedStyle(content).paddingTop.replace(\"px\", \"\")\n        )\n\n        let insertDOM = `\n        <div style=\"position: fixed; width: 100%; z-index: 9999999999; height: ${\n            window.$docsify[\"progress\"].height\n        };\n        ${\n            window.$docsify[\"progress\"].position === \"top\"\n                ? \"top: 0;\"\n                : \"bottom: 0;\"\n        }\">\n            <div id=\"progress-display\" style=\"background-color: ${\n                window.$docsify[\"progress\"].color\n            }; width: 0; border-radius: 2px; height: ${\n            window.$docsify[\"progress\"].height\n        }; transition: width 0.3s;\"></div>\n        </div>\n        `\n        const mainDOM = document.getElementsByTagName(\"body\")[0]\n        mainDOM.innerHTML = mainDOM.innerHTML + insertDOM\n\n        function switcher() {\n            const body = document.getElementsByTagName(\"body\")[0]\n            if (!body.classList.contains(\"close\")) {\n                body.classList.add(\"close\")\n            } else {\n                body.classList.remove(\"close\")\n            }\n        }\n\n        const btn = document.querySelector(\"div.sidebar-toggle-button\")\n        btn.addEventListener(\"click\", function (e) {\n            e.stopPropagation()\n            switcher()\n        })\n    })\n    hook.ready(function () {\n        window.addEventListener(\"scroll\", function (e) {\n            let totalHeight =\n                marginTop +\n                parseFloat(\n                    window\n                        .getComputedStyle(document.getElementById(\"main\"))\n                        .height.replace(\"px\", \"\")\n                )\n            let scrollTop =\n                document.body.scrollTop + document.documentElement.scrollTop\n            let remain = totalHeight - document.body.offsetHeight\n            document.getElementById(\"progress-display\").style.width =\n                Math.ceil((scrollTop / remain) * 100) + \"%\"\n        })\n    })\n}\n\n// Docsify plugin options\nwindow.$docsify[\"progress\"] = Object.assign(\n    {\n        position: \"top\",\n        color: \"var(--theme-color,#42b983)\",\n        height: \"3px\",\n    },\n    window.$docsify[\"progress\"]\n)\nwindow.$docsify.plugins = [].concat(plugin, window.$docsify.plugins)"
  },
  {
    "path": "sa-token-doc/static/docsify-plugins/sub-nav-draw.js",
    "content": "// 提取次级导航栏显示到右上角 \n// \n\n// 是否都开右边菜单\nlet isOpenRightSubTitle = false;\n\n\t\t// 重新定位 active-rep 对应的菜单 \nfunction positioningVmActiveRep(vm) {\n\tconst vmPath = '#' + vm.route.path;\n\t$('.sidebar-nav>ul>li>ul>li>a').each(function(item) {\n\t\tif($(this).attr('href') === vmPath) {\n\t\t\t// $(this).parent().attr('active-rep', true);\n\t\t\t$(this).parent().addClass('active-rep')\n\t\t\t// console.log($(this));\n\t\t}\n\t})\n}\n\nfunction subNavDraw(hook, vm) {\n\t\n    // 钩子函数：每次路由切换时数据全部加载完成后调用，没有参数。\n    hook.doneEach(function () {\n\t\t// 只在宽屏下展现，太小的屏幕不展现 \n\t\tif(document.body.clientWidth < 1100) {\n\t\t\tisOpenRightSubTitle = false;\n\t\t\treturn;\n\t\t} else {\n\t\t\tisOpenRightSubTitle = true;\n\t\t}\n\t\t\n\t\t// 修改高度 \n\t\tconst $dom = $('.app-sub-sidebar');\n\t\tconsole.log($dom, $dom.height());\n\t\t$('.doc-right-more-item').css({ top: (($dom.height() ?? 0) + 80) + 'px' })\n\t\t\n\t\t// 重新定位 active-rep 对应的菜单 \n\t\tpositioningVmActiveRep(vm);\n    })\n\t\n\t\n\t// 钩子函数：初始化并第一次加载完成数据后调用，没有参数。\n\thook.ready(function () {\n\t\t\n\t})\n\t\n\t\n}\n\nwindow.$docsify.plugins = [].concat(subNavDraw, window.$docsify.plugins)\n\n// 滚动时设置一下左侧滚动条高度，不要超出可视区域 \n$(document).scroll(function(){\n\tif(isOpenRightSubTitle) {\n\t\ttry{\n\t\t\tconst offsetTop = $('.active-rep').get(0).offsetTop;\n\t\t\t$('.sidebar').scrollTop(offsetTop - ($('.sidebar').height() / 2))\n\t\t} catch (e) {\n\t\t\t// console.log(e);\n\t\t}\n\t}\n})\n"
  },
  {
    "path": "sa-token-doc/static/donate/donate-fun.js",
    "content": "\n// --------------------- 工具方法 ---------------------\n\n// 打开 loading\nloadingIcon = function(msg) {\n\tlayer.closeAll();\t// 开始前先把所有弹窗关了\n\treturn layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });\n};\n\n// 隐藏 loading\nhideLoadingIcon = function() {\n\tlayer.closeAll();\n};\n\n\n// --------------------- 渲染赞助者名单 ---------------------\n\n// 返回赞助者名单副本\nfunction getCopyDonateList() {\n\tvar arr = [];\n\tfor (var i = 0; i < donateList.length; i++) {\n\t\tvar item = donateList[i];\n\t\t// 时间转时间戳，方便排序 \n\t\titem.dateT = new Date(item.date).getTime();\n\t\t// 金额补 .0 \n\t\titem.moneyS = item.money + '';  \n\t\tif(item.moneyS.indexOf('.') == -1) {\n\t\t\titem.moneyS = item.moneyS + '.0';\n\t\t}\n\t\tarr.push(item);\n\t}\n\treturn arr;\n}\n// 返回赞助者名单副本，根据日期倒叙排列\nfunction getCopyDonateListByDateSort() {\n\tvar arr = getCopyDonateList();\n\tarr.sort(function(a, b){\n\t\tvar value = b.dateT - a.dateT;\n\t\tif(value == 0) {\n\t\t\tvalue = -1;\n\t\t}\n\t\treturn value;\n\t})\n\treturn arr;\n}\n// 返回赞助者名单副本，根据赞助金额倒叙排列\nfunction getCopyDonateListByMoneySort() {\n\tvar arr = getCopyDonateList();\n\tarr.sort(function(a, b){\n\t\tvar value = b.money - a.money;\n\t\tif(value == 0) {\n\t\t\tvalue = b.dateT - a.dateT;\n\t\t}\n\t\treturn value;\n\t})\n\treturn arr;\n}\n// console.log(getCopyDonateListByMoneySort());\n\n\n// 赞助配置 \nvar zzCfg = {\n\tcurr: 1,  // 当前页\n\tsize: 15, // 页大小 \n\tpageCount: 0, // 页总数 \n\tdataCount: 0, // 数据总数 \n\tsort: 1,  // 排序方式（1=按照日期倒叙，2=按照金额倒叙）\n}\n\n// 将赞助者名单渲染到页面上 \nfunction renderDonateTable() {\n\t// 先清空旧数据 \n\t$('.zanzhu-table tbody').empty();\n\t\n\t// 拼接 tr 字符串 \n\tvar trArrStr = '';\n\tvar arr = zzCfg.sort == 1 ? getCopyDonateListByDateSort() : getCopyDonateListByMoneySort();\n\t\n\t// 按照页参数进行遍历 \n\tlet index = (zzCfg.curr - 1) * zzCfg.size; // 起始索引\n\tlet end = index + zzCfg.size;\t// 结束索引 \n\tif(end > arr.length) {\n\t\tend = arr.length;\n\t}\n\tzzCfg.pageCount = parseInt(arr.length / zzCfg.size); // 页总数 \n\tif(arr.length % zzCfg.size != 0) {\n\t\tzzCfg.pageCount++;\n\t}\n\tzzCfg.dataCount = arr.length; // 数据总数 \n\t\n\t// 开始拼接字符串 \n\tfor (let i = index; i < end; i++) {\n\t\t// console.log(item);\n\t\tlet item = arr[i];\n\t\tlet name = item.name;\n\t\tif(item.link) {\n\t\t\tname = '<a href=\"' + item.link + '\" target=\"_blank\">' + name + '</a>'\n\t\t}\n\t\tvar trStr = `\n\t\t\t<tr>\n\t\t\t\t<td class=\"zanzhu-name\">${name}</td>\n\t\t\t\t<td class=\"zanzhu-money\">¥ ${item.moneyS}</td>\n\t\t\t\t<td>${item.msg}</td>\n\t\t\t\t<td>${item.date}</td>\n\t\t\t</tr>\n\t\t`;\n\t\ttrArrStr += trStr;\n\t}\n\t\n\t// 渲染到 table 里 \n\t$('.zanzhu-table tbody').html(trArrStr);\n\t\n\t// 重置分页信息 \n\tconst pageInfo = `第 ${zzCfg.curr}/${zzCfg.pageCount} 页（共${zzCfg.dataCount}位）`;\n\t$('.zz-pageInfo').text(pageInfo);\n}\n// 带动画的渲染 \nfunction renderDonateTable2() {\n\t// 模拟ajax的延时\n\tloadingIcon('努力加载中...');\n\tsetTimeout(function() {\n\t\thideLoadingIcon();\t// 隐藏掉转圈圈 \n\t\trenderDonateTable();\n\t}, 300);\n}\n// renderDonateTable();\n\n// 上一页\nfunction prevPageRDT(){\n\tif(zzCfg.curr <= 1) {\n\t\treturn layer.msg('达咩，不能再往前了');\n\t}\n\tzzCfg.curr--;\n\trenderDonateTable2();\n}\n// 下一页\nfunction nextPageRDT(){\n\tif(zzCfg.curr >= zzCfg.pageCount) {\n\t\treturn layer.msg('嘿，到底了');\n\t}\n\tzzCfg.curr++;\n\trenderDonateTable2();\n}\n\n// 绑定事件：切换排序\nfunction onZanzhuSortClick(){\n\t$('.zanzhu-sort-btn').click(function(){\n\t\t// 切换 class\n\t\t$('.zz-sort-native').removeClass('zz-sort-native');\n\t\t$(this).addClass('zz-sort-native');\n\t\t\n\t\t// 切换数据 \n\t\tzzCfg.curr = 1;  // 重置为第1页 \n\t\tzzCfg.sort = parseInt($(this).attr('sort-value'));\n\t\trenderDonateTable2();\n\t})\n}\nonZanzhuSortClick();\n\n// 读取 sa-token-donate 页数据为 json \nfunction readDataToJson() {\n\tvar arr = [];\n\tvar trList = $('.zanzhu-box table tbody tr');\n\tfor (let tr of trList) {\n\t\tvar tdArr = $(tr).find('td');\n\t\tvar item = {\n\t\t\tname: $(tdArr[0]).text(),\n\t\t\tlink: $(tdArr[0]).find('a').attr('href') || '',\n\t\t\tmoney: parseFloat($(tdArr[1]).text().replaceAll('¥', '')),\n\t\t\tmsg: $(tdArr[2]).html(),\n\t\t\tdate: $(tdArr[3]).text(),\n\t\t};\n\t\tarr.push(item);\n\t}\n\treturn arr;\n}\nfunction readDataToJsonStr() {\n\tvar arr = readDataToJson();\n\tvar str = '';\n\tfor (let item of arr) {\n\t\tstr = JSON.stringify(item) + ',' + str;\n\t}\n\treturn str;\n}\n\n"
  },
  {
    "path": "sa-token-doc/static/donate/donate-list.js",
    "content": "// 赞助者名单\nvar donateList = [\n\t{\n\t\t\"name\": \"省长\",\n\t\t\"link\": \"https://gitee.com/click33\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"java中最好用的权限认证框架！\",\n\t\t\"date\": \"2020-12-15\"\n\t},\n\t{\n\t\t\"name\": \"知知\",\n\t\t\"link\": \"https://gitee.com/double_zhi\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2020-12-15\"\n\t},\n\t{\n\t\t\"name\": \"zhangjiaxiaozhuo\",\n\t\t\"link\": \"https://gitee.com/zhangjiaxiaozhuo\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2020-12-15\"\n\t},\n\t{\n\t\t\"name\": \"RockMan\",\n\t\t\"link\": \"https://gitee.com/njx33\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2020-12-17\"\n\t},\n\t{\n\t\t\"name\": \"whcrow\",\n\t\t\"link\": \"https://gitee.com/whcrow\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"军师加油！\",\n\t\t\"date\": \"2021-03-16\"\n\t},\n\t{\n\t\t\"name\": \"xue1992wz\",\n\t\t\"link\": \"https://gitee.com/xue1992wz\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-03-16\"\n\t},\n\t{\n\t\t\"name\": \"萧瑟\",\n\t\t\"link\": \"https://gitee.com/fengduidui\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-03-16\"\n\t},\n\t{\n\t\t\"name\": \"二范先生\",\n\t\t\"link\": \"https://gitee.com/mr-er-fan\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"省长加油啊 喝杯茶\",\n\t\t\"date\": \"2021-03-16\"\n\t},\n\t{\n\t\t\"name\": \"Wizzer\",\n\t\t\"link\": \"https://gitee.com/wizzer\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-05-22\"\n\t},\n\t{\n\t\t\"name\": \"孔孔的空空\",\n\t\t\"link\": \"https://gitee.com/kongmr\",\n\t\t\"money\": 500,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-07-30\"\n\t},\n\t{\n\t\t\"name\": \"xiaoyan\",\n\t\t\"link\": \"https://gitee.com/l-yun\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"be better\",\n\t\t\"date\": \"2021-07-31\"\n\t},\n\t{\n\t\t\"name\": \"xiaoyan\",\n\t\t\"link\": \"https://gitee.com/l-yun\",\n\t\t\"money\": 200,\n\t\t\"msg\": \"好的作者理应被认可\",\n\t\t\"date\": \"2021-08-24\"\n\t},\n\t{\n\t\t\"name\": \"苏永晓\",\n\t\t\"link\": \"https://gitee.com/suyongxiao\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-09-01\"\n\t},\n\t{\n\t\t\"name\": \"永夜\",\n\t\t\"link\": \"https://gitee.com/cn-src\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-09-18\"\n\t},\n\t{\n\t\t\"name\": \"apifox001\",\n\t\t\"link\": \"https://gitee.com/apifox001\",\n\t\t\"money\": 200,\n\t\t\"msg\": \"<a href=\\\"https://apifox.com/\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Apifox：API 文档、API 调试、API Mock、API 自动化测试</a>\",\n\t\t\"date\": \"2021-10-15\"\n\t},\n\t{\n\t\t\"name\": \"xiaoyan\",\n\t\t\"link\": \"https://gitee.com/l-yun\",\n\t\t\"money\": 200,\n\t\t\"msg\": \"节日快乐\",\n\t\t\"date\": \"2021-10-24\"\n\t},\n\t{\n\t\t\"name\": \"ithorns\",\n\t\t\"link\": \"https://gitee.com/ithorns\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-10-25\"\n\t},\n\t{\n\t\t\"name\": \"songfazhun\",\n\t\t\"link\": \"https://gitee.com/fzsong\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-10-28\"\n\t},\n\t{\n\t\t\"name\": \"孔孔的空空\",\n\t\t\"link\": \"https://gitee.com/kongmr\",\n\t\t\"money\": 100,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-02\"\n\t},\n\t{\n\t\t\"name\": \"铂赛东\",\n\t\t\"link\": \"https://gitee.com/bryan31\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"开源加油！\",\n\t\t\"date\": \"2021-11-08\"\n\t},\n\t{\n\t\t\"name\": \"公子骏\",\n\t\t\"link\": \"https://gitee.com/dt_flys\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-08\"\n\t},\n\t{\n\t\t\"name\": \"Taller\",\n\t\t\"link\": \"https://gitee.com/evilatom\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-13\"\n\t},\n\t{\n\t\t\"name\": \"万声鹉\",\n\t\t\"link\": \"https://gitee.com/wanshengwu\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-15\"\n\t},\n\t{\n\t\t\"name\": \"yijunzhao\",\n\t\t\"link\": \"https://gitee.com/yijunzhao\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-21\"\n\t},\n\t{\n\t\t\"name\": \"xiaoyan\",\n\t\t\"link\": \"https://gitee.com/l-yun\",\n\t\t\"money\": 200,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-26\"\n\t},\n\t{\n\t\t\"name\": \"luyuan\",\n\t\t\"link\": \"https://gitee.com/meitesi\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-29\"\n\t},\n\t{\n\t\t\"name\": \"图灵谷\",\n\t\t\"link\": \"https://gitee.com/stephenson37\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-11-30\"\n\t},\n\t{\n\t\t\"name\": \"fuhouyin\",\n\t\t\"link\": \"https://gitee.com/fuhouyin\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-12-01\"\n\t},\n\t{\n\t\t\"name\": \"liu\",\n\t\t\"link\": \"https://gitee.com/liuliuliu123456\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-12-15\"\n\t},\n\t{\n\t\t\"name\": \"duyiliu\",\n\t\t\"link\": \"https://gitee.com/duyiliu\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"化繁为简，是门艺术。\",\n\t\t\"date\": \"2021-12-16\"\n\t},\n\t{\n\t\t\"name\": \"MrXionGe\",\n\t\t\"link\": \"https://gitee.com/MrXionGe\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"SA加油~~\",\n\t\t\"date\": \"2021-12-17\"\n\t},\n\t{\n\t\t\"name\": \"周周周杨\",\n\t\t\"link\": \"https://gitee.com/ChaoGeWanJiu\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-12-18\"\n\t},\n\t{\n\t\t\"name\": \"网络小渣渣\",\n\t\t\"link\": \"https://gitee.com/a9777\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2021-12-24\"\n\t},\n\t{\n\t\t\"name\": \"刚子 （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"微信打赏\",\n\t\t\"date\": \"2021-12-27\"\n\t},\n\t{\n\t\t\"name\": \"两岁 （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 188,\n\t\t\"msg\": \"微信打赏\",\n\t\t\"date\": \"2021-12-27\"\n\t},\n\t{\n\t\t\"name\": \"前世男友\",\n\t\t\"link\": \"https://gitee.com/lanbaba666\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-02-17\"\n\t},\n\t{\n\t\t\"name\": \"赵津 （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 16,\n\t\t\"msg\": \"微信打赏\",\n\t\t\"date\": \"2022-02-20\"\n\t},\n\t{\n\t\t\"name\": \"老杨\",\n\t\t\"link\": \"https://gitee.com/yangs914\",\n\t\t\"money\": 6.66,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-03-01\"\n\t},\n\t{\n\t\t\"name\": \"晓辉\",\n\t\t\"link\": \"https://gitee.com/zxhShow\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-03-07\"\n\t},\n\t{\n\t\t\"name\": \"Charles7c\",\n\t\t\"link\": \"https://gitee.com/Charles7c\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！希望 SSO 模块越来越好！\",\n\t\t\"date\": \"2022-03-17\"\n\t},\n\t{\n\t\t\"name\": \"黎子豪 （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 18.88,\n\t\t\"msg\": \"请你喝杯咖啡\",\n\t\t\"date\": \"2022-03-21\"\n\t},\n\t{\n\t\t\"name\": \"秦政 （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 6.66,\n\t\t\"msg\": \"微信打赏\",\n\t\t\"date\": \"2022-03-22\"\n\t},\n\t{\n\t\t\"name\": \"秦政 （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"微信打赏\",\n\t\t\"date\": \"2022-03-22\"\n\t},\n\t{\n\t\t\"name\": \"刘嘉威\",\n\t\t\"link\": \"https://gitee.com/liu_jiawei\",\n\t\t\"money\": 6.66,\n\t\t\"msg\": \"真滴好用~\",\n\t\t\"date\": \"2022-03-23\"\n\t},\n\t{\n\t\t\"name\": \"Robin Tin （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 28.88,\n\t\t\"msg\": \"微信打赏\",\n\t\t\"date\": \"2022-03-24\"\n\t},\n\t{\n\t\t\"name\": \"lele\",\n\t\t\"link\": \"https://gitee.com/lelez\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-03-29\"\n\t},\n\t{\n\t\t\"name\": \"alkinn\",\n\t\t\"link\": \"https://gitee.com/alkinn\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-03-29\"\n\t},\n\t{\n\t\t\"name\": \"yukihane\",\n\t\t\"link\": \"https://gitee.com/yukihane\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-04-07\"\n\t},\n\t{\n\t\t\"name\": \"xq584\",\n\t\t\"link\": \"https://gitee.com/xq584\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-04-08\"\n\t},\n\t{\n\t\t\"name\": \"行长 （微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"微信打赏\",\n\t\t\"date\": \"2022-04-15\"\n\t},\n\t{\n\t\t\"name\": \"阿文\",\n\t\t\"link\": \"https://gitee.com/qq921124136\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"很好的框架，在开发文档里学到了很多知识点\",\n\t\t\"date\": \"2022-04-21\"\n\t},\n\t{\n\t\t\"name\": \"Horatio201\",\n\t\t\"link\": \"https://gitee.com/horatio201\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"太牛了！\",\n\t\t\"date\": \"2022-04-25\"\n\t},\n\t{\n\t\t\"name\": \"乡村阿土哥\",\n\t\t\"link\": \"https://gitee.com/895995040\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-04-29\"\n\t},\n\t{\n\t\t\"name\": \"李洪星\",\n\t\t\"link\": \"https://gitee.com/li_hong_xing\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"解决了很多之前项目中遇到的问题。感谢您的开源项目！\",\n\t\t\"date\": \"2022-04-29\"\n\t},\n\t{\n\t\t\"name\": \"别处理\",\n\t\t\"link\": \"https://gitee.com/zshnb\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"非常好的项目，希望能一直做下去\",\n\t\t\"date\": \"2022-05-01\"\n\t},\n\t{\n\t\t\"name\": \"cray\",\n\t\t\"link\": \"https://gitee.com/hyy6300\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-05-10\"\n\t},\n\t{\n\t\t\"name\": \"LZ\",\n\t\t\"link\": \"https://gitee.com/FUNKBOY\",\n\t\t\"money\": 6.66,\n\t\t\"msg\": \"感谢您的开源项目！顺便踩一脚Spring Security，sa加油！\",\n\t\t\"date\": \"2022-05-18\"\n\t},\n\t{\n\t\t\"name\": \"sun_2020\",\n\t\t\"link\": \"https://gitee.com/sun-two-thousand-and-twenty\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-06-08\"\n\t},\n\t{\n\t\t\"name\": \"yuncai929\",\n\t\t\"link\": \"https://gitee.com/null_448_5562\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-06-10\"\n\t},\n\t{\n\t\t\"name\": \"刘时立\",\n\t\t\"link\": \"https://gitee.com/liu-shili\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"非常棒的开源项目!\",\n\t\t\"date\": \"2022-06-13\"\n\t},\n\t{\n\t\t\"name\": \"qiuyue\",\n\t\t\"link\": \"https://gitee.com/bmlt\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"satoken牛逼\",\n\t\t\"date\": \"2022-06-16\"\n\t},\n\t{\n\t\t\"name\": \"风如歌\",\n\t\t\"link\": \"https://gitee.com/the-wind-is-like-a-song\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"这个框架简直满足了我所有对于安全框架的需求,赞一个,加油sa-token加油中国开源!\",\n\t\t\"date\": \"2022-06-17\"\n\t},\n\t{\n\t\t\"name\": \"zhihong\",\n\t\t\"link\": \"https://gitee.com/zzh13520704819\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-06-20\"\n\t},\n\t{\n\t\t\"name\": \"jwc_gitee\",\n\t\t\"link\": \"https://gitee.com/jwc-gitee\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-07-07\"\n\t},\n\t{\n\t\t\"name\": \"小北宸呀\",\n\t\t\"link\": \"https://gitee.com/a_aas\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！我就喜欢你这种把我当白痴的官方文档\",\n\t\t\"date\": \"2022-07-08\"\n\t},\n\t{\n\t\t\"name\": \"jerrydo\",\n\t\t\"link\": \"https://gitee.com/jerrydo\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！很强大！\",\n\t\t\"date\": \"2022-08-10\"\n\t},\n\t{\n\t\t\"name\": \"邱道长\",\n\t\t\"link\": \"https://gitee.com/qiudaozhang\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"优秀的项目，赞\",\n\t\t\"date\": \"2022-09-09\"\n\t},\n\t{\n\t\t\"name\": \"BlueRose\",\n\t\t\"link\": \"https://gitee.com/Bluerose_2\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的付出，项目非常棒！\",\n\t\t\"date\": \"2022-09-22\"\n\t},\n\t{\n\t\t\"name\": \"西东\",\n\t\t\"link\": \"https://gitee.com/noear_admin\",\n\t\t\"money\": 99,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-10-05\"\n\t},\n\t{\n\t\t\"name\": \"xueshize\",\n\t\t\"link\": \"https://gitee.com/xueshize\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-10-12\"\n\t},\n\t{\n\t\t\"name\": \"feyong\",\n\t\t\"link\": \"https://gitee.com/feyong\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-10-18\"\n\t},\n\t{\n\t\t\"name\": \"王文博\",\n\t\t\"link\": \"https://gitee.com/rl520\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-10-24\"\n\t},\n\t{\n\t\t\"name\": \"就眠儀式\",\n\t\t\"link\": \"https://gitee.com/Jmysy\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-10-26\"\n\t},\n\t{\n\t\t\"name\": \"laruui\",\n\t\t\"link\": \"https://gitee.com/laruui\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-10-28\"\n\t},\n\t{\n\t\t\"name\": \"feel\",\n\t\t\"link\": \"https://gitee.com/xujiahuim\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-11-17\"\n\t},\n\t{\n\t\t\"name\": \"IlovePea\",\n\t\t\"link\": \"https://gitee.com/IlovePea\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-11-22\"\n\t},\n\t{\n\t\t\"name\": \"ThatYear\",\n\t\t\"link\": \"https://gitee.com/wangmuqing\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-11-24\"\n\t},\n\t{\n\t\t\"name\": \"时间很快\",\n\t\t\"link\": \"https://gitee.com/frsimple\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-11-29\"\n\t},\n\t{\n\t\t\"name\": \"刘涛\",\n\t\t\"link\": \"https://gitee.com/doILike\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-12-13\"\n\t},\n\t{\n\t\t\"name\": \"ken\",\n\t\t\"link\": \"https://gitee.com/affction\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-12-19\"\n\t},\n\t{\n\t\t\"name\": \"Peter Z\",\n\t\t\"link\": \"https://gitee.com/zj1995\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2022-12-26\"\n\t},\n\t{\n\t\t\"name\": \"SWmachine\",\n\t\t\"link\": \"https://gitee.com/SWmachine\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"您的开源很好用！\",\n\t\t\"date\": \"2023-01-07\"\n\t},\n\t{\n\t\t\"name\": \"tsing\",\n\t\t\"link\": \"https://gitee.com/tsing666\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-01-08\"\n\t},\n\t{\n\t\t\"name\": \"不问烟雨\",\n\t\t\"link\": \"https://gitee.com/xiaominfagui\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"牛\",\n\t\t\"date\": \"2023-01-12\"\n\t},\n\t{\n\t\t\"name\": \"熊孩子\",\n\t\t\"link\": \"https://gitee.com/xhz1230\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-02-17\"\n\t},\n\t{\n\t\t\"name\": \"陈乾\",\n\t\t\"link\": \"https://gitee.com/qianpou\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-03-05\"\n\t},\n\t{\n\t\t\"name\": \"陈乾\",\n\t\t\"link\": \"https://gitee.com/qianpou\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-03-07\"\n\t},\n\t{\n\t\t\"name\": \"李一博\",\n\t\t\"link\": \"https://gitee.com/haust_lyb\",\n\t\t\"money\": 8.88,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-03-07\"\n\t},\n\t{\n\t\t\"name\": \"空空（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-03-08\"\n\t},\n\t{\n\t\t\"name\": \"Java_小生\",\n\t\t\"link\": \"https://gitee.com/zhang_hanzhe\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢Sa-Token让我不用去B站肯几十个小时的教程，框架很优秀文档更优秀\",\n\t\t\"date\": \"2023-03-09\"\n\t},\n\t{\n\t\t\"name\": \"zhou\",\n\t\t\"link\": \"https://gitee.com/mrzhou1\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢答疑\",\n\t\t\"date\": \"2023-03-29\"\n\t},\n\t{\n\t\t\"name\": \"F（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-04-09\"\n\t},\n\t{\n\t\t\"name\": \"王宁波\",\n\t\t\"link\": \"https://gitee.com/wang-ningbo\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-04-10\"\n\t},\n\t{\n\t\t\"name\": \"Admin\",\n\t\t\"link\": \"https://gitee.com/jinan-jimeng-network_0\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-04-12\"\n\t},\n\t{\n\t\t\"name\": \"李广龙\",\n\t\t\"link\": \"https://gitee.com/ak47-b\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"跟大哥学习一辈子学不完\",\n\t\t\"date\": \"2023-04-14\"\n\t},\n\t{\n\t\t\"name\": \"hurumo\",\n\t\t\"link\": \"https://gitee.com/hurumo\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-04-17\"\n\t},\n\t{\n\t\t\"name\": \"c（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 100,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-04-17\"\n\t},\n\t{\n\t\t\"name\": \"bootx\",\n\t\t\"link\": \"https://gitee.com/bootx\",\n\t\t\"money\": 100,\n\t\t\"msg\": \"<a href=\\\"https://gitee.com/bootx/bootx-platform\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Bootx-Platform：支付收单、三方对接、后端基于 Spring Boot、Spring Cloud 应用脚手架</a>\",\n\t\t\"date\": \"2023-04-18\"\n\t},\n\t{\n\t\t\"name\": \"gdl\",\n\t\t\"link\": \"https://gitee.com/gdl97\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！作者牛逼！\",\n\t\t\"date\": \"2023-04-29\"\n\t},\n\t{\n\t\t\"name\": \"SummerHy\",\n\t\t\"link\": \"https://gitee.com/hurumo\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"国产，就是棒，：）\",\n\t\t\"date\": \"2023-05-07\"\n\t},\n\t{\n\t\t\"name\": \"BeckJin\",\n\t\t\"link\": \"https://gitee.com/beckjin666\",\n\t\t\"money\": 100,\n\t\t\"msg\": \"<a href=\\\"https://mingdao.com?s=st\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">明道云-零代码开发平台，快速响应业务需求。从“IT背锅侠”，变成“IT英雄”。</a>\",\n\t\t\"date\": \"2023-05-08\"\n\t},\n\t{\n\t\t\"name\": \"xc_Moving\",\n\t\t\"link\": \"https://gitee.com/fireZhang\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！感谢SA-token帮我度过项目的难关\",\n\t\t\"date\": \"2023-05-11\"\n\t},\n\t{\n\t\t\"name\": \"砰嚓嚓（QQ打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"一点打赏不成敬意\",\n\t\t\"date\": \"2023-05-15\"\n\t},\n\t{\n\t\t\"name\": \"dyjgitdyjgit\",\n\t\t\"link\": \"https://gitee.com/qtinfogit\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-05-22\"\n\t},\n\t{\n\t\t\"name\": \"javahuang\",\n\t\t\"link\": \"https://gitee.com/javahrp\",\n\t\t\"money\": 200,\n\t\t\"msg\": \"<a href=\\\"https://gitee.com/surveyking/surveyking\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">SurveyKing：功能最强大的调查问卷系统和考试系统，开源</a>\",\n\t\t\"date\": \"2023-06-08\"\n\t},\n\t{\n\t\t\"name\": \"SP\",\n\t\t\"link\": \"https://gitee.com/LSP1999\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"就是需要这种简单上手的项目\",\n\t\t\"date\": \"2023-06-15\"\n\t},\n\t{\n\t\t\"name\": \"Dear胜哥\",\n\t\t\"link\": \"https://gitee.com/DearShengGe\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"有幸在摸鱼时间认真看完了全文档，感觉很是不错。开源不易，望作者继续扩展该框架功能！\",\n\t\t\"date\": \"2023-06-30\"\n\t},\n\t{\n\t\t\"name\": \"吴其敏（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 200,\n\t\t\"msg\": \"<a href=\\\"https://github.com/dianping/cat\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">CAT 是基于 Java 开发的实时应用监控平台，为美团点评提供了全面的实时监控告警服务。</a>\",\n\t\t\"date\": \"2023-07-11\"\n\t},\n\t{\n\t\t\"name\": \"mikeinshanghai\",\n\t\t\"link\": \"https://gitee.com/mikeinshanghai\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"Sa-Token, MeterSphere共成长， 共辉煌！ \",\n\t\t\"date\": \"2023-07-14\"\n\t},\n\t{\n\t\t\"name\": \"张兆伟\",\n\t\t\"link\": \"https://gitee.com/zhang865700\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-07-24\"\n\t},\n\t{\n\t\t\"name\": \"XiaoYi\",\n\t\t\"link\": \"https://gitee.com/getianit\",\n\t\t\"money\": 100,\n\t\t\"msg\": \"<a href=\\\"https://www.asiayun.com/cart?action=configureproduct&amp;pid=300\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">亚洲云深圳BGP云服务器</a>\",\n\t\t\"date\": \"2023-07-24\"\n\t},\n\t{\n\t\t\"name\": \"好心肠的老哥\",\n\t\t\"link\": \"https://gitee.com/ntdm\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"非常好的开源项目，希望越来越好！\",\n\t\t\"date\": \"2023-08-02\"\n\t},\n\t{\n\t\t\"name\": \"结弦奏（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-08-07\"\n\t},\n\t{\n\t\t\"name\": \"失败女神\",\n\t\t\"link\": \"https://gitee.com/failedgoddess\",\n\t\t\"money\": 50,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-08-03\"\n\t},\n\t{\n\t\t\"name\": \"快快乐乐小码农\",\n\t\t\"link\": \"https://gitee.com/happy-little-farmer\",\n\t\t\"money\": 1,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-08-17\"\n\t},\n\t{\n\t\t\"name\": \"刘斌\",\n\t\t\"link\": \"https://gitee.com/xuanfather\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-08-17\"\n\t},\n\t{\n\t\t\"name\": \"Meteor\",\n\t\t\"link\": \"https://gitee.com/meteoroc\",\n\t\t\"money\": 2.5,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-08-23\"\n\t},\n\t{\n\t\t\"name\": \"上下求索（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 12,\n\t\t\"msg\": \"明天请你吃个早餐吧\",\n\t\t\"date\": \"2023-08-31\"\n\t},\n\t{\n\t\t\"name\": \"T_T\",\n\t\t\"link\": \"https://gitee.com/wm26hua\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-09-07\"\n\t},\n\t{\n\t\t\"name\": \"huni\",\n\t\t\"link\": \"https://gitee.com/simin_sizi\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-09-11\"\n\t},\n\t{\n\t\t\"name\": \"lostyue\",\n\t\t\"link\": \"https://gitee.com/lostyue\",\n\t\t\"money\": 20,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-09-14\"\n\t},\n\t{\n\t\t\"name\": \"shenlicao\",\n\t\t\"link\": \"https://gitee.com/shenlicao\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-09-15\"\n\t},\n\t{\n\t\t\"name\": \"明道云\",\n\t\t\"link\": \"https://gitee.com/lunan-yn\",\n\t\t\"money\": 200,\n\t\t\"msg\": \"明道云2023年伙伴大会，<a href=\\\"https://www.mingdao.com/event/mpc/2023\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">报名链接</a>\",\n\t\t\"date\": \"2023-09-25\"\n\t},\n\t{\n\t\t\"name\": \"yang\",\n\t\t\"link\": \"https://gitee.com/hansdm\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-09-27\"\n\t},\n\t{\n\t\t\"name\": \"lee\",\n\t\t\"link\": \"https://gitee.com/cngeeklee\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"真正的轻量级权限安全框架，希望继续更新\",\n\t\t\"date\": \"2023-10-06\"\n\t},\n\t{\n\t\t\"name\": \"yangs2w\",\n\t\t\"link\": \"https://gitee.com/yangs2w\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-10-10\"\n\t},\n\t{\n\t\t\"name\": \"老马（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 99,\n\t\t\"msg\": \"我使用过的开源项目，作者我都给过红包了。请收下\",\n\t\t\"date\": \"2023-10-16\"\n\t},\n\t{\n\t\t\"name\": \"ly-chn\",\n\t\t\"link\": \"https://gitee.com/ly-chn\",\n\t\t\"money\": 99,\n\t\t\"msg\": \"一定的资金支持有助于开源项目走的更加长远\",\n\t\t\"date\": \"2023-10-17\"\n\t},\n\t{\n\t\t\"name\": \"PotatoLoofah\",\n\t\t\"link\": \"https://gitee.com/PotatoLoofah\",\n\t\t\"money\": 10,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-10-27\"\n\t},\n\t{\n\t\t\"name\": \"立秋\",\n\t\t\"link\": \"https://gitee.com/code_wh\",\n\t\t\"money\": 2.5,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-10-27\"\n\t},\n\t{\n\t\t\"name\": \"时间很快\",\n\t\t\"link\": \"https://gitee.com/frsimple\",\n\t\t\"money\": 220,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-10-27\"\n\t},\n\t{\n\t\t\"name\": \"flydongdong\",\n\t\t\"link\": \"https://gitee.com/flydongdong\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-10-31\"\n\t},\n\t\n\t\n\t{\n\t\t\"name\": \"MetaLowCode\",\n\t\t\"link\": \"https://gitee.com/meta_low_code_admin\",\n\t\t\"money\": 220.0,\n\t\t\"msg\": '<a href=\"https://melecode.com/\" target=\"_blank\">可能是最适合Java程序员的低代码平台 -- 美乐低代码 https://melecode.com/</a>',\n\t\t\"date\": \"2023-11-23\"\n\t},\n\t{\n\t\t\"name\": \"rednettle\",\n\t\t\"link\": \"https://gitee.com/rednettle\",\n\t\t\"money\": 5.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-11-24\"\n\t},\n\t{\n\t\t\"name\": \"郑志强\",\n\t\t\"link\": \"https://gitee.com/zhi_qiang_zheng\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-12-01\"\n\t},\n\t{\n\t\t\"name\": \"Justin Chia\",\n\t\t\"link\": \"https://gitee.com/justin-chia\",\n\t\t\"money\": 218.0,\n\t\t\"msg\": '<a href=\"https://vform666.com/\" target=\"_blank\">可以二开的国产低代码表单 https://vform666.com/</a>',\n\t\t\"date\": \"2023-12-05\"\n\t},\n\t{\n\t\t\"name\": \"asalan570\",\n\t\t\"link\": \"https://gitee.com/asalan570\",\n\t\t\"money\": 2.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-12-12\"\n\t},\n\t{\n\t\t\"name\": \"guwq\",\n\t\t\"link\": \"https://gitee.com/guweiqiang2016\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2023-12-14\"\n\t},\n\t{\n\t\t\"name\": \"少年\",\n\t\t\"link\": \"https://gitee.com/tingfengBlog\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-01-10\"\n\t},\n\t{\n\t\t\"name\": \"mshk\",\n\t\t\"link\": \"https://gitee.com/yueguangshuiyan\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": \"Thank you for your open source repository!\",\n\t\t\"date\": \"2024-02-21\"\n\t},\n\t{\n\t\t\"name\": \"CSpy\",\n\t\t\"link\": \"https://gitee.com/cspy\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"希望在线文档网站能有个“我已点赞”的跳过按钮，互相尊重一下，谢谢。\",\n\t\t\"date\": \"2024-03-07\"\n\t},\n\t{\n\t\t\"name\": \"EtSKY\",\n\t\t\"link\": \"https://gitee.com/ecoiyun\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-08\"\n\t},\n\t{\n\t\t\"name\": \"李富康\",\n\t\t\"link\": \"https://gitee.com/li-fukang0719\",\n\t\t\"money\": 5.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-15\"\n\t},\n\t\n\t\n\t{\n\t\t\"name\": \"Jacky\",\n\t\t\"link\": \"https://gitee.com/jackywjj\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"yuluo\",\n\t\t\"link\": \"https://gitee.com/hlzha\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"ai稞\",\n\t\t\"link\": \"https://gitee.com/bbpla\",\n\t\t\"money\": 300.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"Smile丶掩饰\",\n\t\t\"link\": \"https://gitee.com/smile_gjy\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": \"感谢您的开源项目！加油！\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"小雪纷飞\",\n\t\t\"link\": \"https://gitee.com/wujiangwu\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"dengyuanke\",\n\t\t\"link\": \"https://gitee.com/dengyuanke\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"Brath\",\n\t\t\"link\": \"https://gitee.com/Guoqing-Li\",\n\t\t\"money\": 230.0,\n\t\t\"msg\": '<a href=\"https://www.brath.cn\" target=\"_blank\">感谢SaToken开源！荔知AI是一款优秀的AI网站，地址：https://www.brath.cn</a>',\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"厉军\",\n\t\t\"link\": \"https://gitee.com/shlijun\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"Blue\",\n\t\t\"link\": \"https://gitee.com/my-blue\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"Cole Xu（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": \"一直在使用satoken，感谢你的付出\",\n\t\t\"date\": \"2024-03-20\"\n\t},\n\t{\n\t\t\"name\": \"cy42\",\n\t\t\"link\": \"https://gitee.com/third-party-framework\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-03-26\"\n\t},\n\t{\n\t\t\"name\": \"YaeMivo（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": \"祝越做越好\",\n\t\t\"date\": \"2024-03-29\"\n\t},\n\t{\n\t\t\"name\": \"炮孩子\",\n\t\t\"link\": \"https://gitee.com/paohaizi\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"拳打Apach shiro，脚踢 Spring Security。\",\n\t\t\"date\": \"2024-03-30\"\n\t},\n\t{\n\t\t\"name\": \"孤独的造梦者\",\n\t\t\"link\": \"https://gitee.com/dpxz\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-04-01\"\n\t},\n\t{\n\t\t\"name\": \"HiSin\",\n\t\t\"link\": \"https://gitee.com/HisinLx\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-05-07\"\n\t},\n\t{\n\t\t\"name\": \"INS6\",\n\t\t\"link\": \"https://gitee.com/feiyuchuixue\",\n\t\t\"money\": 188.0,\n\t\t\"msg\": '<a href=\"https://szadmin.cn/\" target=\"_blank\">感谢Sa-Token开源！Sz-Admin一个轻量化RBAC开源框架。</a>',\n\t\t\"date\": \"2024-06-05\"\n\t},\n\t{\n\t\t\"name\": \"Zongyy\",\n\t\t\"link\": \"https://gitee.com/zongyY11\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": \"感谢您的开源项目！\",\n\t\t\"date\": \"2024-06-05\"\n\t},\n\t{\n\t\t\"name\": \"驰骋BPM\",\n\t\t\"link\": \"https://gitee.com/chichengsoft\",\n\t\t\"money\": 100.0,\n\t\t\"msg\": '感谢开源, 欢迎下载：驰骋低代码BPM <a href=\"https://gitee.com/opencc/JFlow\" target=\"_blank\">https://gitee.com/opencc/JFlow</a>',\n\t\t\"date\": \"2024-06-11\"\n\t},\n\t{\n\t\t\"name\": \"flydongdong\",\n\t\t\"link\": \"https://gitee.com/flydongdong\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-06-18\"\n\t},\n\t{\n\t\t\"name\": \"驰骋BPM\",\n\t\t\"link\": \"https://gitee.com/chichengsoft\",\n\t\t\"money\": 100.0,\n\t\t\"msg\": '感谢您的开源项目！欢迎了解驰骋BPM低代码. <a href=\"https://gitee.com/opencc/JFlow\" target=\"_blank\">https://gitee.com/opencc/JFlow</a>',\n\t\t\"date\": \"2024-06-20\"\n\t},\n\t{\n\t\t\"name\": \"Mall4j商城系统\",\n\t\t\"link\": \"https://gitee.com/gz-yami_admin\",\n\t\t\"money\": 218.0,\n\t\t\"msg\": '感谢开源！Mall4j商城系统： <a href=\"https://gitee.com/gz-yami/mall4j\" target=\"_blank\">https://gitee.com/gz-yami/mall4j</a>',\n\t\t\"date\": \"2024-06-21\"\n\t},\n\t{\n\t\t\"name\": \"FlyFlow\",\n\t\t\"link\": \"https://gitee.com/junyue\",\n\t\t\"money\": 200.0,\n\t\t\"msg\": '感谢开源！FlyFlow工作流： <a href=\"https://gitee.com/junyue/flyflow\" target=\"_blank\">https://gitee.com/junyue/flyflow</a>',\n\t\t\"date\": \"2024-06-25\"\n\t},\n\t{\n\t\t\"name\": \"immortal\",\n\t\t\"link\": \"https://gitee.com/immortal-wang\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目，内部项目鉴权框架参考了您的部分设计思想（用户会话和令牌会话）。',\n\t\t\"date\": \"2024-07-20\"\n\t},\n\t{\n\t\t\"name\": \"张磊\",\n\t\t\"link\": \"https://gitee.com/zl18282425038\",\n\t\t\"money\": 1.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-08-03\"\n\t},\n\t{\n\t\t\"name\": \"老黄H\",\n\t\t\"link\": \"https://gitee.com/lao-huang-h\",\n\t\t\"money\": 1.0,\n\t\t\"msg\": '感谢您的开源项目我是王攀',\n\t\t\"date\": \"2024-08-04\"\n\t},\n\t{\n\t\t\"name\": \"Chat2DB\",\n\t\t\"link\": \"https://gitee.com/jipengfei001\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": 'https://github.com/chat2db/Chat2DB/ 数据库客户端',\n\t\t\"date\": \"2024-08-05\"\n\t},\n\t\n\t{\n\t\t\"name\": \"gentleman\",\n\t\t\"link\": \"https://gitee.com/guoweiweigege\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '设计简单 功能多且强大 我杜伟坤为你代言',\n\t\t\"date\": \"2024-08-09\"\n\t},\n\t{\n\t\t\"name\": \"june\",\n\t\t\"link\": \"https://gitee.com/june_home\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": '非常方便简单易用，感谢您的开源项目！',\n\t\t\"date\": \"2024-08-14\"\n\t},\n\t{\n\t\t\"name\": \"kaka\",\n\t\t\"link\": \"https://gitee.com/blueair\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-08-30\"\n\t},\n\t{\n\t\t\"name\": \"有锦\",\n\t\t\"link\": \"https://gitee.com/mushi00\",\n\t\t\"money\": 1.0,\n\t\t\"msg\": '好厉害的项目啊 我郭威虽然没什么钱但是我郭威还是捐赠一下我郭威真的很认可这个项目，我郭威太崇拜了',\n\t\t\"date\": \"2024-09-03\"\n\t},\n\t{\n\t\t\"name\": \"zhangboyang\",\n\t\t\"link\": \"https://gitee.com/zhangboyangos\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-09-04\"\n\t},\n\t{\n\t\t\"name\": \"读钓\",\n\t\t\"link\": \"https://gitee.com/songyinyin\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": '感谢您的开源项目！致敬用爱发电',\n\t\t\"date\": \"2024-09-14\"\n\t},\n\t{\n\t\t\"name\": \"sswiki\",\n\t\t\"link\": \"https://gitee.com/sswiki\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": '感谢开源！私有化部署的企业知识库：<a href=\"https://doc.zyplayer.com\" target=\"_blank\">https://doc.zyplayer.com</a>',\n\t\t\"date\": \"2024-09-24\"\n\t},\n\t{\n\t\t\"name\": \"坚持就是胜利\",\n\t\t\"link\": \"https://gitee.com/insistppp\",\n\t\t\"money\": 1.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-09-27\"\n\t},\n\t{\n\t\t\"name\": \"StrawberryerBlue\",\n\t\t\"link\": \"https://gitee.com/strawberryerblue\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-10-14\"\n\t},\n\t{\n\t\t\"name\": \"qing\",\n\t\t\"link\": \"https://gitee.com/haomao1\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": '非常好用，感谢您的开源项目！',\n\t\t\"date\": \"2024-10-15\"\n\t},\n\t{\n\t\t\"name\": \"厉飞雨\",\n\t\t\"link\": \"https://gitee.com/david666a\",\n\t\t\"money\": 58.0,\n\t\t\"msg\": '感谢道友，深有启发。',\n\t\t\"date\": \"2024-10-16\"\n\t},\n\t{\n\t\t\"name\": \"李嘉辉\",\n\t\t\"link\": \"https://gitee.com/lee_kiahwee\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-10-17\"\n\t},\n\t{\n\t\t\"name\": \"不问烟雨\",\n\t\t\"link\": \"https://gitee.com/xiaominfagui\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": '加油',\n\t\t\"date\": \"2024-11-04\"\n\t},\n\t{\n\t\t\"name\": \"zonglinjiang\",\n\t\t\"link\": \"https://gitee.com/jiang-zonglin0427\",\n\t\t\"money\": 5.0,\n\t\t\"msg\": '已经在至少两个商业项目里面使用了 ，非常好用，感谢作者的开源精神',\n\t\t\"date\": \"2024-11-05\"\n\t},\n\t{\n\t\t\"name\": \"当下\",\n\t\t\"link\": \"https://gitee.com/carl1974\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-11-18\"\n\t},\n\t{\n\t\t\"name\": \"唐醋鱼(微信打赏)\",\n\t\t\"link\": \"\",\n\t\t\"money\": 8.8,\n\t\t\"msg\": '小小心意，群主请受纳',\n\t\t\"date\": \"2024-11-19\"\n\t},\n\t{\n\t\t\"name\": \"cunyun\",\n\t\t\"link\": \"https://gitee.com/cunyun\",\n\t\t\"money\": 1.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-11-27\"\n\t},\n\t{\n\t\t\"name\": \"guwq\",\n\t\t\"link\": \"https://gitee.com/guweiqiang2016\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2024-12-05\"\n\t},\n\t{\n\t\t\"name\": \"kingkick\",\n\t\t\"link\": \"https://gitee.com/kingkick\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '文档真好！学习到不止是 Sa-Token 框架本身，更是绝大多数场景下权限设计的最佳实践。',\n\t\t\"date\": \"2024-12-12\"\n\t},\n\t{\n\t\t\"name\": \"JavaBean\",\n\t\t\"link\": \"https://gitee.com/DearShengGe\",\n\t\t\"money\": 6.6,\n\t\t\"msg\": '跟着Sa的文档一点点理解仿佛有位老师在带领着一步步去学，尤其是SSO单点登录部分！好东西不能被埋没！',\n\t\t\"date\": \"2024-12-19\"\n\t},\n\t{\n\t\t\"name\": \"焱枫\",\n\t\t\"link\": \"https://gitee.com/dellibrunaway\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '开心快乐每一天',\n\t\t\"date\": \"2024-12-20\"\n\t},\n\t{\n\t\t\"name\": \"dmyi\",\n\t\t\"link\": \"https://gitee.com/dmyi\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-01-03\"\n\t},\n\t{\n\t\t\"name\": \"费雷\",\n\t\t\"link\": \"https://gitee.com/feileier\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-01-09\"\n\t},\n\t{\n\t\t\"name\": \"苏俊\",\n\t\t\"link\": \"https://gitee.com/fareuwell\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-01-10\"\n\t},\n\t{\n\t\t\"name\": \"阡陌兮\",\n\t\t\"link\": \"https://gitee.com/i_kang\",\n\t\t\"money\": 9.9,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-01-16\"\n\t},\n\t{\n\t\t\"name\": \"main\",\n\t\t\"link\": \"https://gitee.com/zgx1179399522\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-01-22\"\n\t},\n\t{\n\t\t\"name\": \"shalixiaohu\",\n\t\t\"link\": \"https://gitee.com/jiaruozhi\",\n\t\t\"money\": 10.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-02-06\"\n\t},\n\t{\n\t\t\"name\": \"林佳奇\",\n\t\t\"link\": \"https://gitee.com/ljq1307\",\n\t\t\"money\": 20.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-02-15\"\n\t},\n\t{\n\t\t\"name\": \"AAA方一翻（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 28.8,\n\t\t\"msg\": '请你喝杯奶茶',\n\t\t\"date\": \"2025-04-07\"\n\t},\n\t{\n\t\t\"name\": \"16群群友 错别字先生（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 50,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-05-27\"\n\t},\n\t{\n\t\t\"name\": \"李猛（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 100,\n\t\t\"msg\": '小小红包不成敬意，就当支持 satoken 的社区了哈',\n\t\t\"date\": \"2025-09-17\"\n\t},\n\t{\n\t\t\"name\": \"Owen（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 66.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2025-09-19\"\n\t},\n\t{\n\t\t\"name\": \"Linex（赞赏码）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 0.01,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2026-01-27\"\n\t},\n\t{\n\t\t\"name\": \"Json.张（赞赏码）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 1.0,\n\t\t\"msg\": '小小心意',\n\t\t\"date\": \"2026-01-27\"\n\t},\n\t{\n\t\t\"name\": \"Nafil-鱼泡直聘运营（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 18.88,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2026-01-29\"\n\t},\n\t{\n\t\t\"name\": \"偶T啊M（赞赏码）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 5.0,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2026-02-06\"\n\t},\n\t{\n\t\t\"name\": \"马潮毅（赞赏码）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 50.0,\n\t\t\"msg\": '希望 sa-token 社区越做越好',\n\t\t\"date\": \"2026-02-08\"\n\t},\n\t{\n\t\t\"name\": \"秋末-（赞赏码）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 29.9,\n\t\t\"msg\": '非常棒的项目，加油',\n\t\t\"date\": \"2026-02-09\"\n\t},\n\t{\n\t\t\"name\": \"飘飘（赞赏码）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 88.88,\n\t\t\"msg\": '做的很好，不白嫖，希望收费',\n\t\t\"date\": \"2026-02-25\"\n\t},\n\t{\n\t\t\"name\": \"Nafil-鱼泡直聘运营（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 16.88,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2026-02-26\"\n\t},\n\t{\n\t\t\"name\": \"美人鱼（微信打赏）\",\n\t\t\"link\": \"\",\n\t\t\"money\": 18.88,\n\t\t\"msg\": '感谢您的开源项目！',\n\t\t\"date\": \"2026-03-3\"\n\t},\n\t\n]"
  },
  {
    "path": "sa-token-doc/static/index.css",
    "content": "/* ================================== 内容 ====================================== */\n/* 总 */\n*{margin: 0px; padding: 0px;}\nbody{font-size: 16px; color: #34495E; font-family: \"Source Sans Pro\",\"Helvetica Neue\",\"Arial,sans-serif\";}\n.z-div{}\n\n.s-width{width: 1000px; margin: auto;}\n/* 栏目标题 */\n.s-title{color: #000; margin-top: 90px; margin-bottom: 50px;}\n/* 解释性文字 */\n.re-text{color: #4e6e8e;}\n/* 分割线 */\n.s-fenge{width: 80%; margin: auto; border-top: 1px #ddd solid;}\n\n/* ------- 头部样式 ------- */\n.doc-header{position: fixed; top: 0; z-index: 1000; width: 100%; height: 60px; line-height: 60px;}\n.doc-header{background-color: hsla(0,0%,100%,0.97); box-shadow: 0 1px 3px rgba(26,26,26,0.1);}\n\n/* 左边logo */\n.nav-left{display: inline-block; float: left;}\n.logo-box {display: inline-block; cursor: pointer; color: #000; padding-left: 24px; height: 60px; line-height: 60px;}\n.logo-box img {width: 50px; height: 50px; vertical-align: middle; position: relative; top: -1px;}\n.logo-box .logo-text {display: inline-block; margin: 0; padding: 0; padding-left: 5px; vertical-align: middle; font-size: 22px;/* font-weight: 700; */}\n\n/* 右边导航 */\n.doc-header .nav-right{margin: 0;  float: right; line-height: 60px; padding-right: 4em; white-space: nowrap; }\n.doc-header .nav-right>*{padding: 0px; margin: 0 9px;}\n.doc-header .nav-right>*:last-child{position: relative; z-index: 1002; }\n\n.nav-right a{color: #34495E; text-decoration: none; transition: all 0.2s;}\n.nav-right a:hover{color: #42B983;}\n.doc-header .nav-right .wzi{font-size: 14px; line-height: 61px; transition: color 0.2s; padding-bottom: 4px;}\n.doc-header .nav-right .wzi:hover{border-bottom: 2px #42B983 solid;}\n\n/* 小章鱼 */\n.github-corner svg{color: #fff; fill: var(--theme-color, #42b983); height: 80px; width: 80px; z-index: 1001 !important;}\n\n\n/* -------- 海报部分 --------- */\n.main-box{width: 100%; /* min-height: 70vh; */ /* height: 80vh; */ text-align: center; }\n.main-box{display: flex; align-items: center; text-align: center; }\n.fenge{min-height: 90px;}\n.content-box{color: #000; flex: 1; padding: 120px 1em 70px;}\n.content-box h1{font-size: 100px; font-weight: 400; position: relative; margin-top: 40px; /* margin-top: 15vh; */}\n.content-box h1 small{font-size: 18px; position: absolute; bottom: 10px; margin-left: 5px; font-weight: 100;}\n/* .title-logo{width: 221px; cursor: pointer; transition: all 0.2s;}\n.title-logo:hover{transform: scale(1.2, 1.2);} */\n\n.sub-title{font-size: 22px; font-weight: 400; margin-top: 30px; margin-bottom: 25px; color: #6a8bad; color: #234;}\n/* .content-box p{line-height: 30px; padding: 0px 1em;} */\n/* 角标位置修复 */\n.badge-box a:nth-child(-n+2) img{position: relative; top: 1px;}\n/* 模拟副标题的光标闪烁 */\n.gb-cursor {display: inline-block;width: 2px;height: 22px;position: relative;top: 4px;left: -4px;background-color: black;animation: blink 0.7s infinite alternate;}\n@keyframes blink { from {opacity: 0;} to {opacity: 1;} }\n\n.main-box{background-image: url(/big-file/index/home-bg3.jpg); background-size: 120% 100%;}\n.main-box{animation: changes 30s 0.2s linear infinite normal; /* background-attachment: ; */}  /* normal | alternate */\n@keyframes changes {\n\tfrom {background-position: 0vw 0%;}\n\tto {background-position: -20vw 0%;}\n}\n\n/* 几个按钮 */\n.btn-box{margin-top: 50px; margin-bottom: 40px;}\n.btn-box a{border: 1px #1e8f5c solid; border-radius: 2em; box-sizing: border-box; color: #1e8f5c; display: inline-block;transition: all 0.1s;}\n.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; }\n.btn-box a:hover{/* transform: scale(1.05, 1.05); */padding: 1em 2.3em; margin-left: 0.2em; margin-right: 0.2em;}\n/* 最后一个加深底色 */\n.btn-box .doc-btn {color: #fff; background-color: #42B983; border: 1px green solid;}\n\n/* 按钮发光动画 */\n.btn-box .doc-btn{animation: bganimation 3s infinite;}\n@keyframes bganimation{\n    0%{box-shadow: 0 0 1px #42B983;}\n    50%{box-shadow: 0 0 20px #42B983;}\n    100%{box-shadow: 0 0 20px #FFF;}\n}\n\n/* 其它平台链接 */\n.qt-pt-box{margin-top: 40px;}\n.qt-pt-box a{text-decoration: none; margin-right: 20px;}\n.qt-pt-box>a img{height: 40px;}\n.qt-pt-box a img{/* width: 130px; */ /* height: 40px; */ transition: all 0.2s !important;}\n.qt-pt-box a img:hover{transform: scale(1.1, 1.1);}\n/* .img-gitcode{width: 140px;} */\n\n/* 多媒体平台 */\n.qt-pt-box .dmt-link{ }\n.qt-pt-box .dmt-img{width: 40px; height: 40px; }\n.qt-pt-box .dmt-tips{ vertical-align: 100%; color: #888; font-size: 12px; margin-left: 5px; }\n.dmt-detail h4{padding-top: 10px; padding-bottom: 20px;}\n/* 悬浮时展开 */\n.dmt-link{display: inline-block; position: relative;}\n.dmt-detail{position: absolute; transform: translate(-300px, 0px); background-color: #FFF; overflow: hidden; display: none; transition: all 0.2s;}\n.dmt-link:hover .dmt-detail{display: block; }\n.dmt-detail{padding: 30px; padding-top: 15px; border: 1px #ccc solid;}\n.dmt-item-box{display: flex; width: 600px;}\n.dmt-detail .dmt-item{width: 200px; flex: 1; text-align: center; cursor: pointer;}\n.dmt-item .dmt-qr-img{ max-width: 180px; }\n.dmt-item .dmt-logo-img{ max-width: 180px; max-height: 50px; margin-top: 10px;}\n.dmt-item-douyin,.dmt-item-wxsph{ margin-top: 15px; }\n.dmt-item-bilibili .dmt-logo-img{margin-top: 20px;}\n.dmt-item-wxsph .dmt-logo-img{margin-top: 15px;}\n\n/* 微信二维码 */\n.wx-qr-box{margin-top: 50px;}\n.qr-item{display: inline-block;}\n.qr-item p{font-size: 12px; padding: 0 0.5em;}\n/* .qr-item a{color: #42B983;} */\n.wx-qr{width: 150px;}\n.wx-qr-box p{margin-top: 10px; color: #666; margin-bottom: 20px;}\n.wx-qr,.dro-qr{cursor: pointer;}\n\n\n/* -------- 支持特性 --------- */\n.feature-z{padding: 0em 1em; padding-top: 0px; padding-bottom: 60px; text-align: center; color: #000;}\n.feature-z .s-title{font-size: 30px; font-weight: 400; margin-top: 70px; margin-bottom: 40px;}\n.feature-z{color: rgb(128, 128, 128); text-align: center; box-sizing: border-box; line-height: 24px; font-size: 16px;}\n\n.feature-box{margin-top: 10px; margin-bottom: 70px; display: flex; flex-wrap: wrap; justify-content: space-between; /* justify-content: flex-start; */}\n.feature{border: 0px #000 solid; flex: 0 0 33%; text-align: left; padding: 1.8em 1.2em; box-sizing: border-box;}\n.feature h2{font-size: 22px; color: #000; font-weight: 400;}\n.feature p{margin-top: 14px; font-size: 16px; color: #4e6e8e;}\n\n.sa-token-jss-img{ width: 100%; }\n\n/* 功能结构图介绍 */\n/* .sa-token-js-box{margin-bottom: 50px; transition: all 0.2s;}\n.sa-token-js-box img{max-width: 100%;}\n.sa-token-js-box:hover{cursor: pointer; box-shadow: 0 0 20px #ccc;} */\n\n.re-text{padding: 0 1em;}\n.re-text a{color: #0969da; text-decoration: none;}\n.re-text a:hover{border-bottom: 1px #0969da solid;}\n\n/* -------- 集成案例 --------- */\n.s-case-box{justify-content: space-between;}\n.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;}\n.s-case{position: relative; transition: all 0.2s; background-color: #FFF;}\n.s-case-link{display: block; width: 100%; height: 0px; padding-bottom: 50%; position: relative; overflow: hidden;}\n.s-case-link img{width: 100%; height: 100%; object-fit: cover; object-position: center; position: absolute;}\n.s-case-title,.s-case-intro{padding: 0 16px;}\n.s-case-title{margin-top: 20px; font-size: 18px; font-weight: 400; color: #333; font-family: \"microsoft yahei\";}\n.s-case-intro{margin-top: 15px; font-size: 14px; line-height: 20px; color: #777; word-break:break-all;}\n.s-author{color: #ff5722; border: 1px #ff5722 solid; position: absolute; right: 20px; display: inline-block;}\n.s-author{padding: 0 5px; font-size: 12px; transform: translate(0, -25px);}\n\n\n/* 悬浮动画 */\n.s-case:hover{box-shadow: 0 0 20px #ccc;}\n.s-case:hover img{transform: scale(1.3, 1.3); }\n.s-case-link img{transition: transform 0.3s !important;}\n.s-case img:hover{cursor: pointer;}\n.s-case:hover .s-case-link:after {background-color: rgba(0, 0, 0, .35); color: #FFF;}\n.s-case .s-case-link:after {\n    content: \"详情\";\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background-color: rgba(0, 0, 0, 0);\n    transition: all .4s;\n\ttext-align: center;\n\tline-height: 10;\n\tcolor: rgba(0,0,0,0);\n\tcursor: pointer;\n}\n\n\n/* -------- 赞助者名单 --------- */\n.zanzhu-box{font-size: 14px;}\n.zanzhu-table{width: 100%; margin-top: 20px; /* border-color: #fff; */ text-align: left; color: #333;}\n.zanzhu-table th,.zanzhu-table td{padding: 5px 10px;}\n.zanzhu-table tr:nth-child(even){background: #F8F8F8;}\n.zanzhu-table .zanzhu-money{color: red;}\n.zanzhu-table .zanzhu-name a{text-decoration: none; color: #333;}\n.zanzhu-table .zanzhu-name a:hover{text-decoration: underline; color: blue;}\n/* 赞助排序盒子 */\n.zanzhu-sort-box{font-size: 14px; margin-top: -10px;}\n.zanzhu-sort-box .zanzhu-sort-btn{text-decoration: none; color: #999; cursor: pointer;}\n.zanzhu-sort-box .zanzhu-sort-btn:hover{text-decoration: underline; color: #55a;}\n.zanzhu-sort-box .zanzhu-sort-btn.zz-sort-native{text-decoration: underline; color: #55a;}\n/* 底部按钮盒子 */\n.zz-btn-box{text-align: center; margin-top: 20px; font-size: 14px;}\n.zz-btn-box button{padding: 5px 10px; cursor: pointer; border: 1px #ccc solid; color: #999;}\n.zz-btn-box button:hover{box-shadow: 0 0 10px #ddd;}\n\n\n\n/* -------- 使用公司 --------- */\n.com-box-f{padding: 1em 1em; padding-bottom: 30px; text-align: center;}\n.com-box-f h2{font-size: 30px; color: #000; font-weight: 400;}\n.com-box{display: flex; flex-wrap: wrap; width: 100%; margin-bottom: 50px; justify-content: flex-start;}\n.com-box a{display: block; flex: 0 0 13%; margin: 5px; cursor: pointer; border: 0px #ddd solid;}\n.com-box a{line-height: 75px;}\n.com-box a img{transition: transform 0.2s !important; vertical-align: middle; min-width: 60%; max-width: 100%; max-height: 100%;}\n.com-box a img:hover{transform: scale(1.05, 1.05);}\n.com-box-you a img:hover{transform: none;}\n\n/* -------- 友情链接 --------- */\n.com-box-you a{flex: 0 0 14.5%; line-height: 60px; height: 60px; margin: 10px;}\n.com-box-you a img{min-width: 60%; max-width: 85%; vertical-align: middle; max-height: 100%;}\n\n/* -------- Dromara 成员项目 --------- */\n.table-show-pj{border: 1px #d5d5d5 solid; border-width: 1px 0 0 1px ;}\n.table-show-pj a{flex: 0 0 16.5%; border: 1px #d5d5d5 solid; margin: 0; padding: 7px 0; overflow: hidden;}\n.table-show-pj a{border-width: 0 1px 1px 0px;}\n.table-show-pj a img{min-width: 60%; max-width: 70%; }\n\n/* -------- 底部 - 连接 --------- */\n#footer{background-color: #181818;}\n#footer h3{font-weight: 400; font-size: 16px; color: #ccc; margin-top: 20px; margin-bottom: 20px;}\n#footer{border-top: 1px #666 solid;}\n.footer-r-b{display: flex; padding: 40px 0;}\n.ss-box{display: inline-block; flex: 1; color: #595959; margin: 0 50px; font-size: 14px;}\n.ss-box a{color: #595959; text-decoration: none;}\n.ss-box a:hover{color: #EEE; text-decoration: underline;}\n.ss-box ul{margin: 0; padding: 0;}\n.ss-box li{list-style: none; line-height: 28px;}\n\n/* -------- 底部 - 版权 --------- */\n.foot-box{background-color: #000; color: #ddd; padding: 2em 0px; line-height: 28px; overflow: hidden; position: relative; z-index: 100;}\n.foot-box{border-top: 0px #666 solid;}\n.foot-box p{text-indent: 1em;}\n.foot-box b{font-size: 1.1em;}\n.foot-box a{color: #ddd; font-size: 0.9em;}\n.foot-box a:hover{text-decoration: underline;}\n\n\n\n/* -------- 自适应 --------- */\n/* 一般的笔记本 */\n@media screen and (max-width: 1700px) {\n\t.content-box{padding-top: 100px;}\n\t.content-box h1{font-size: 80px;}\n\t\n\t/* 支持特性部分的间距 */\n\t.s-title.s-title-tx{margin-top: 50px; margin-bottom: 20px; /* display: none; */}\n}\n\n/* 一般的手机 */\n@media screen and (max-width: 800px) {\n\t\n\t.s-title{padding: 0 16px;}\n\t.s-width{width: 100%;}\n\t\n\t.logo-box .logo-text,.copyright {display: none;}\n\t.main-box{ height: auto;}\n\t.content-box{padding-top: 100px;}\n\t.content-box h1{font-size: 50px;}\n\t\n\t.feature-z{padding: 0em; padding-bottom: 50px;}\n\t.feature{min-width: 100%;}\n\t\n\t.com-box-f{padding: 0em;}\n\t.com-box{justify-content: space-around;}\n\t.com-box a{flex: 0 0 90%;}\n\t\n\t.s-case-box{justify-content: space-around;}\n\t.s-case{flex: 0 0 90%;}\n\t\n\t.footer-r-b{display: block;}\n\t.ss-box{display: block; text-align: center; width: 90%; margin: 0px; padding-left: 1.5em;}\n\t\n\t/* .s-header{position: static;} */\n\tfooter{position: static; line-height: 40px;}\n\t\n\t/* 手机端不显示广告，和一些其它东西 */\n\t.wwads-cn,.p-none{display:none!important}\n}\n\n/* 手机端不显示广告，和一些其它东西 */\n/* @media (max-width: 576px) {.wwads-cn,.p-none{display:none!important}} */\n\n\n\n/* 工具栏超链接 展开、收缩div */\n.zk-box{display: inline-block; padding-right: 0px; margin-right: -25px;}\n/* 外层盒 */\n.zk-box .zk-context{max-height: 0px; position: absolute; overflow: hidden;}\n.zk-box:hover .zk-context{max-height: 400px;}\n/* 内层盒 */\n.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;}\n.zk-box:hover .zk-context>div{opacity: 1;}\n/* 小链接 */\n.zk-box .zk-context a{font-size: 14px; display: block; line-height: 32px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}\n.zk-box .zk-context a{text-align: left; padding: 0 1.5em 0 1em;}\n.zk-box .zk-context .zk-fengexian{border-bottom: 1px #d9d9d9 solid; margin: 10px 0;}\n\n/* 下三角小图标 */\n.zk-icon{display: inline-block; width: 0px; height: 0px; position: relative;top: 3px; margin-left: 4px;}\n.zk-icon{border-style: solid; border-width: 5px; border-color: #aaa transparent transparent transparent; }\n\n\n/* -------------  背景色相关 ------------- */\n/* 侧边栏需要透明 */\n.sidebar-toggle{background-color: transparent !important;}\n.sidebar{background-color: transparent !important;}\n\n/* 变色的动画 */\n.doc-header,body{transition: all 0.5s !important;}\n\n/* 调色按钮 */\n.theme-btn{width: 25px; height: 25px; line-height: 60px; vertical-align: middle; position: relative; top: -1px;}\n.theme-box{width: 160px; text-align: left; line-height: 20px; margin-top: -20px; white-space: normal;}\n.theme-box span{\n\tdisplay: inline-block;\n\twidth: 20px; \n\theight: 20px; \n\tmargin: 1px 2px; \n\tborder: 1px #ccc solid; \n\tcursor: pointer;\n\tborder-radius: 1px;\n\tbox-sizing: border-box;\n}\n\n\n\n/* ajax加载时的转圈圈样式 */\n.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }\n"
  },
  {
    "path": "sa-token-doc/static/is-fill-in-wj-plugin.js",
    "content": "// \n\n// 声明 docsify 插件\nvar isFillInWjPlugin = function(hook, vm) {\n\t\n\t// 钩子函数：解析之前执行\n\thook.beforeEach(function(content) {\n\t\treturn content;\n\t});\n\t\n\t// 钩子函数：每次路由切换时，解析内容之后执行 \n\thook.afterEach(function(html) {\n\t\treturn html;\n\t});\n\t\n\t// 钩子函数：每次路由切换时数据全部加载完成后调用，没有参数。\n\thook.doneEach(function() {\n\t\tisFillIn(vm);\n\t});\n\t\n\t// 钩子函数：初始化并第一次加载完成数据后调用，没有参数。\n\thook.ready(function() {\n\t\t\n\t});\n\t\n}\n\n\n// 检查成功后，多少天不再检查 \nconst wjAllowDisparity = 1000 * 60 * 60 * 24 * 30 * 3;\n// const allowDisparity = 1000 * 10;\n\n\n// 判断当前是否已填写 \nfunction isFillIn(vm) {\n\t// 非PC端不检查\n\tif(document.body.offsetWidth < 800) {\n\t\tconsole.log('small screen ... wj ');\n\t\treturn;\n\t}\n\t\n\t// 白名单路由不判断\n\tconst whiteList = ['/', '/more/link', '/more/demand-commit', '/more/join-group', '/more/sa-token-donate', '/more/wenjuan', \n\t\t\t'/sso/sso-pro', '/more/update-log', '/more/common-questions', '/fun/sa-token-test', '/fun/issue-template'];\n\tif(whiteList.indexOf(vm.route.path) >= 0) {\n\t\tconsole.log('white route ... wj');\n\t\treturn;\n\t}\n\t\n\t// 判断是否近期已经判断过了\n\ttry{\n\t\tconst isFillIn = localStorage.isFillIn;\n\t\tif(isFillIn) {\n\t\t\t// 记录 star 的时间，和当前时间的差距\n\t\t\tconst disparity = new Date().getTime() - parseInt(isFillIn);\n\t\t\t\n\t\t\t// 差距小于一月，不再检测，大于一月，再检测一下\n\t\t\tif(disparity < wjAllowDisparity) {\n\t\t\t\tconsole.log('checked ... wj ');\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}catch(e){\n\t\tconsole.error(e);\n\t}\n\t\n\t// 本次打开页面的内存内已经弹出了的话，也不再弹了 \n\tif(window.isYtcXsjfkasjda) {\n\t\treturn;\n\t}\n\twindow.isYtcXsjfkasjda = true;\n\t\n\t// 弹出弹框，邀请填写 \n\tconst tipStr = `\n\t\t<div>\n\t\t\t<h3>\n\t\t\t\t嗨，同学你好！  \n\t\t\t</h3>\n\t\t\t<p>\n\t\t\t\t我们想以运营一款产品的心态来运营一个开源框架，所以我们迫切希望您能够填写这份问卷，这有 6 道选择题，\n\t\t\t\t应该只会略微占用您 1~3 分钟的时间。  \n\t\t\t</p>\n\t\t\t<p>问卷地址：<a href=\"https://wj.qq.com/s2/14587150/b5b4/\" target=\"_blank\">https://wj.qq.com/s2/14587150/b5b4/</a></p>\n\t\t\t<p>Sa-Token 将会非常重视每一位粉丝的宝贵意见！😇😇😇</p>\n\t\t</div>\n\t\t`;\n\t\n\tconst index = layer.confirm(tipStr, {\n\t\t\ttitle: '问卷调查填写邀请', \n\t\t\tbtn: ['我已填写 (1月内不再弹出)', '暂时不要 (1天内不再弹出)'], \n\t\t\t// btn: ['同意授权检测', '暂时不要，我先看看文档'], \n\t\t\tarea: '480px', \n\t\t\toffset: '30%'\n\t\t}, \n\t\t// 点击确定\n\t\tfunction(index) {\n\t\t\tlayer.close(index);\n\t\t\tlocalStorage.isFillIn = new Date().getTime();\n\t\t\t\n\t\t\tlayer.msg('感谢你的支持，Sa-Token 将努力变得更加完善！  ❤️ ❤️ ❤️ ')\n\t\t},\n\t\t// 点击取消\n\t\tfunction(){\n\t\t\t// 一天内不再检查\n\t\t\tconst ygTime = allowDisparity - (1000 * 60 * 60 * 24);\n\t\t\tlocalStorage.isFillIn = new Date().getTime() - ygTime;\n\t\t\t\n\t\t\tlayer.alert('你可以随时在右上角 [ 相关资源 -> 问卷调查 ] 处找到问卷链接', function(index) {\n\t\t\t\tlayer.close(index);\n\t\t\t})\n\t\t}\n\t);\n}\n\n"
  },
  {
    "path": "sa-token-doc/static/is-star-plugin.js",
    "content": "// \n\n// 声明 docsify 插件\nvar isStarPlugin = function(hook, vm) {\n\t\n\t// 钩子函数：解析之前执行\n\thook.beforeEach(function(content) {\n\t\treturn content;\n\t});\n\t\n\t// 钩子函数：每次路由切换时，解析内容之后执行 \n\thook.afterEach(function(html) {\n\t\treturn html;\n\t});\n\t\n\t// 钩子函数：每次路由切换时数据全部加载完成后调用，没有参数。\n\thook.doneEach(function() {\n\t\t//isStarRepo(vm);\n\t});\n\t\n\t// 钩子函数：初始化并第一次加载完成数据后调用，没有参数。\n\thook.ready(function() {\n\t\t\n\t});\n\t\n}\n\n// 应用参数 \nconst client_id = '0cc618beb08db99bff50e500e38c2144d95ada9abb51c00c44592726ecd583f4';\nconst client_secret = 'xxx';\nconst redirect_uri = 'https://sa-token.cc/doc.html';\nconst docDomain = 'sa-token.cc';\n// const redirect_uri = 'http://127.0.0.1:8848/sa-token-doc/doc.html';\n// const docDomain = '127.0.0.1:8848';\n\t\t\n// 检查成功后，多少天不再检查 \nconst allowDisparity = 1000 * 60 * 60 * 24 * 30 * 3;\n// const allowDisparity = 1000 * 10;\n\n\n// 判断当前是否已 star\nfunction isStarRepo(vm) {\n\t// 非PC端不检查\n\tif(document.body.offsetWidth < 800) {\n\t\tconsole.log('small screen ...');\n\t\treturn;\n\t}\n\t\n\t// 判断是否在主域名下\n\tif(location.host !== docDomain) {\n\t\tconsole.log('not domain, no check...');\n\t\treturn;\n\t}\n\t\n\t// 判断是否近期已经判断过了\n\ttry{\n\t\tconst isStarRepo = localStorage.isStarRepo;\n\t\tif(isStarRepo) {\n\t\t\t// 记录 star 的时间，和当前时间的差距\n\t\t\tconst disparity = new Date().getTime() - parseInt(isStarRepo);\n\t\t\t\n\t\t\t// 差距小于一月，不再检测，大于一月，再检测一下\n\t\t\tif(disparity < allowDisparity) {\n\t\t\t\tconsole.log('checked ...');\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}catch(e){\n\t\tconsole.error(e);\n\t}\n\t\n\t// 白名单路由不判断\n\tconst whiteList = ['/a', '/more/link', '/more/demand-commit', '/more/join-group', '/more/sa-token-donate', \n\t\t\t'/sso/sso-pro', '/more/update-log', '/more/common-questions', '/fun/sa-token-test', '/fun/issue-template'];\n\tif(whiteList.indexOf(vm.route.path) >= 0 && getParam('code') === null) {\n\t\tconsole.log('white route ...');\n\t\treturn;\n\t}\n\t\n\t// 开始获取 code \n\t$('body').css({'overflow': 'hidden'});\n\tgetCode();\n}\n\t\t\n// 去请求授权\nfunction getCode() {\n\t\n\t// 检查url中是否有code\n\tconst code = getParam('code');\n\tif(code) {\n\t\t// 有 code，进一步去请求 access_token\n\t\tgetAccessToken(code);\n\t} else {\n\t\t// 不存在code，弹窗提示询问\n\t\tconfirmStar();\n\t}\n}\n\n// 弹窗提示点 star \nfunction confirmStar() {\n\t\n\t// 弹窗提示文字 \n\tconst tipStr = `\n\t\t<div>\n\t\t\t<p><b>嗨，同学，来支持一下 Sa-Token 吧，为项目点个 star ！</b></p>\n\t\t\t<div>仅需两步即可完成：<br>\n\t\t\t\t<div>1、打开 Sa-Token <a href=\"https://gitee.com/dromara/sa-token\" target=\"_blank\">开源仓库主页</a>，在右上角点个 star 。</div>\n\t\t\t\t<div>2、点击下方 [ 同意授权检测 ] 按钮，同意 Sa-Token 获取 API 权限进行检测。<a href=\"javascript:authDetails();\" style=\"text-decoration: none;\">？</a></div>\n\t\t\t</div>\n\t\t\t<p><b>本章节文档将在 star 后正常开放展示。</b></p>\n\t\t\t<p style=\"color: green;\">开源不易，希望您不吝支持，激励开源项目走的更加长远 😇😇😇</p>\n\t\t</div>\n\t\t`;\n\t\n\tconst index = layer.confirm(tipStr, {\n\t\t\ttitle: '提示', \n\t\t\tbtn: ['同意授权检测'], \n\t\t\t// btn: ['同意授权检测', '暂时不要，我先看看文档'], \n\t\t\tarea: '460px', \n\t\t\toffset: '25%',\n\t\t\tcloseBtn: false\n\t\t}, \n\t\tfunction(index) {\n\t\t\t// \n\t\t\tlayer.close(index);\n\t\t\t// 用户点了确认，去 gitee 官方请求授权获取\n\t\t\tgoAuth();\n\t\t}\n\t);\n\t\n\t// 源码注释提示 \n\tconst closeLayer = \n\t`\t\n\t\t<!-- \n\t\t\t↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓\n\t\t\t 在 f12 控制台 执行一下：\n\t\t\t\t localStorage.isStarRepo = new Date().getTime()\n\t\t\t 即可取消弹窗 ，执行完刷新一下页面\n\t\t\t↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑\n\t\t-->\n\t`;\n\t$('#layui-layer' + index).prepend(closeLayer)\n}\n\n\n// 跳转到 gitee 授权界面\nfunction goAuth() {\n\tconst authUrl = \"https://gitee.com/oauth/authorize\" +\n\t\t\t\t\t\"?client_id=\" + client_id + \n\t\t\t\t\t\"&redirect_uri=\" + redirect_uri + \n\t\t\t\t\t\"&response_type=code\";\n\tlocation.href = authUrl;\n}\n\n\n// 获取 access_token \nfunction getAccessToken(code) {\n\t// 根据 code 获取 access_token\n\t$.ajax({\n\t\turl: 'https://sa-token.cc/server/oauth/token',\n\t\tmethod: 'post',\n\t\tdata: {\n\t\t\tgrant_type: 'authorization_code',\n\t\t\tcode: code,\n\t\t\tclient_id: client_id,\n\t\t\tredirect_uri: redirect_uri,\n\t\t\tclient_secret: client_secret,\n\t\t},\n\t\tsuccess: function(res) {\n\t\t\t// 如果返回的不是 200\n\t\t\tif(res.code !== 200) {\n\t\t\t\treturn layer.alert(res.msg, {closeBtn: false}, function(){\n\t\t\t\t\t// 刷新url，去掉 code 参数 \n\t\t\t\t\tlocation.href = 'doc.html';\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t// 拿到 access_token \n\t\t\tconst access_token = res.access_token;\n\t\t\t\n\t\t\t// 根据 access_token 判断是否 star 了仓库\n\t\t\t$.ajax({\n\t\t\t\turl: 'https://gitee.com/api/v5/user/starred/dromara/sa-token',\n\t\t\t\tmethod: 'get',\n\t\t\t\tdata: {\n\t\t\t\t\taccess_token: access_token\n\t\t\t\t},\n\t\t\t\tsuccess: function(res) {\n\t\t\t\t\t// success 回调即代表已经 star，gitee API 请求体不返回任何数据\n\t\t\t\t\tconsole.log('-> stared ...');\n\t\t\t\t\t// 记录本次检查时间 \n\t\t\t\t\tlocalStorage.isStarRepo = new Date().getTime();\n\t\t\t\t\t// \n\t\t\t\t\tlayer.alert('感谢你的支持  ❤️ ❤️ ❤️ ，Sa-Token 将努力变得更加完善！', function(index) {\n\t\t\t\t\t\tlayer.close(index);\n\t\t\t\t\t\t// 刷新url，去掉 code 参数 \n\t\t\t\t\t\tlocation.href = location.href.replace(\"?code=\" + code, '');\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\terror: function(e) {\n\t\t\t\t\t// console.log('ff请求错误 ', e);\n\t\t\t\t\t// 如下返回，代表没有 star \n\t\t\t\t\tif(e.statusText = 'Not Found'){\n\t\t\t\t\t\tconsole.log('not star ...');\n\t\t\t\t\t\tlayer.alert('未检测到 star 数据...', {closeBtn: false}, function() {\n\t\t\t\t\t\t\t// 刷新url，去掉 code 参数 \n\t\t\t\t\t\t\tlocation.href = location.href.replace(\"?code=\" + code, '');\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t},\n\t\terror: function(e) {\n\t\t\tconsole.log('请求错误 ', e);\n\t\t\t// 如果请求地址有错，可能是服务器宕机了，暂停一天检测\n\t\t\tif(e.status === 0 || e.status === 502) {\n\t\t\t\treturn layer.alert(JSON.stringify(e), {closeBtn: false}, function(){\n\t\t\t\t\t// 一天内不再检查 \n\t\t\t\t\tconst ygTime = allowDisparity - (1000 * 60 * 60 * 24);\n\t\t\t\t\tlocalStorage.isStarRepo = new Date().getTime() - ygTime;\n\t\t\t\t\t// 刷新 url，去掉 code 参数 \n\t\t\t\t\tlocation.href = location.href.replace(\"?code=\" + code, '');\n\t\t\t\t});\n\t\t\t}\n\t\t\t\n\t\t\t// 无效授权，可能是 code 无效 \n\t\t\tconst errorMsg = (e.responseJSON && e.responseJSON.error) || JSON.stringify(e);\n\t\t\tif(errorMsg == 'invalid_grant') {\n\t\t\t\tconsole.log('无效code', code);\n\t\t\t}\n\t\t\tlayer.alert('check error... ' + errorMsg, function(index) {\n\t\t\t\tlayer.close(index);\n\t\t\t\t// 刷新url，去掉 code 参数 \n\t\t\t\tlet url = location.href.replace(\"?code=\" + code, '');\n\t\t\t\turl = url.replace(\"&code=\" + code, '');\n\t\t\t\tlocation.href = url;\n\t\t\t});\n\t\t}\n\t})\n}\n\n// 疑问\nfunction authDetails() {\n\tconst str = \"用于检测的凭证信息将仅保存你的浏览器本地，Sa-Token 文档已完整开源，源码可查\";\n\talert(str);\n}\n\n// 获取 url 携带的参数 \nfunction getParam(name, defaultValue){\n\tvar query = window.location.search.substring(1);\n\tvar vars = query.split(\"&\");\n\tfor (var i=0;i<vars.length;i++) {\n\t\tvar pair = vars[i].split(\"=\");\n\t\tif(pair[0] == name){return pair[1];}\n\t}\n\treturn(defaultValue == undefined ? null : defaultValue);\n}\n\t\n"
  },
  {
    "path": "sa-token-doc/static/jquery.lazyload-1.9.3.js",
    "content": "/*\n * Lazy Load - jQuery plugin for lazy loading images\n *\n * Copyright (c) 2007-2013 Mika Tuupola\n *\n * Licensed under the MIT license:\n *   http://www.opensource.org/licenses/mit-license.php\n *\n * Project home:\n *   http://www.appelsiini.net/projects/lazyload\n *\n * Version:  1.9.3\n *\n */\n\n(function($, window, document, undefined) {\n    var $window = $(window);\n\n    $.fn.lazyload = function(options) {\n        var elements = this;\n        var $container;\n        var settings = {\n            threshold       : 0,\n            failure_limit   : 0,\n            event           : \"scroll\",\n            effect          : \"show\",\n            container       : window,\n            data_attribute  : \"original\",\n            skip_invisible  : true,\n            appear          : null,\n            load            : null,\n            placeholder     : \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC\"\n        };\n\n        function update() {\n            var counter = 0;\n\n            elements.each(function() {\n                var $this = $(this);\n                if (settings.skip_invisible && !$this.is(\":visible\")) {\n                    return;\n                }\n                if ($.abovethetop(this, settings) ||\n                    $.leftofbegin(this, settings)) {\n                        /* Nothing. */\n                } else if (!$.belowthefold(this, settings) &&\n                    !$.rightoffold(this, settings)) {\n                        $this.trigger(\"appear\");\n                        /* if we found an image we'll load, reset the counter */\n                        counter = 0;\n                } else {\n                    if (++counter > settings.failure_limit) {\n                        return false;\n                    }\n                }\n            });\n\n        }\n\n        if(options) {\n            /* Maintain BC for a couple of versions. */\n            if (undefined !== options.failurelimit) {\n                options.failure_limit = options.failurelimit;\n                delete options.failurelimit;\n            }\n            if (undefined !== options.effectspeed) {\n                options.effect_speed = options.effectspeed;\n                delete options.effectspeed;\n            }\n\n            $.extend(settings, options);\n        }\n\n        /* Cache container as jQuery as object. */\n        $container = (settings.container === undefined ||\n                      settings.container === window) ? $window : $(settings.container);\n\n        /* Fire one scroll event per scroll. Not one scroll event per image. */\n        if (0 === settings.event.indexOf(\"scroll\")) {\n            $container.bind(settings.event, function() {\n                return update();\n            });\n        }\n\n        this.each(function() {\n            var self = this;\n            var $self = $(self);\n\n            self.loaded = false;\n\n            /* If no src attribute given use data:uri. */\n            if ($self.attr(\"src\") === undefined || $self.attr(\"src\") === false) {\n                if ($self.is(\"img\")) {\n                    $self.attr(\"src\", settings.placeholder);\n                }\n            }\n\n            /* When appear is triggered load original image. */\n            $self.one(\"appear\", function() {\n                if (!this.loaded) {\n                    if (settings.appear) {\n                        var elements_left = elements.length;\n                        settings.appear.call(self, elements_left, settings);\n                    }\n                    $(\"<img />\")\n                        .bind(\"load\", function() {\n\n                            var original = $self.attr(\"data-\" + settings.data_attribute);\n                            $self.hide();\n                            if ($self.is(\"img\")) {\n                                $self.attr(\"src\", original);\n                            } else {\n                                $self.css(\"background-image\", \"url('\" + original + \"')\");\n                            }\n                            $self[settings.effect](settings.effect_speed);\n\n                            self.loaded = true;\n\n                            /* Remove image from array so it is not looped next time. */\n                            var temp = $.grep(elements, function(element) {\n                                return !element.loaded;\n                            });\n                            elements = $(temp);\n\n                            if (settings.load) {\n                                var elements_left = elements.length;\n                                settings.load.call(self, elements_left, settings);\n                            }\n                        })\n                        .attr(\"src\", $self.attr(\"data-\" + settings.data_attribute));\n                }\n            });\n\n            /* When wanted event is triggered load original image */\n            /* by triggering appear.                              */\n            if (0 !== settings.event.indexOf(\"scroll\")) {\n                $self.bind(settings.event, function() {\n                    if (!self.loaded) {\n                        $self.trigger(\"appear\");\n                    }\n                });\n            }\n        });\n\n        /* Check if something appears when window is resized. */\n        $window.bind(\"resize\", function() {\n            update();\n        });\n\n        /* With IOS5 force loading images when navigating with back button. */\n        /* Non optimal workaround. */\n        if ((/(?:iphone|ipod|ipad).*os 5/gi).test(navigator.appVersion)) {\n            $window.bind(\"pageshow\", function(event) {\n                if (event.originalEvent && event.originalEvent.persisted) {\n                    elements.each(function() {\n                        $(this).trigger(\"appear\");\n                    });\n                }\n            });\n        }\n\n        /* Force initial check if images should appear. */\n        $(document).ready(function() {\n            update();\n        });\n\n        return this;\n    };\n\n    /* Convenience methods in jQuery namespace.           */\n    /* Use as  $.belowthefold(element, {threshold : 100, container : window}) */\n\n    $.belowthefold = function(element, settings) {\n        var fold;\n\n        if (settings.container === undefined || settings.container === window) {\n            fold = (window.innerHeight ? window.innerHeight : $window.height()) + $window.scrollTop();\n        } else {\n            fold = $(settings.container).offset().top + $(settings.container).height();\n        }\n\n        return fold <= $(element).offset().top - settings.threshold;\n    };\n\n    $.rightoffold = function(element, settings) {\n        var fold;\n\n        if (settings.container === undefined || settings.container === window) {\n            fold = $window.width() + $window.scrollLeft();\n        } else {\n            fold = $(settings.container).offset().left + $(settings.container).width();\n        }\n\n        return fold <= $(element).offset().left - settings.threshold;\n    };\n\n    $.abovethetop = function(element, settings) {\n        var fold;\n\n        if (settings.container === undefined || settings.container === window) {\n            fold = $window.scrollTop();\n        } else {\n            fold = $(settings.container).offset().top;\n        }\n\n        return fold >= $(element).offset().top + settings.threshold  + $(element).height();\n    };\n\n    $.leftofbegin = function(element, settings) {\n        var fold;\n\n        if (settings.container === undefined || settings.container === window) {\n            fold = $window.scrollLeft();\n        } else {\n            fold = $(settings.container).offset().left;\n        }\n\n        return fold >= $(element).offset().left + settings.threshold + $(element).width();\n    };\n\n    $.inviewport = function(element, settings) {\n         return !$.rightoffold(element, settings) && !$.leftofbegin(element, settings) &&\n                !$.belowthefold(element, settings) && !$.abovethetop(element, settings);\n     };\n\n    /* Custom selectors for your convenience.   */\n    /* Use as $(\"img:below-the-fold\").something() or */\n    /* $(\"img\").filter(\":below-the-fold\").something() which is faster */\n\n    $.extend($.expr[\":\"], {\n        \"below-the-fold\" : function(a) { return $.belowthefold(a, {threshold : 0}); },\n        \"above-the-top\"  : function(a) { return !$.belowthefold(a, {threshold : 0}); },\n        \"right-of-screen\": function(a) { return $.rightoffold(a, {threshold : 0}); },\n        \"left-of-screen\" : function(a) { return !$.rightoffold(a, {threshold : 0}); },\n        \"in-viewport\"    : function(a) { return $.inviewport(a, {threshold : 0}); },\n        /* Maintain BC for couple of versions. */\n        \"above-the-fold\" : function(a) { return !$.belowthefold(a, {threshold : 0}); },\n        \"right-of-fold\"  : function(a) { return $.rightoffold(a, {threshold : 0}); },\n        \"left-of-fold\"   : function(a) { return !$.rightoffold(a, {threshold : 0}); }\n    });\n\n})(jQuery, window, document);"
  },
  {
    "path": "sa-token-doc/static/layer-v3.1.1/layer.js",
    "content": "/*! layer-v3.1.1 Web弹层组件 MIT License  http://layer.layui.com/  By 贤心 */\n ;!function(e,t){\"use strict\";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;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:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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:\"&#x4FE1;&#x606F;\",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?'<div class=\"layui-layer-title\" style=\"'+(f?r.title[1]:\"\")+'\">'+(f?r.title[0]:r.title)+\"</div>\":\"\";return r.zIndex=s,t([r.shade?'<div class=\"layui-layer-shade\" id=\"layui-layer-shade'+a+'\" times=\"'+a+'\" style=\"'+(\"z-index:\"+(s-1)+\"; \")+'\"></div>':\"\",'<div class=\"'+l[0]+(\" layui-layer-\"+o.type[r.type])+(0!=r.type&&2!=r.type||r.shade?\"\":\" layui-layer-border\")+\" \"+(r.skin||\"\")+'\" id=\"'+l[0]+a+'\" type=\"'+o.type[r.type]+'\" times=\"'+a+'\" showtime=\"'+r.time+'\" conType=\"'+(e?\"object\":\"string\")+'\" style=\"z-index: '+s+\"; width:\"+r.area[0]+\";height:\"+r.area[1]+(r.fixed?\"\":\";position:absolute;\")+'\">'+(e&&2!=r.type?\"\":u)+'<div id=\"'+(r.id||\"\")+'\" class=\"layui-layer-content'+(0==r.type&&r.icon!==-1?\" layui-layer-padding\":\"\")+(3==r.type?\" layui-layer-loading\"+r.icon:\"\")+'\">'+(0==r.type&&r.icon!==-1?'<i class=\"layui-layer-ico layui-layer-ico'+r.icon+'\"></i>':\"\")+(1==r.type&&e?\"\":r.content||\"\")+'</div><span class=\"layui-layer-setwin\">'+function(){var e=c?'<a class=\"layui-layer-min\" href=\"javascript:;\"><cite></cite></a><a class=\"layui-layer-ico layui-layer-max\" href=\"javascript:;\"></a>':\"\";return r.closeBtn&&(e+='<a class=\"layui-layer-ico '+l[7]+\" \"+l[7]+(r.title?r.closeBtn:4==r.type?\"1\":\"2\")+'\" href=\"javascript:;\"></a>'),e}()+\"</span>\"+(r.btn?function(){var e=\"\";\"string\"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t<i;t++)e+='<a class=\"'+l[6]+t+'\">'+r.btn[t]+\"</a>\";return'<div class=\"'+l[6]+\" layui-layer-btn-\"+(r.btnAlign||\"\")+'\">'+e+\"</div>\"}():\"\")+(r.resize?'<span class=\"layui-layer-resize\"></span>':\"\")+\"</div>\"],u,i('<div class=\"layui-layer-move\"></div>')),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='<iframe scrolling=\"'+(t.content[1]||\"auto\")+'\" allowtransparency=\"true\" id=\"'+l[4]+a+'\" name=\"'+l[4]+a+'\" onload=\"this.className=\\'\\';\" class=\"layui-layer-load\" frameborder=\"0\" src=\"'+t.content[0]+'\"></iframe>';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]+'<i class=\"layui-layer-TipsG\"></i>',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;a<c.stX&&(a=c.stX),a>f&&(a=f),o<c.stY&&(o=c.stY),o>u&&(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?'<textarea class=\"layui-layer-input\"'+a+\">\"+(e.value||\"\")+\"</textarea>\":function(){return'<input type=\"'+(1==e.formType?\"password\":\"text\")+'\" class=\"layui-layer-input\" value=\"'+(e.value||\"\")+'\">'}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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(\"&#x6700;&#x591A;&#x8F93;&#x5165;\"+(e.maxlength||500)+\"&#x4E2A;&#x5B57;&#x6570;\",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='<span class=\"'+n+'\">'+t[0].title+\"</span>\";i<e;i++)a+=\"<span>\"+t[i].title+\"</span>\";return a}(),content:'<ul class=\"layui-layer-tabmain\">'+function(){var e=t.length,i=1,a=\"\";if(e>0)for(a='<li class=\"layui-layer-tabli '+n+'\">'+(t[0].content||\"no content\")+\"</li>\";i<e;i++)a+='<li class=\"layui-layer-tabli\">'+(t[i].content||\"no  content\")+\"</li>\";return a}()+\"</ul>\",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(\"&#x6CA1;&#x6709;&#x56FE;&#x7247;\")}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]<r[1]&&(a[0]=a[0]/r[1],a[1]=a[1]/r[1])}return[a[0]+\"px\",a[1]+\"px\"]}(),title:!1,shade:.9,shadeClose:!0,closeBtn:!1,move:\".layui-layer-phimg img\",moveType:1,scrollbar:!1,moveOut:!0,isOutAnim:!1,skin:\"layui-layer-photos\"+c(\"photos\"),content:'<div class=\"layui-layer-phimg\"><img src=\"'+u[d].src+'\" alt=\"'+(u[d].alt||\"\")+'\" layer-pid=\"'+u[d].pid+'\"><div class=\"layui-layer-imgsee\">'+(u.length>1?'<span class=\"layui-layer-imguide\"><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgprev\"></a><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgnext\"></a></span>':\"\")+'<div class=\"layui-layer-imgbar\" style=\"display:'+(a?\"block\":\"\")+'\"><span class=\"layui-layer-imgtit\"><a href=\"javascript:;\">'+(u[d].alt||\"\")+\"</a><em>\"+s.imgIndex+\"/\"+u.length+\"</em></span></div></div></div>\",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(\"&#x5F53;&#x524D;&#x56FE;&#x7247;&#x5730;&#x5740;&#x5F02;&#x5E38;<br>&#x662F;&#x5426;&#x7EE7;&#x7EED;&#x67E5;&#x770B;&#x4E0B;&#x4E00;&#x5F20;&#xFF1F;\",{time:3e4,btn:[\"&#x4E0B;&#x4E00;&#x5F20;\",\"&#x4E0D;&#x770B;&#x4E86;\"],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);"
  },
  {
    "path": "sa-token-doc/static/layer-v3.1.1/mobile/layer.js",
    "content": "/*! layer mobile-v2.0.0 Web弹层组件 MIT License  http://layer.layui.com/mobile  By 贤心 */\n ;!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?'<h3 style=\"'+(e?n.title[1]:\"\")+'\">'+(e?n.title[0]:n.title)+\"</h3>\":\"\"}(),c=function(){\"string\"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e='<span yes type=\"1\">'+n.btn[0]+\"</span>\",2===t&&(e='<span no type=\"0\">'+n.btn[1]+\"</span>\"+e),'<div class=\"layui-m-layerbtn\">'+e+\"</div>\"):\"\"}();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='<i></i><i class=\"layui-m-layerload\"></i><i></i><p>'+(n.content||\"\")+\"</p>\"),n.skin&&(n.anim=\"up\"),\"msg\"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?\"<div \"+(\"string\"==typeof n.shade?'style=\"'+n.shade+'\"':\"\")+' class=\"layui-m-layershade\"></div>':\"\")+'<div class=\"layui-m-layermain\" '+(n.fixed?\"\":'style=\"position:static;\"')+'><div class=\"layui-m-layersection\"><div class=\"layui-m-layerchild '+(n.skin?\"layui-m-layer-\"+n.skin+\" \":\"\")+(n.className?n.className:\"\")+\" \"+(n.anim?\"layui-m-anim-\"+n.anim:\"\")+'\" '+(n.style?'style=\"'+n.style+'\"':\"\")+\">\"+l+'<div class=\"layui-m-layercont\">'+n.content+\"</div>\"+c+\"</div></div></div>\",!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;o<r;o++)l.touch(s[o],a);if(e.shade&&e.shadeClose){var c=t[i](\"layui-m-layershade\")[0];l.touch(c,function(){layer.close(n.index,e.end)})}e.end&&(l.end[n.index]=e.end)},e.layer={v:\"2.0\",index:r,open:function(e){var t=new c(e||{});return t.index},close:function(e){var n=a(\"#\"+o[0]+e)[0];n&&(n.innerHTML=\"\",t.body.removeChild(n),clearTimeout(l.timer[e]),delete l.timer[e],\"function\"==typeof l.end[e]&&l.end[e](),delete l.end[e])},closeAll:function(){for(var e=t[i](o[0]),n=0,a=e.length;n<a;n++)layer.close(0|e[0].getAttribute(\"index\"))}},\"function\"==typeof define?define(function(){return layer}):function(){var e=document.scripts,n=e[e.length-1],i=n.src,a=i.substring(0,i.lastIndexOf(\"/\")+1);n.getAttribute(\"merge\")||document.head.appendChild(function(){var e=t.createElement(\"link\");return e.href=a+\"need/layer.css?2.0\",e.type=\"text/css\",e.rel=\"styleSheet\",e.id=\"layermcss\",e}())}()}(window);"
  },
  {
    "path": "sa-token-doc/static/layer-v3.1.1/mobile/need/layer.css",
    "content": ".layui-m-layer{position:relative;z-index:19891014}.layui-m-layer *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.layui-m-layermain,.layui-m-layershade{position:fixed;left:0;top:0;width:100%;height:100%}.layui-m-layershade{background-color:rgba(0,0,0,.7);pointer-events:auto}.layui-m-layermain{display:table;font-family:Helvetica,arial,sans-serif;pointer-events:none}.layui-m-layermain .layui-m-layersection{display:table-cell;vertical-align:middle;text-align:center}.layui-m-layerchild{position:relative;display:inline-block;text-align:left;background-color:#fff;font-size:14px;border-radius:5px;box-shadow:0 0 8px rgba(0,0,0,.1);pointer-events:auto;-webkit-overflow-scrolling:touch;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@-webkit-keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.layui-m-anim-scale{animation-name:layui-m-anim-scale;-webkit-animation-name:layui-m-anim-scale}@-webkit-keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}.layui-m-anim-up{-webkit-animation-name:layui-m-anim-up;animation-name:layui-m-anim-up}.layui-m-layer0 .layui-m-layerchild{width:90%;max-width:640px}.layui-m-layer1 .layui-m-layerchild{border:none;border-radius:0}.layui-m-layer2 .layui-m-layerchild{width:auto;max-width:260px;min-width:40px;border:none;background:0 0;box-shadow:none;color:#fff}.layui-m-layerchild h3{padding:0 10px;height:60px;line-height:60px;font-size:16px;font-weight:400;border-radius:5px 5px 0 0;text-align:center}.layui-m-layerbtn span,.layui-m-layerchild h3{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-m-layercont{padding:50px 30px;line-height:22px;text-align:center}.layui-m-layer1 .layui-m-layercont{padding:0;text-align:left}.layui-m-layer2 .layui-m-layercont{text-align:center;padding:0;line-height:0}.layui-m-layer2 .layui-m-layercont i{width:25px;height:25px;margin-left:8px;display:inline-block;background-color:#fff;border-radius:100%;-webkit-animation:layui-m-anim-loading 1.4s infinite ease-in-out;animation:layui-m-anim-loading 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.layui-m-layerbtn,.layui-m-layerbtn span{position:relative;text-align:center;border-radius:0 0 5px 5px}.layui-m-layer2 .layui-m-layercont p{margin-top:20px}@-webkit-keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}@keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.layui-m-layer2 .layui-m-layercont i:first-child{margin-left:0;-webkit-animation-delay:-.32s;animation-delay:-.32s}.layui-m-layer2 .layui-m-layercont i.layui-m-layerload{-webkit-animation-delay:-.16s;animation-delay:-.16s}.layui-m-layer2 .layui-m-layercont>div{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}"
  },
  {
    "path": "sa-token-doc/static/layer-v3.1.1/theme/default/layer.css",
    "content": ".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}}"
  },
  {
    "path": "sa-token-doc/static/page-com/github-stars-vs/echarts.min-5.4.3.js",
    "content": "\n/*\n* Licensed to the Apache Software Foundation (ASF) under one\n* or more contributor license agreements.  See the NOTICE file\n* distributed with this work for additional information\n* regarding copyright ownership.  The ASF licenses this file\n* to you under the Apache License, Version 2.0 (the\n* \"License\"); you may not use this file except in compliance\n* with the License.  You may obtain a copy of the License at\n*\n*   http://www.apache.org/licenses/LICENSE-2.0\n*\n* Unless required by applicable law or agreed to in writing,\n* software distributed under the License is distributed on an\n* \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n* KIND, either express or implied.  See the License for the\n* specific language governing permissions and limitations\n* under the License.\n*/\n\n!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\";\n/*! *****************************************************************************\n    Copyright (c) Microsoft Corporation.\n\n    Permission to use, copy, modify, and/or distribute this software for any\n    purpose with or without fee is hereby granted.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n    REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n    INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n    LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n    OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n    PERFORMANCE OF THIS SOFTWARE.\n    ***************************************************************************** */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<t.length;n++){var i=String.fromCharCode(n+32),r=(t.charCodeAt(n)-20)/100;e[i]=r}return e}(\"007LLmW'55;N0500LLLLLLLLLL00NNNLzWW\\\\\\\\WQb\\\\0FWLg\\\\bWb\\\\WQ\\\\WrWWQ000CL5LLFLL0LL**F*gLLLL5F0LF\\\\FFF5.5N\"),h={createCanvas:function(){return\"undefined\"!=typeof document&&document.createElement(\"canvas\")},measureText:function(t,e){if(!s){var n=h.createCanvas();s=n&&n.getContext(\"2d\")}if(s)return l!==e&&(l=s.font=e||a),s.measureText(t);t=t||\"\";var i=/(\\d+)px/.exec(e=e||a),r=i&&+i[1]||12,o=0;if(e.indexOf(\"mono\")>=0)o=r*t.length;else for(var c=0;c<t.length;c++){var p=u[t[c]];o+=null==p?r:p*r}return{width:o}},loadImage:function(t,e,n){var i=new Image;return i.onload=e,i.onerror=n,i.src=t,i}};function c(t){for(var e in h)t[e]&&(h[e]=t[e])}var p=V([\"Function\",\"RegExp\",\"Date\",\"Error\",\"CanvasGradient\",\"CanvasPattern\",\"Image\",\"Canvas\"],(function(t,e){return t[\"[object \"+e+\"]\"]=!0,t}),{}),d=V([\"Int8\",\"Uint8\",\"Uint8Clamped\",\"Int16\",\"Uint16\",\"Int32\",\"Uint32\",\"Float32\",\"Float64\"],(function(t,e){return t[\"[object \"+e+\"Array]\"]=!0,t}),{}),f=Object.prototype.toString,g=Array.prototype,y=g.forEach,v=g.filter,m=g.slice,x=g.map,_=function(){}.constructor,b=_?_.prototype:null,w=\"__proto__\",S=2311;function M(){return S++}function I(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];\"undefined\"!=typeof console&&console.error.apply(console,t)}function T(t){if(null==t||\"object\"!=typeof t)return t;var e=t,n=f.call(t);if(\"[object Array]\"===n){if(!pt(t)){e=[];for(var i=0,r=t.length;i<r;i++)e[i]=T(t[i])}}else if(d[n]){if(!pt(t)){var o=t.constructor;if(o.from)e=o.from(t);else{e=new o(t.length);for(i=0,r=t.length;i<r;i++)e[i]=t[i]}}}else if(!p[n]&&!pt(t)&&!J(t))for(var a in e={},t)t.hasOwnProperty(a)&&a!==w&&(e[a]=T(t[a]));return e}function C(t,e,n){if(!q(e)||!q(t))return n?T(e):t;for(var i in e)if(e.hasOwnProperty(i)&&i!==w){var r=t[i],o=e[i];!q(o)||!q(r)||Y(o)||Y(r)||J(o)||J(r)||K(o)||K(r)||pt(o)||pt(r)?!n&&i in t||(t[i]=T(e[i])):C(r,o,n)}return t}function D(t,e){for(var n=t[0],i=1,r=t.length;i<r;i++)n=C(n,t[i],e);return n}function A(t,e){if(Object.assign)Object.assign(t,e);else for(var n in e)e.hasOwnProperty(n)&&n!==w&&(t[n]=e[n]);return t}function k(t,e,n){for(var i=G(e),r=0;r<i.length;r++){var o=i[r];(n?null!=e[o]:null==t[o])&&(t[o]=e[o])}return t}var L=h.createCanvas;function P(t,e){if(t){if(t.indexOf)return t.indexOf(e);for(var n=0,i=t.length;n<i;n++)if(t[n]===e)return n}return-1}function O(t,e){var n=t.prototype;function i(){}for(var r in i.prototype=e.prototype,t.prototype=new i,n)n.hasOwnProperty(r)&&(t.prototype[r]=n[r]);t.prototype.constructor=t,t.superClass=e}function R(t,e,n){if(t=\"prototype\"in t?t.prototype:t,e=\"prototype\"in e?e.prototype:e,Object.getOwnPropertyNames)for(var i=Object.getOwnPropertyNames(e),r=0;r<i.length;r++){var o=i[r];\"constructor\"!==o&&(n?null!=e[o]:null==t[o])&&(t[o]=e[o])}else k(t,e,n)}function N(t){return!!t&&(\"string\"!=typeof t&&\"number\"==typeof t.length)}function E(t,e,n){if(t&&e)if(t.forEach&&t.forEach===y)t.forEach(e,n);else if(t.length===+t.length)for(var i=0,r=t.length;i<r;i++)e.call(n,t[i],i,t);else for(var o in t)t.hasOwnProperty(o)&&e.call(n,t[o],o,t)}function z(t,e,n){if(!t)return[];if(!e)return at(t);if(t.map&&t.map===x)return t.map(e,n);for(var i=[],r=0,o=t.length;r<o;r++)i.push(e.call(n,t[r],r,t));return i}function V(t,e,n,i){if(t&&e){for(var r=0,o=t.length;r<o;r++)n=e.call(i,n,t[r],r,t);return n}}function B(t,e,n){if(!t)return[];if(!e)return at(t);if(t.filter&&t.filter===v)return t.filter(e,n);for(var i=[],r=0,o=t.length;r<o;r++)e.call(n,t[r],r,t)&&i.push(t[r]);return i}function F(t,e,n){if(t&&e)for(var i=0,r=t.length;i<r;i++)if(e.call(n,t[i],i,t))return t[i]}function G(t){if(!t)return[];if(Object.keys)return Object.keys(t);var e=[];for(var n in t)t.hasOwnProperty(n)&&e.push(n);return e}var W=b&&X(b.bind)?b.call.bind(b.bind):function(t,e){for(var n=[],i=2;i<arguments.length;i++)n[i-2]=arguments[i];return function(){return t.apply(e,n.concat(m.call(arguments)))}};function H(t){for(var e=[],n=1;n<arguments.length;n++)e[n-1]=arguments[n];return function(){return t.apply(this,e.concat(m.call(arguments)))}}function Y(t){return Array.isArray?Array.isArray(t):\"[object Array]\"===f.call(t)}function X(t){return\"function\"==typeof t}function U(t){return\"string\"==typeof t}function Z(t){return\"[object String]\"===f.call(t)}function j(t){return\"number\"==typeof t}function q(t){var e=typeof t;return\"function\"===e||!!t&&\"object\"===e}function K(t){return!!p[f.call(t)]}function $(t){return!!d[f.call(t)]}function J(t){return\"object\"==typeof t&&\"number\"==typeof t.nodeType&&\"object\"==typeof t.ownerDocument}function Q(t){return null!=t.colorStops}function tt(t){return null!=t.image}function et(t){return\"[object RegExp]\"===f.call(t)}function nt(t){return t!=t}function it(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];for(var n=0,i=t.length;n<i;n++)if(null!=t[n])return t[n]}function rt(t,e){return null!=t?t:e}function ot(t,e,n){return null!=t?t:null!=e?e:n}function at(t){for(var e=[],n=1;n<arguments.length;n++)e[n-1]=arguments[n];return m.apply(t,e)}function st(t){if(\"number\"==typeof t)return[t,t,t,t];var e=t.length;return 2===e?[t[0],t[1],t[0],t[1]]:3===e?[t[0],t[1],t[2],t[1]]:t}function lt(t,e){if(!t)throw new Error(e)}function ut(t){return null==t?null:\"function\"==typeof t.trim?t.trim():t.replace(/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,\"\")}var ht=\"__ec_primitive__\";function ct(t){t[ht]=!0}function pt(t){return t[ht]}var dt=function(){function t(){this.data={}}return t.prototype.delete=function(t){var e=this.has(t);return e&&delete this.data[t],e},t.prototype.has=function(t){return this.data.hasOwnProperty(t)},t.prototype.get=function(t){return this.data[t]},t.prototype.set=function(t,e){return this.data[t]=e,this},t.prototype.keys=function(){return G(this.data)},t.prototype.forEach=function(t){var e=this.data;for(var n in e)e.hasOwnProperty(n)&&t(e[n],n)},t}(),ft=\"function\"==typeof Map;var gt=function(){function t(e){var n=Y(e);this.data=ft?new Map:new dt;var i=this;function r(t,e){n?i.set(t,e):i.set(e,t)}e instanceof t?e.each(r):e&&E(e,r)}return t.prototype.hasKey=function(t){return this.data.has(t)},t.prototype.get=function(t){return this.data.get(t)},t.prototype.set=function(t,e){return this.data.set(t,e),e},t.prototype.each=function(t,e){this.data.forEach((function(n,i){t.call(e,n,i)}))},t.prototype.keys=function(){var t=this.data.keys();return ft?Array.from(t):t},t.prototype.removeKey=function(t){this.data.delete(t)},t}();function yt(t){return new gt(t)}function vt(t,e){for(var n=new t.constructor(t.length+e.length),i=0;i<t.length;i++)n[i]=t[i];var r=t.length;for(i=0;i<e.length;i++)n[i+r]=e[i];return n}function mt(t,e){var n;if(Object.create)n=Object.create(t);else{var i=function(){};i.prototype=t,n=new i}return e&&A(n,e),n}function xt(t){var e=t.style;e.webkitUserSelect=\"none\",e.userSelect=\"none\",e.webkitTapHighlightColor=\"rgba(0,0,0,0)\",e[\"-webkit-touch-callout\"]=\"none\"}function _t(t,e){return t.hasOwnProperty(e)}function bt(){}var wt=180/Math.PI,St=Object.freeze({__proto__:null,guid:M,logError:I,clone:T,merge:C,mergeAll:D,extend:A,defaults:k,createCanvas:L,indexOf:P,inherits:O,mixin:R,isArrayLike:N,each:E,map:z,reduce:V,filter:B,find:F,keys:G,bind:W,curry:H,isArray:Y,isFunction:X,isString:U,isStringSafe:Z,isNumber:j,isObject:q,isBuiltInObject:K,isTypedArray:$,isDom:J,isGradientObject:Q,isImagePatternObject:tt,isRegExp:et,eqNaN:nt,retrieve:it,retrieve2:rt,retrieve3:ot,slice:at,normalizeCssArray:st,assert:lt,trim:ut,setAsPrimitive:ct,isPrimitive:pt,HashMap:gt,createHashMap:yt,concatArray:vt,createObject:mt,disableUserSelect:xt,hasOwn:_t,noop:bt,RADIAN_TO_DEGREE:wt});function Mt(t,e){return null==t&&(t=0),null==e&&(e=0),[t,e]}function It(t,e){return t[0]=e[0],t[1]=e[1],t}function Tt(t){return[t[0],t[1]]}function Ct(t,e,n){return t[0]=e,t[1]=n,t}function Dt(t,e,n){return t[0]=e[0]+n[0],t[1]=e[1]+n[1],t}function At(t,e,n,i){return t[0]=e[0]+n[0]*i,t[1]=e[1]+n[1]*i,t}function kt(t,e,n){return t[0]=e[0]-n[0],t[1]=e[1]-n[1],t}function Lt(t){return Math.sqrt(Ot(t))}var Pt=Lt;function Ot(t){return t[0]*t[0]+t[1]*t[1]}var Rt=Ot;function Nt(t,e,n){return t[0]=e[0]*n,t[1]=e[1]*n,t}function Et(t,e){var n=Lt(e);return 0===n?(t[0]=0,t[1]=0):(t[0]=e[0]/n,t[1]=e[1]/n),t}function zt(t,e){return Math.sqrt((t[0]-e[0])*(t[0]-e[0])+(t[1]-e[1])*(t[1]-e[1]))}var Vt=zt;function Bt(t,e){return(t[0]-e[0])*(t[0]-e[0])+(t[1]-e[1])*(t[1]-e[1])}var Ft=Bt;function Gt(t,e,n,i){return t[0]=e[0]+i*(n[0]-e[0]),t[1]=e[1]+i*(n[1]-e[1]),t}function Wt(t,e,n){var i=e[0],r=e[1];return t[0]=n[0]*i+n[2]*r+n[4],t[1]=n[1]*i+n[3]*r+n[5],t}function Ht(t,e,n){return t[0]=Math.min(e[0],n[0]),t[1]=Math.min(e[1],n[1]),t}function Yt(t,e,n){return t[0]=Math.max(e[0],n[0]),t[1]=Math.max(e[1],n[1]),t}var Xt=Object.freeze({__proto__:null,create:Mt,copy:It,clone:Tt,set:Ct,add:Dt,scaleAndAdd:At,sub:kt,len:Lt,length:Pt,lenSquare:Ot,lengthSquare:Rt,mul:function(t,e,n){return t[0]=e[0]*n[0],t[1]=e[1]*n[1],t},div:function(t,e,n){return t[0]=e[0]/n[0],t[1]=e[1]/n[1],t},dot:function(t,e){return t[0]*e[0]+t[1]*e[1]},scale:Nt,normalize:Et,distance:zt,dist:Vt,distanceSquare:Bt,distSquare:Ft,negate:function(t,e){return t[0]=-e[0],t[1]=-e[1],t},lerp:Gt,applyTransform:Wt,min:Ht,max:Yt}),Ut=function(t,e){this.target=t,this.topTarget=e&&e.topTarget},Zt=function(){function t(t){this.handler=t,t.on(\"mousedown\",this._dragStart,this),t.on(\"mousemove\",this._drag,this),t.on(\"mouseup\",this._dragEnd,this)}return t.prototype._dragStart=function(t){for(var e=t.target;e&&!e.draggable;)e=e.parent||e.__hostTarget;e&&(this._draggingTarget=e,e.dragging=!0,this._x=t.offsetX,this._y=t.offsetY,this.handler.dispatchToElement(new Ut(e,t),\"dragstart\",t.event))},t.prototype._drag=function(t){var e=this._draggingTarget;if(e){var n=t.offsetX,i=t.offsetY,r=n-this._x,o=i-this._y;this._x=n,this._y=i,e.drift(r,o,t),this.handler.dispatchToElement(new Ut(e,t),\"drag\",t.event);var a=this.handler.findHover(n,i,e).target,s=this._dropTarget;this._dropTarget=a,e!==a&&(s&&a!==s&&this.handler.dispatchToElement(new Ut(s,t),\"dragleave\",t.event),a&&a!==s&&this.handler.dispatchToElement(new Ut(a,t),\"dragenter\",t.event))}},t.prototype._dragEnd=function(t){var e=this._draggingTarget;e&&(e.dragging=!1),this.handler.dispatchToElement(new Ut(e,t),\"dragend\",t.event),this._dropTarget&&this.handler.dispatchToElement(new Ut(this._dropTarget,t),\"drop\",t.event),this._draggingTarget=null,this._dropTarget=null},t}(),jt=function(){function t(t){t&&(this._$eventProcessor=t)}return t.prototype.on=function(t,e,n,i){this._$handlers||(this._$handlers={});var r=this._$handlers;if(\"function\"==typeof e&&(i=n,n=e,e=null),!n||!t)return this;var o=this._$eventProcessor;null!=e&&o&&o.normalizeQuery&&(e=o.normalizeQuery(e)),r[t]||(r[t]=[]);for(var a=0;a<r[t].length;a++)if(r[t][a].h===n)return this;var s={h:n,query:e,ctx:i||this,callAtLast:n.zrEventfulCallAtLast},l=r[t].length-1,u=r[t][l];return u&&u.callAtLast?r[t].splice(l,0,s):r[t].push(s),this},t.prototype.isSilent=function(t){var e=this._$handlers;return!e||!e[t]||!e[t].length},t.prototype.off=function(t,e){var n=this._$handlers;if(!n)return this;if(!t)return this._$handlers={},this;if(e){if(n[t]){for(var i=[],r=0,o=n[t].length;r<o;r++)n[t][r].h!==e&&i.push(n[t][r]);n[t]=i}n[t]&&0===n[t].length&&delete n[t]}else delete n[t];return this},t.prototype.trigger=function(t){for(var e=[],n=1;n<arguments.length;n++)e[n-1]=arguments[n];if(!this._$handlers)return this;var i=this._$handlers[t],r=this._$eventProcessor;if(i)for(var o=e.length,a=i.length,s=0;s<a;s++){var l=i[s];if(!r||!r.filter||null==l.query||r.filter(t,l.query))switch(o){case 0:l.h.call(l.ctx);break;case 1:l.h.call(l.ctx,e[0]);break;case 2:l.h.call(l.ctx,e[0],e[1]);break;default:l.h.apply(l.ctx,e)}}return r&&r.afterTrigger&&r.afterTrigger(t),this},t.prototype.triggerWithContext=function(t){for(var e=[],n=1;n<arguments.length;n++)e[n-1]=arguments[n];if(!this._$handlers)return this;var i=this._$handlers[t],r=this._$eventProcessor;if(i)for(var o=e.length,a=e[o-1],s=i.length,l=0;l<s;l++){var u=i[l];if(!r||!r.filter||null==u.query||r.filter(t,u.query))switch(o){case 0:u.h.call(a);break;case 1:u.h.call(a,e[0]);break;case 2:u.h.call(a,e[0],e[1]);break;default:u.h.apply(a,e.slice(1,o-1))}}return r&&r.afterTrigger&&r.afterTrigger(t),this},t}(),qt=Math.log(2);function Kt(t,e,n,i,r,o){var a=i+\"-\"+r,s=t.length;if(o.hasOwnProperty(a))return o[a];if(1===e){var l=Math.round(Math.log((1<<s)-1&~r)/qt);return t[n][l]}for(var u=i|1<<n,h=n+1;i&1<<h;)h++;for(var c=0,p=0,d=0;p<s;p++){var f=1<<p;f&r||(c+=(d%2?-1:1)*t[n][p]*Kt(t,e-1,h,u,r|f,o),d++)}return o[a]=c,c}function $t(t,e){var n=[[t[0],t[1],1,0,0,0,-e[0]*t[0],-e[0]*t[1]],[0,0,0,t[0],t[1],1,-e[1]*t[0],-e[1]*t[1]],[t[2],t[3],1,0,0,0,-e[2]*t[2],-e[2]*t[3]],[0,0,0,t[2],t[3],1,-e[3]*t[2],-e[3]*t[3]],[t[4],t[5],1,0,0,0,-e[4]*t[4],-e[4]*t[5]],[0,0,0,t[4],t[5],1,-e[5]*t[4],-e[5]*t[5]],[t[6],t[7],1,0,0,0,-e[6]*t[6],-e[6]*t[7]],[0,0,0,t[6],t[7],1,-e[7]*t[6],-e[7]*t[7]]],i={},r=Kt(n,8,0,0,0,i);if(0!==r){for(var o=[],a=0;a<8;a++)for(var s=0;s<8;s++)null==o[s]&&(o[s]=0),o[s]+=((a+s)%2?-1:1)*Kt(n,7,0===a?1:0,1<<a,1<<s,i)/r*e[a];return function(t,e,n){var i=e*o[6]+n*o[7]+1;t[0]=(e*o[0]+n*o[1]+o[2])/i,t[1]=(e*o[3]+n*o[4]+o[5])/i}}}var Jt=\"___zrEVENTSAVED\",Qt=[];function te(t,e,n,i,o){if(e.getBoundingClientRect&&r.domSupported&&!ee(e)){var a=e[Jt]||(e[Jt]={}),s=function(t,e){var n=e.markers;if(n)return n;n=e.markers=[];for(var i=[\"left\",\"right\"],r=[\"top\",\"bottom\"],o=0;o<4;o++){var a=document.createElement(\"div\"),s=o%2,l=(o>>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={\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\"};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;o<a;o++){var s=i[o],l=le(n,s,{});r.points.push([l.zrX,l.zrY]),r.touches.push(s)}this._track.push(r)}},t.prototype._recognize=function(t){for(var e in ve)if(ve.hasOwnProperty(e)){var n=ve[e](this._track,t);if(n)return n}},t}();function ye(t){var e=t[1][0]-t[0][0],n=t[1][1]-t[0][1];return Math.sqrt(e*e+n*n)}var ve={pinch:function(t,e){var n=t.length;if(n){var i,r=(t[n-1]||{}).points,o=(t[n-2]||{}).points||r;if(o&&o.length>1&&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=!(o<l||u<r||s<h||c<a);if(n){var d=1/0,f=0,g=Math.abs(o-l),y=Math.abs(u-r),v=Math.abs(s-h),m=Math.abs(c-a),x=Math.min(g,y),_=Math.min(v,m);o<l||u<r?x>f&&(f=x,g<y?De.set(Ee,-g,0):De.set(Ee,y,0)):x<d&&(d=x,g<y?De.set(Ne,g,0):De.set(Ne,-y,0)),s<h||c<a?_>f&&(f=_,v<m?De.set(Ee,0,-v):De.set(Ee,0,m)):x<d&&(d=x,v<m?De.set(Ne,0,v):De.set(Ne,0,-m))}return n&&De.copy(n,p?Ne:Ee),p},t.prototype.contain=function(t,e){var n=this;return t>=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<s;d+=4)for(var f=0;f<p;f+=c){if(Ue(o,r,t+d*Math.cos(f),e+d*Math.sin(f),n),r.target)return r}}return r},e.prototype.processGesture=function(t,e){this._gestureMgr||(this._gestureMgr=new ge);var n=this._gestureMgr;\"start\"===e&&n.clear();var i=n.recognize(t,this.findHover(t.zrX,t.zrY,null).target,this.proxy.dom);if(\"end\"===e&&n.clear(),i){var r=i.type;t.gestureEvent=r;var o=new Ge;o.target=i.target,this.dispatchToElement(o,r,i.event)}},e}(jt);function Xe(t,e,n){if(t[t.rectHover?\"rectContain\":\"contain\"](e,n)){for(var i=t,r=void 0,o=!1;i;){if(i.ignoreClip&&(o=!0),!o){var a=i.getClipPath();if(a&&!a.contain(e,n))return!1;i.silent&&(r=!0)}var s=i.__hostTarget;i=s||i.parent}return!r||Ve}return!1}function Ue(t,e,n,i,r){for(var o=t.length-1;o>=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<n&&i(t[r],t[r-1])<0;)r++;!function(t,e,n){n--;for(;e<n;){var i=t[e];t[e++]=t[n],t[n--]=i}}(t,e,r)}else for(;r<n&&i(t[r],t[r-1])>=0;)r++;return r-e}function qe(t,e,n,i,r){for(i===e&&i++;i<n;i++){for(var o,a=t[i],s=e,l=i;s<l;)r(a,t[o=s+l>>>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;l<s&&o(t,e[n+r+l])>0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}else{for(s=r+1;l<s&&o(t,e[n+r-l])<=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s);var u=a;a=r-l,l=r-u}for(a++;a<l;){var h=a+(l-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;l<s&&o(t,e[n+r-l])<0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s);var u=a;a=r-l,l=r-u}else{for(s=i-r;l<s&&o(t,e[n+r+l])>=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}for(a++;a<l;){var h=a+(l-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<i;l++)a[l]=t[n+l];var u=0,h=o,c=n;if(t[c++]=t[h++],0==--s){for(l=0;l<i;l++)t[c+l]=a[u+l];return}if(1===i){for(l=0;l<s;l++)t[c+l]=t[h+l];return void(t[c+s]=a[u])}var p,d,f,g=r;for(;;){p=0,d=0,f=!1;do{if(e(t[h],a[u])<0){if(t[c++]=t[h++],d++,p=0,0==--s){f=!0;break}}else if(t[c++]=a[u++],p++,d=0,1==--i){f=!0;break}}while((p|d)<g);if(f)break;do{if(0!==(p=$e(t[h],a,u,i,0,e))){for(l=0;l<p;l++)t[c+l]=a[u+l];if(c+=p,u+=p,(i-=p)<=1){f=!0;break}}if(t[c++]=t[h++],0==--s){f=!0;break}if(0!==(d=Ke(a[u],t,h,s,0,e))){for(l=0;l<d;l++)t[c+l]=t[h+l];if(c+=d,h+=d,0===(s-=d)){f=!0;break}}if(t[c++]=a[u++],1==--i){f=!0;break}g--}while(p>=7||d>=7);if(f)break;g<0&&(g=0),g+=2}if((r=g)<1&&(r=1),1===i){for(l=0;l<s;l++)t[c+l]=t[h+l];t[c+s]=a[u]}else{if(0===i)throw new Error;for(l=0;l<i;l++)t[c+l]=a[u+l]}}(l,u,h,c):function(n,i,o,s){var l=0;for(l=0;l<s;l++)a[l]=t[o+l];var u=n+i-1,h=s-1,c=o+s-1,p=0,d=0;if(t[c--]=t[u--],0==--i){for(p=c-(s-1),l=0;l<s;l++)t[p+l]=a[l];return}if(1===s){for(d=(c-=i)+1,p=(u-=i)+1,l=i-1;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)<f);if(v)break;do{if(0!==(g=i-$e(a[h],t,n,i,i-1,e))){for(i-=g,d=(c-=g)+1,p=(u-=g)+1,l=g-1;l>=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<y;l++)t[d+l]=a[p+l];if(s<=1){v=!0;break}}if(t[c--]=t[u--],0==--i){v=!0;break}f--}while(g>=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;l<s;l++)t[p+l]=a[l]}}(l,u,h,c))}return n=[],i=[],{mergeRuns:function(){for(;o>1;){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]&&t--;else if(i[t]>i[t+1])break;s(t)}},forceMergeRuns:function(){for(;o>1;){var t=o-2;t>0&&i[t-1]<i[t+1]&&t--,s(t)}},pushRun:function(t,e){n[o]=t,i[o]=e,o+=1}}}function Qe(t,e,n,i){n||(n=0),i||(i=t.length);var r=i-n;if(!(r<2)){var o=0;if(r<32)qe(t,n,i,n+(o=je(t,n,i,e)),e);else{var a=Je(t,e),s=function(t){for(var e=0;t>=32;)e|=1&t,t>>=1;return t+e}(r);do{if((o=je(t,n,i,e))<s){var l=r;l>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;n<this._roots.length;n++)this._roots[n].traverse(t,e)},t.prototype.getDisplayList=function(t,e){e=e||!1;var n=this._displayList;return!t&&n.length||this.updateDisplayList(e),n},t.prototype.updateDisplayList=function(t){this._displayListLen=0;for(var e=this._roots,n=this._displayList,i=0,r=e.length;i<r;i++)this._updateAndAddDisplayable(e[i],null,t);n.length=this._displayListLen,Qe(n,nn)},t.prototype._updateAndAddDisplayable=function(t,e,n){if(!t.ignore||n){t.beforeUpdate(),t.update(),t.afterUpdate();var i=t.getClipPath();if(t.ignoreClip)e=null;else if(i){e=e?e.slice():[];for(var r=i,o=t;r;)r.parent=o,r.updateTransform(),e.push(r),o=r,r=r.getClipPath()}if(t.childrenRef){for(var a=t.childrenRef(),s=0;s<a.length;s++){var l=a[s];t.__dirty&&(l.__dirty|=1),this._updateAndAddDisplayable(l,e,n)}t.__dirty=0}else{var u=t;e&&e.length?u.__clipPaths=e:u.__clipPaths&&u.__clipPaths.length>0&&(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<n;e++)this.delRoot(t[e]);else{var i=P(this._roots,t);i>=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&&t<un}function vn(t){return t>un||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))<v&&(c=m,v=f);v=1/0;for(var x=0;x<32&&!(y<hn);x++)p=c-y,d=c+y,fn[0]=mn(t,n,r,a,p),fn[1]=mn(e,i,o,s,p),f=Ft(fn,dn),p>=0&&f<v?(c=p,v=f):(gn[0]=mn(t,n,r,a,d),gn[1]=mn(e,i,o,s,d),g=Ft(gn,dn),d<=1&&g<v?(c=d,v=g):y*=.5);return h&&(h[0]=mn(t,n,r,a,c),h[1]=mn(e,i,o,s,c)),ln(v)}function Mn(t,e,n,i,r,o,a,s,l){for(var u=t,h=e,c=0,p=1/l,d=1;d<=l;d++){var f=d*p,g=mn(t,n,r,a,f),y=mn(e,i,o,s,f),v=g-u,m=y-h;c+=Math.sqrt(v*v+m*m),u=g,h=y}return c}function In(t,e,n,i){var r=1-i;return r*(r*t+2*i*e)+i*i*n}function Tn(t,e,n,i){return 2*((1-i)*(e-t)+i*(n-e))}function Cn(t,e,n){var i=t+n-2*e;return 0===i?.5:(t-e)/i}function Dn(t,e,n,i,r){var o=(e-t)*i+t,a=(n-e)*i+e,s=(a-o)*i+o;r[0]=t,r[1]=o,r[2]=s,r[3]=s,r[4]=a,r[5]=n}function An(t,e,n,i,r,o,a,s,l){var u,h=.005,c=1/0;dn[0]=a,dn[1]=s;for(var p=0;p<1;p+=.05){fn[0]=In(t,n,r,p),fn[1]=In(e,i,o,p),(y=Ft(dn,fn))<c&&(u=p,c=y)}c=1/0;for(var d=0;d<32&&!(h<hn);d++){var f=u-h,g=u+h;fn[0]=In(t,n,r,f),fn[1]=In(e,i,o,f);var y=Ft(fn,dn);if(f>=0&&y<c)u=f,c=y;else{gn[0]=In(t,n,r,g),gn[1]=In(e,i,o,g);var v=Ft(gn,dn);g<=1&&v<c?(u=g,c=v):h*=.5}}return l&&(l[0]=In(t,n,r,u),l[1]=In(e,i,o,u)),ln(c)}function kn(t,e,n,i,r,o,a){for(var s=t,l=e,u=0,h=1/a,c=1;c<=a;c++){var p=c*h,d=In(t,n,r,p),f=In(e,i,o,p),g=d-s,y=f-l;u+=Math.sqrt(g*g+y*y),s=d,l=f}return u}var Ln=/cubic-bezier\\(([0-9,\\.e ]+)\\)/;function Pn(t){var e=t&&Ln.exec(t);if(e){var n=e[1].split(\",\"),i=+ut(n[0]),r=+ut(n[1]),o=+ut(n[2]),a=+ut(n[3]);if(isNaN(i+r+o+a))return;var s=[];return function(t){return t<=0?0:t>=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<ui&&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;o<r;o++)t[o]=Mi(e[o],n[o],i);return t}function Ti(t,e,n,i){for(var r=e.length,o=0;o<r;o++)t[o]=e[o]+n[o]*i;return t}function Ci(t,e,n,i){for(var r=e.length,o=r&&e[0].length,a=0;a<r;a++){t[a]||(t[a]=[]);for(var s=0;s<o;s++)t[a][s]=e[a][s]+n[a][s]*i}return t}function Di(t,e){for(var n=t.length,i=e.length,r=n>i?e:t,o=Math.min(n,i),a=r[o-1]||{color:[0,0,0,0],offset:0},s=o;s<Math.max(n,i);s++)r.push({offset:a.offset,color:a.color.slice()})}function Ai(t,e,n){var i=t,r=e;if(i.push&&r.push){var o=i.length,a=r.length;if(o!==a)if(o>a)i.length=a;else for(var s=o;s<a;s++)i.push(1===n?r[s]:Si.call(r[s]));var l=i[0]&&i[0].length;for(s=0;s<i.length;s++)if(1===n)isNaN(i[s])&&(i[s]=r[s]);else for(var u=0;u<l;u++)isNaN(i[s][u])&&(i[s][u]=r[s][u])}}function ki(t){if(N(t)){var e=t.length;if(N(t[0])){for(var n=[],i=0;i<e;i++)n.push(Si.call(t[i]));return n}return Si.call(t)}return t}function Li(t){return t[0]=Math.floor(t[0])||0,t[1]=Math.floor(t[1])||0,t[2]=Math.floor(t[2])||0,t[3]=null==t[3]?1:t[3],\"rgba(\"+t.join(\",\")+\")\"}function Pi(t){return 4===t||5===t}function Oi(t){return 1===t||2===t}var Ri=[0,0,0,0],Ni=function(){function t(t){this.keyframes=[],this.discrete=!1,this._invalid=!1,this._needsSort=!1,this._lastFr=0,this._lastFrP=0,this.propName=t}return t.prototype.isFinished=function(){return this._finished},t.prototype.setFinished=function(){this._finished=!0,this._additiveTrack&&this._additiveTrack.setFinished()},t.prototype.needsAnimate=function(){return this.keyframes.length>=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<r;u++){var h=n[u],c=h.value,p=o.value;h.percent=h.time/t,a||(s&&u!==r-1?Ai(c,p,i):l&&Di(c.colorStops,p.colorStops))}if(!a&&5!==i&&e&&this.needsAnimate()&&e.needsAnimate()&&i===e.valType&&!e._finished){this._additiveTrack=e;var d=n[0].value;for(u=0;u<r;u++)0===i?n[u].additiveValue=n[u].value-d:3===i?n[u].additiveValue=Ti([],n[u].value,d,-1):Oi(i)&&(n[u].additiveValue=1===i?Ti([],n[u].value,d,-1):Ci([],n[u].value,d,-1))}},t.prototype.step=function(t,e){if(!this._finished){this._additiveTrack&&this._additiveTrack._finished&&(this._additiveTrack=null);var n,i,r,o=null!=this._additiveTrack,a=o?\"additiveValue\":\"value\",s=this.valType,l=this.keyframes,u=l.length,h=this.propName,c=3===s,p=this._lastFr,d=Math.min;if(1===u)i=r=l[0];else{if(e<0)n=0;else if(e<this._lastFrP){for(n=d(p+1,u-1);n>=0&&!(l[n].percent<=e);n--);n=d(n,u-2)}else{for(n=p;n<u&&!(l[n].percent>e);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;a<r;a++){t[a]||(t[a]=[]);for(var s=0;s<o;s++)t[a][s]=Mi(e[a][s],n[a][s],i)}}(y,i[a],r[a],g);else if(Pi(s)){var v=i[a],m=r[a],x=4===s;t[h]={type:x?\"linear\":\"radial\",x:Mi(v.x,m.x,g),y:Mi(v.y,m.y,g),colorStops:z(v.colorStops,(function(t,e){var n=m.colorStops[e];return{offset:Mi(t.offset,n.offset,g),color:Li(Ii([],t.color,n.color,g))}})),global:m.global},x?(t[h].x2=Mi(v.x2,m.x2,g),t[h].y2=Mi(v.y2,m.y2,g)):t[h].r=Mi(v.r,m.r,g)}else if(c)Ii(y,i[a],r[a],g),o||(t[h]=Li(y));else{var _=Mi(i[a],r[a],g);o?this._additiveValue=_:t[h]=_}o&&this._addToTarget(t)}}},t.prototype._addToTarget=function(t){var e=this.valType,n=this.propName,i=this._additiveValue;0===e?t[n]=t[n]+i:3===e?(qn(t[n],Ri),Ti(Ri,Ri,i,1),t[n]=Li(Ri)):1===e?Ti(t[n],t[n],i,1):2===e&&Ci(t[n],t[n],i,1)},t}(),Ei=function(){function t(t,e,n,i){this._tracks={},this._trackKeys=[],this._maxTime=0,this._started=0,this._clip=null,this._target=t,this._loop=e,e&&i?I(\"Can' use additive animation on looped animation.\"):(this._additiveAnimators=i,this._allowDiscrete=n)}return t.prototype.getMaxTime=function(){return this._maxTime},t.prototype.getDelay=function(){return this._delay},t.prototype.getLoop=function(){return this._loop},t.prototype.getTarget=function(){return this._target},t.prototype.changeTarget=function(t){this._target=t},t.prototype.when=function(t,e,n){return this.whenWithKeys(t,e,G(e),n)},t.prototype.whenWithKeys=function(t,e,n,i){for(var r=this._tracks,o=0;o<n.length;o++){var a=n[o],s=r[a];if(!s){s=r[a]=new Ni(a);var l=void 0,u=this._getAdditiveTrack(a);if(u){var h=u.keyframes,c=h[h.length-1];l=c&&c.value,3===u.valType&&l&&(l=Li(l))}else l=this._target[a];if(null==l)continue;t>0&&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;n<e;n++)t[n].call(this)},t.prototype._abortedCallback=function(){this._setTracksFinished();var t=this.animation,e=this._abortedCbs;if(t&&t.removeClip(this._clip),this._clip=null,e)for(var n=0;n<e.length;n++)e[n].call(this)},t.prototype._setTracksFinished=function(){for(var t=this._tracks,e=this._trackKeys,n=0;n<e.length;n++)t[e[n]].setFinished()},t.prototype._getAdditiveTrack=function(t){var e,n=this._additiveAnimators;if(n)for(var i=0;i<n.length;i++){var r=n[i].getTrack(t);r&&(e=r)}return e},t.prototype.start=function(t){if(!(this._started>0)){this._started=1;for(var e=this,n=[],i=this._maxTime||0,r=0;r<this._trackKeys.length;r++){var o=this._trackKeys[r],a=this._tracks[o],s=this._getAdditiveTrack(o),l=a.keyframes,u=l.length;if(a.prepare(i,s),a.needsAnimate())if(!this._allowDiscrete&&a.discrete){var h=l[u-1];h&&(e._target[a.propName]=h.rawValue),a.setFinished()}else n.push(a)}if(n.length||this._force){var c=new On({life:i,loop:this._loop,delay:this._delay||0,onframe:function(t){e._started=2;var i=e._additiveAnimators;if(i){for(var r=!1,o=0;o<i.length;o++)if(i[o]._clip){r=!0;break}r||(e._additiveAnimators=null)}for(o=0;o<n.length;o++)n[o].step(e._target,t);var a=e._onframeCbs;if(a)for(o=0;o<a.length;o++)a[o](e._target,t)},ondestroy:function(){e._doneCallback()}});this._clip=c,this.animation&&this.animation.addClip(c),t&&c.setEasing(t)}else this._doneCallback();return this}},t.prototype.stop=function(t){if(this._clip){var e=this._clip;t&&e.onframe(1),this._abortedCallback()}},t.prototype.delay=function(t){return this._delay=t,this},t.prototype.during=function(t){return t&&(this._onframeCbs||(this._onframeCbs=[]),this._onframeCbs.push(t)),this},t.prototype.done=function(t){return t&&(this._doneCbs||(this._doneCbs=[]),this._doneCbs.push(t)),this},t.prototype.aborted=function(t){return t&&(this._abortedCbs||(this._abortedCbs=[]),this._abortedCbs.push(t)),this},t.prototype.getClip=function(){return this._clip},t.prototype.getTrack=function(t){return this._tracks[t]},t.prototype.getTracks=function(){var t=this;return z(this._trackKeys,(function(e){return t._tracks[e]}))},t.prototype.stopTracks=function(t,e){if(!t.length||!this._clip)return!0;for(var n=this._tracks,i=this._trackKeys,r=0;r<t.length;r++){var o=n[t[r]];o&&!o.isFinished()&&(e?o.step(this._target,1):1===this._started&&o.step(this._target,0),o.setFinished())}var a=!0;for(r=0;r<i.length;r++)if(!n[i[r]].isFinished()){a=!1;break}return a&&this._abortedCallback(),a},t.prototype.saveTo=function(t,e,n){if(t){e=e||this._trackKeys;for(var i=0;i<e.length;i++){var r=e[i],o=this._tracks[r];if(o&&!o.isFinished()){var a=o.keyframes,s=a[n?0:a.length-1];s&&(t[r]=ki(s.rawValue))}}}},t.prototype.__changeFinalValue=function(t,e){e=e||G(t);for(var n=0;n<e.length;n++){var i=e[n],r=this._tracks[i];if(r){var o=r.keyframes;if(o.length>1){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<yr.length;n++){var i=yr[n];t[i]=e[i]}}var mr={};function xr(t,e){var n=mr[e=e||a];n||(n=mr[e]=new En(500));var i=n.get(t);return null==i&&(i=h.measureText(t,e).width,n.put(t,i)),i}function _r(t,e,n,i){var r=xr(t,e),o=Mr(e),a=wr(0,r,n),s=Sr(0,o,i);return new ze(a,s,r,o)}function br(t,e,n,i){var r=((t||\"\")+\"\").split(\"\\n\");if(1===r.length)return _r(r[0],e,n,i);for(var o=new ze(0,0,0,0),a=0;a<r.length;a++){var s=_r(r[a],e,n,i);0===a?o.copy(s):o.union(s)}return o}function wr(t,e,n){return\"right\"===n?t-=e:\"center\"===n&&(t-=e/2),t}function Sr(t,e,n){return\"middle\"===n?t-=e/2:\"bottom\"===n&&(t-=e),t}function Mr(t){return xr(\"国\",t)}function Ir(t,e){return\"string\"==typeof t?t.lastIndexOf(\"%\")>=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;i<n.length;i++){var r=n[i];this.attrKV(r,t[r])}return this.markRedraw(),this},t.prototype.saveCurrentToNormalState=function(t){this._innerSaveToNormal(t);for(var e=this._normalState,n=0;n<this.animators.length;n++){var i=this.animators[n],r=i.__fromStateTransition;if(!(i.getLoop()||r&&r!==Cr)){var o=i.targetName,a=o?e[o]:e;i.saveTo(a)}}},t.prototype._innerSaveToNormal=function(t){var e=this._normalState;e||(e=this._normalState={}),t.textConfig&&!e.textConfig&&(e.textConfig=this.textConfig),this._savePrimaryToNormal(t,e,Dr)},t.prototype._savePrimaryToNormal=function(t,e,n){for(var i=0;i<n.length;i++){var r=n[i];null==t[r]||r in e||(e[r]=this[r])}},t.prototype.hasState=function(){return this.currentStates.length>0},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;s<o;s++)if(t[s]!==r[s]){a=!1;break}if(a)return;for(s=0;s<o;s++){var l=t[s],u=void 0;this.stateProxy&&(u=this.stateProxy(l,t)),u||(u=this.states[l]),u&&i.push(u)}var h=i[o-1],c=!!(h&&h.hoverLayer||n);c&&this._toggleHoverLayerFlag(!0);var p=this._mergeStates(i),d=this.stateTransition;this.saveCurrentToNormalState(p),this._applyStateObj(t.join(\",\"),p,this._normalState,!1,!e&&!this.__inHover&&d&&d.duration>0,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<this.animators.length;t++){var e=this.animators[t];e.targetName&&e.changeTarget(this[e.targetName])}},t.prototype.removeState=function(t){var e=P(this.currentStates,t);if(e>=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<t.length;i++){var r=t[i];A(n,r),r.textConfig&&A(e=e||{},r.textConfig)}return e&&(n.textConfig=e),n},t.prototype._applyStateObj=function(t,e,n,i,r,o){var a=!(e&&i);e&&e.textConfig?(this.textConfig=A({},i?this.textConfig:n.textConfig),A(this.textConfig,e.textConfig)):a&&n.textConfig&&(this.textConfig=n.textConfig);for(var s={},l=!1,u=0;u<Dr.length;u++){var h=Dr[u],c=r&&Ar[h];e&&null!=e[h]?c?(l=!0,s[h]=e[h]):this[h]=e[h]:a&&null!=n[h]&&(c?(l=!0,s[h]=n[h]):this[h]=n[h])}if(!r)for(u=0;u<this.animators.length;u++){var p=this.animators[u],d=p.targetName;p.getLoop()||p.__changeFinalValue(d?(e||n)[d]:e||n)}l&&this._transitionState(t,s,o)},t.prototype._attachComponent=function(t){if((!t.__zr||t.__hostTarget)&&t!==this){var e=this.__zr;e&&t.addSelfToZr(e),t.__zr=e,t.__hostTarget=this}},t.prototype._detachComponent=function(t){t.__zr&&t.removeSelfFromZr(t.__zr),t.__zr=null,t.__hostTarget=null},t.prototype.getClipPath=function(){return this._clipPath},t.prototype.setClipPath=function(t){this._clipPath&&this._clipPath!==t&&this.removeClipPath(),this._attachComponent(t),this._clipPath=t,this.markRedraw()},t.prototype.removeClipPath=function(){var t=this._clipPath;t&&(this._detachComponent(t),this._clipPath=null,this.markRedraw())},t.prototype.getTextContent=function(){return this._textContent},t.prototype.setTextContent=function(t){var e=this._textContent;e!==t&&(e&&e!==t&&this.removeTextContent(),t.innerTransformable=new gr,this._attachComponent(t),this._textContent=t,this.markRedraw())},t.prototype.setTextConfig=function(t){this.textConfig||(this.textConfig={}),A(this.textConfig,t),this.markRedraw()},t.prototype.removeTextConfig=function(){this.textConfig=null,this.markRedraw()},t.prototype.removeTextContent=function(){var t=this._textContent;t&&(t.innerTransformable=null,this._detachComponent(t),this._textContent=null,this._innerTextDefaultStyle=null,this.markRedraw())},t.prototype.getTextGuideLine=function(){return this._textGuide},t.prototype.setTextGuideLine=function(t){this._textGuide&&this._textGuide!==t&&this.removeTextGuideLine(),this._attachComponent(t),this._textGuide=t,this.markRedraw()},t.prototype.removeTextGuideLine=function(){var t=this._textGuide;t&&(this._detachComponent(t),this._textGuide=null,this.markRedraw())},t.prototype.markRedraw=function(){this.__dirty|=1;var t=this.__zr;t&&(this.__inHover?t.refreshHover():t.refresh()),this.__hostTarget&&this.__hostTarget.markRedraw()},t.prototype.dirty=function(){this.markRedraw()},t.prototype._toggleHoverLayerFlag=function(t){this.__inHover=t;var e=this._textContent,n=this._textGuide;e&&(e.__inHover=t),n&&(n.__inHover=t)},t.prototype.addSelfToZr=function(t){if(this.__zr!==t){this.__zr=t;var e=this.animators;if(e)for(var n=0;n<e.length;n++)t.animation.addAnimator(e[n]);this._clipPath&&this._clipPath.addSelfToZr(t),this._textContent&&this._textContent.addSelfToZr(t),this._textGuide&&this._textGuide.addSelfToZr(t)}},t.prototype.removeSelfFromZr=function(t){if(this.__zr){this.__zr=null;var e=this.animators;if(e)for(var n=0;n<e.length;n++)t.animation.removeAnimator(e[n]);this._clipPath&&this._clipPath.removeSelfFromZr(t),this._textContent&&this._textContent.removeSelfFromZr(t),this._textGuide&&this._textGuide.removeSelfFromZr(t)}},t.prototype.animate=function(t,e,n){var i=t?this[t]:this;var r=new Ei(i,e,n);return t&&(r.targetName=t),this.addAnimator(r,t),r},t.prototype.addAnimator=function(t,e){var n=this.__zr,i=this;t.during((function(){i.updateDuringAnimation(e)})).done((function(){var e=i.animators,n=P(e,t);n>=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;o<i;o++){var a=n[o];t&&t!==a.scope?r.push(a):a.stop(e)}return this.animators=r,this},t.prototype.animateTo=function(t,e,n){Or(this,t,e,n)},t.prototype.animateFrom=function(t,e,n){Or(this,t,e,n,!0)},t.prototype._transitionState=function(t,e,n,i){for(var r=Or(this,e,n,i),o=0;o<r.length;o++)r[o].__fromStateTransition=t},t.prototype.getBoundingRect=function(){return null},t.prototype.getPaintRect=function(){return null},t.initDefaultProps=function(){var e=t.prototype;e.type=\"element\",e.name=\"\",e.ignore=e.silent=e.isGroup=e.draggable=e.dragging=e.ignoreClip=e.__inHover=!1,e.__dirty=1;function n(t,n,i,r){function o(t,e){Object.defineProperty(e,0,{get:function(){return t[i]},set:function(e){t[i]=e}}),Object.defineProperty(e,1,{get:function(){return t[r]},set:function(e){t[r]=e}})}Object.defineProperty(e,t,{get:function(){this[n]||o(this,this[n]=[]);return this[n]},set:function(t){this[i]=t[0],this[r]=t[1],this[n]=t,o(this,t)}})}Object.defineProperty&&(n(\"position\",\"_legacyPos\",\"x\",\"y\"),n(\"scale\",\"_legacyScale\",\"scaleX\",\"scaleY\"),n(\"origin\",\"_legacyOrigin\",\"originX\",\"originY\"))}(),t}();function Or(t,e,n,i,r){var o=[];Er(t,\"\",t,e,n=n||{},i,o,r);var a=o.length,s=!1,l=n.done,u=n.aborted,h=function(){s=!0,--a<=0&&(s?l&&l():u&&u())},c=function(){--a<=0&&(s?l&&l():u&&u())};a||l&&l(),o.length>0&&n.during&&o[0].during((function(t,e){n.during(e)}));for(var p=0;p<o.length;p++){var d=o[p];h&&d.done(h),c&&d.aborted(c),n.force&&d.duration(n.duration),d.start(n.easing)}return o}function Rr(t,e,n){for(var i=0;i<n;i++)t[i]=e[i]}function Nr(t,e,n){if(N(e[n]))if(N(t[n])||(t[n]=[]),$(e[n])){var i=e[n].length;t[n].length!==i&&(t[n]=new e[n].constructor(i),Rr(t[n],e[n],i))}else{var r=e[n],o=t[n],a=r.length;if(N(r[0]))for(var s=r[0].length,l=0;l<a;l++)o[l]?Rr(o[l],r[l],s):o[l]=Array.prototype.slice.call(r[l]);else Rr(o,r,a);o.length=r.length}else t[n]=e[n]}function Er(t,e,n,i,r,o,a,s){for(var l=G(i),u=r.duration,h=r.delay,c=r.additive,p=r.setToFinal,d=!q(o),f=t.animators,g=[],y=0;y<l.length;y++){var v=l[y],m=i[v];if(null!=m&&null!=n[v]&&(d||o[v]))if(!q(m)||N(m)||Q(m))g.push(v);else{if(e){s||(n[v]=m,t.updateDuringAnimation(e));continue}Er(t,v,n[v],m,r,o&&o[v],a,s)}else s||(n[v]=m,t.updateDuringAnimation(e),g.push(v))}var x=g.length;if(!c&&x)for(var _=0;_<f.length;_++){if((w=f[_]).targetName===e)if(w.stopTracks(g)){var b=P(f,w);f.splice(b,1)}}if(r.force||(g=B(g,(function(t){return e=i[t],r=n[t],!(e===r||N(e)&&N(r)&&function(t,e){var n=t.length;if(n!==e.length)return!1;for(var i=0;i<n;i++)if(t[i]!==e[i])return!1;return!0}(e,r));var e,r})),x=g.length),x>0||r.force&&!a.length){var w,S=void 0,M=void 0,I=void 0;if(s){M={},p&&(S={});for(_=0;_<x;_++){M[v=g[_]]=n[v],p?S[v]=i[v]:n[v]=i[v]}}else if(p){I={};for(_=0;_<x;_++){I[v=g[_]]=ki(n[v]),Nr(n,i,v)}}(w=new Ei(n,!1,!1,c?B(f,(function(t){return t.targetName===e})):null)).targetName=e,r.scope&&(w.scope=r.scope),p&&S&&w.whenWithKeys(0,S,g),I&&w.whenWithKeys(0,I,g),w.whenWithKeys(null==u?500:u,s?M:i,g).delay(h||0),t.addAnimator(w,e),a.push(w)}}R(Pr,jt),R(Pr,gr);var zr=function(t){function e(e){var n=t.call(this)||this;return n.isGroup=!0,n._children=[],n.attr(e),n}return n(e,t),e.prototype.childrenRef=function(){return this._children},e.prototype.children=function(){return this._children.slice()},e.prototype.childAt=function(t){return this._children[t]},e.prototype.childOfName=function(t){for(var e=this._children,n=0;n<e.length;n++)if(e[n].name===t)return e[n]},e.prototype.childCount=function(){return this._children.length},e.prototype.add=function(t){return t&&t!==this&&t.parent!==this&&(this._children.push(t),this._doAdd(t)),this},e.prototype.addBefore=function(t,e){if(t&&t!==this&&t.parent!==this&&e&&e.parent===this){var n=this._children,i=n.indexOf(e);i>=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;n<t.length;n++){var i=t[n];e&&i.removeSelfFromZr(e),i.parent=null}return t.length=0,this},e.prototype.eachChild=function(t,e){for(var n=this._children,i=0;i<n.length;i++){var r=n[i];t.call(e,r,i)}return this},e.prototype.traverse=function(t,e){for(var n=0;n<this._children.length;n++){var i=this._children[n],r=t.call(e,i);i.isGroup&&!r&&i.traverse(t,e)}return this},e.prototype.addSelfToZr=function(e){t.prototype.addSelfToZr.call(this,e);for(var n=0;n<this._children.length;n++){this._children[n].addSelfToZr(e)}},e.prototype.removeSelfFromZr=function(e){t.prototype.removeSelfFromZr.call(this,e);for(var n=0;n<this._children.length;n++){this._children[n].removeSelfFromZr(e)}},e.prototype.getBoundingRect=function(t){for(var e=new ze(0,0,0,0),n=t||this._children,i=[],r=null,o=0;o<n.length;o++){var a=n[o];if(!a.ignore&&!a.invisible){var s=a.getBoundingRect(),l=a.getLocalTransform(i);l?(ze.applyTransform(e,s,l),(r=r||e.clone()).union(e)):(r=r||s.clone()).union(s)}}return r||e},e}(Pr);zr.prototype.type=\"group\";\n/*!\n    * ZRender, a high performance 2d drawing library.\n    *\n    * Copyright (c) 2013, Baidu Inc.\n    * All rights reserved.\n    *\n    * LICENSE\n    * https://github.com/ecomfe/zrender/blob/master/LICENSE.txt\n    */\nvar Vr={},Br={};var Fr=function(){function t(t,e,n){var i=this;this._sleepAfterStill=10,this._stillFrameAccum=0,this._needsRefresh=!0,this._needsRefreshHover=!0,this._darkMode=!1,n=n||{},this.dom=e,this.id=t;var o=new rn,a=n.renderer||\"canvas\";Vr[a]||(a=G(Vr)[0]),n.useDirtyRect=null!=n.useDirtyRect&&n.useDirtyRect;var s=new Vr[a](e,o,n,t),l=n.ssr||s.ssrOnly;this.storage=o,this.painter=s;var u,h=r.node||r.worker||l?null:new ir(s.getViewportRoot(),s.root),c=n.useCoarsePointer;(null==c||\"auto\"===c?r.touchEventsSupported:!!c)&&(u=rt(n.pointerSize,44)),this.handler=new Ye(o,s,h,s.root,u),this.animation=new Fi({stage:{update:l?null:function(){return i._flush(!0)}}}),l||this.animation.start()}return t.prototype.add=function(t){t&&(this.storage.addRoot(t),t.addSelfToZr(this),this.refresh())},t.prototype.remove=function(t){t&&(this.storage.delRoot(t),t.removeSelfFromZr(this),this.refresh())},t.prototype.configLayer=function(t,e){this.painter.configLayer&&this.painter.configLayer(t,e),this.refresh()},t.prototype.setBackgroundColor=function(t){this.painter.setBackgroundColor&&this.painter.setBackgroundColor(t),this.refresh(),this._backgroundColor=t,this._darkMode=function(t){if(!t)return!1;if(\"string\"==typeof t)return oi(t,1)<.4;if(t.colorStops){for(var e=t.colorStops,n=0,i=e.length,r=0;r<i;r++)n+=oi(e[r].color,1);return(n/=i)<.4}return!1}(t)},t.prototype.getBackgroundColor=function(){return this._backgroundColor},t.prototype.setDarkMode=function(t){this._darkMode=t},t.prototype.isDarkMode=function(){return this._darkMode},t.prototype.refreshImmediately=function(t){t||this.animation.update(!0),this._needsRefresh=!1,this.painter.refresh(),this._needsRefresh=!1},t.prototype.refresh=function(){this._needsRefresh=!0,this.animation.start()},t.prototype.flush=function(){this._flush(!1)},t.prototype._flush=function(t){var e,n=zi();this._needsRefresh&&(e=!0,this.refreshImmediately(t)),this._needsRefreshHover&&(e=!0,this.refreshHoverImmediately());var i=zi();e?(this._stillFrameAccum=0,this.trigger(\"rendered\",{elapsedTime:i-n})):this._sleepAfterStill>0&&(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;e<t.length;e++)t[e]instanceof zr&&t[e].removeSelfFromZr(this);this.storage.delAllRoots(),this.painter.clear()},t.prototype.dispose=function(){var t;this.animation.stop(),this.clear(),this.storage.dispose(),this.painter.dispose(),this.handler.dispose(),this.animation=this.storage=this.painter=this.handler=null,t=this.id,delete Br[t]},t}();function Gr(t,e){var n=new Fr(M(),t,e);return Br[n.id]=n,n}function Wr(t,e){Vr[t]=e}var Hr=Object.freeze({__proto__:null,init:Gr,dispose:function(t){t.dispose()},disposeAll:function(){for(var t in Br)Br.hasOwnProperty(t)&&Br[t].dispose();Br={}},getInstance:function(t){return Br[t]},registerPainter:Wr,version:\"5.4.4\"}),Yr=1e-4;function Xr(t,e,n,i){var r=e[0],o=e[1],a=n[0],s=n[1],l=o-r,u=s-a;if(0===l)return 0===u?a:(a+s)/2;if(i)if(l>0){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]}));s<o;){for(var u=Number.NEGATIVE_INFINITY,h=null,c=0,p=l.length;c<p;++c)l[c]>u&&(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<Yr}var io=/^(?:(\\d{4})(?:[-\\/](\\d{1,2})(?:[-\\/](\\d{1,2})(?:[T ](\\d{1,2})(?::(\\d{1,2})(?::(\\d{1,2})(?:[.,](\\d+))?)?)?(Z|[\\+\\-]\\d\\d:?\\d\\d)?)?)?)?)?$/;function ro(t){if(t instanceof Date)return t;if(U(t)){var e=io.exec(t);if(!e)return new Date(NaN);if(e[8]){var n=+e[4]||0;return\"Z\"!==e[8].toUpperCase()&&(n-=+e[8].slice(0,3)),new Date(Date.UTC(+e[1],+(e[2]||1)-1,+e[3]||1,n,+(e[5]||0),+e[6]||0,e[7]?+e[7].substring(0,3):0))}return new Date(+e[1],+(e[2]||1)-1,+e[3]||1,+e[4]||0,+(e[5]||0),+e[6]||0,e[7]?+e[7].substring(0,3):0)}return null==t?new Date(NaN):new Date(Math.round(t))}function oo(t){return Math.pow(10,ao(t))}function ao(t){if(0===t)return 0;var e=Math.floor(Math.log(t)/Math.LN10);return t/Math.pow(10,e)>=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<t.length;){for(var r=t[i].interval,o=t[i].close,a=0;a<2;a++)r[a]<=e&&(r[a]=e,o[a]=a?1:1-n),e=r[a],n=o[a];r[0]===r[1]&&o[0]*o[1]!=1?t.splice(i,1):i++}return t;function s(t,e,n){return t.interval[n]<e.interval[n]||t.interval[n]===e.interval[n]&&(t.close[n]-e.close[n]==(n?-1:1)||!n&&s(t,e,1))}}function ho(t){var e=parseFloat(t);return e==t&&(0!==e||!U(t)||t.indexOf(\"x\")<=0)?e:NaN}function co(t){return!isNaN(ho(t))}function po(){return Math.round(9*Math.random())}function fo(t,e){return 0===e?t:fo(e,t%e)}function go(t,e){return null==t?e:null==e?t:t*e/fo(t,e)}\"undefined\"!=typeof console&&console.warn&&console.log;function yo(t){0}function vo(t){throw new Error(t)}function mo(t,e,n){return(e-t)*n+t}var xo=\"series\\0\",_o=\"\\0_ec_\\0\";function bo(t){return t instanceof Array?t:null==t?[]:[t]}function wo(t,e,n){if(t){t[e]=t[e]||{},t.emphasis=t.emphasis||{},t.emphasis[e]=t.emphasis[e]||{};for(var i=0,r=n.length;i<r;i++){var o=n[i];!t.emphasis[e].hasOwnProperty(o)&&t[e].hasOwnProperty(o)&&(t.emphasis[e][o]=t[e][o])}}}var So=[\"fontStyle\",\"fontWeight\",\"fontSize\",\"fontFamily\",\"rich\",\"tag\",\"color\",\"textBorderColor\",\"textBorderWidth\",\"width\",\"height\",\"lineHeight\",\"align\",\"verticalAlign\",\"baseline\",\"shadowColor\",\"shadowBlur\",\"shadowOffsetX\",\"shadowOffsetY\",\"textShadowColor\",\"textShadowBlur\",\"textShadowOffsetX\",\"textShadowOffsetY\",\"backgroundColor\",\"borderColor\",\"borderWidth\",\"borderRadius\",\"padding\"];function Mo(t){return!q(t)||Y(t)||t instanceof Date?t:t.value}function Io(t){return q(t)&&!(t instanceof Array)}function To(t,e,n){var i=\"normalMerge\"===n,r=\"replaceMerge\"===n,o=\"replaceAll\"===n;t=t||[],e=(e||[]).slice();var a=yt();E(e,(function(t,n){q(t)||(e[n]=null)}));var s,l,u=function(t,e,n){var i=[];if(\"replaceAll\"===n)return i;for(var r=0;r<t.length;r++){var o=t[r];o&&null!=o.id&&e.set(o.id,r),i.push({existing:\"replaceMerge\"===n||Lo(o)?null:o,newOption:null,keyInfo:null,brandNew:null})}return i}(t,a,n);return(i||r)&&function(t,e,n,i){E(i,(function(r,o){if(r&&null!=r.id){var a=Do(r.id),s=n.get(a);if(null!=s){var l=t[s];lt(!l.newOption,'Duplicated option on id \"'+a+'\".'),l.newOption=r,l.existing=e[s],i[o]=null}}}))}(u,t,a,e),i&&function(t,e){E(e,(function(n,i){if(n&&null!=n.name)for(var r=0;r<t.length;r++){var o=t[r].existing;if(!t[r].newOption&&o&&(null==o.id||null==n.id)&&!Lo(n)&&!Lo(o)&&Co(\"name\",o,n))return t[r].newOption=n,void(e[i]=null)}}))}(u,e),i||r?function(t,e,n){E(e,(function(e){if(e){for(var i,r=0;(i=t[r])&&(i.newOption||Lo(i.existing)||i.existing&&null!=e.id&&!Co(\"id\",e,i.existing));)r++;i?(i.newOption=e,i.brandNew=n):t.push({newOption:e,brandNew:n,existing:null,keyInfo:null}),r++}}))}(u,e,r):o&&function(t,e){E(e,(function(e){t.push({newOption:e,brandNew:!0,existing:null,keyInfo:null})}))}(u,e),s=u,l=yt(),E(s,(function(t){var e=t.existing;e&&l.set(e.id,t)})),E(s,(function(t){var e=t.newOption;lt(!e||null==e.id||!l.get(e.id)||l.get(e.id)===t,\"id duplicates: \"+(e&&e.id)),e&&null!=e.id&&l.set(e.id,t),!t.keyInfo&&(t.keyInfo={})})),E(s,(function(t,e){var n=t.existing,i=t.newOption,r=t.keyInfo;if(q(i)){if(r.name=null!=i.name?Do(i.name):n?n.name:xo+e,n)r.id=Do(n.id);else if(null!=i.id)r.id=Do(i.id);else{var o=0;do{r.id=\"\\0\"+r.name+\"\\0\"+o++}while(l.get(r.id))}l.set(r.id,t)}})),u}function Co(t,e,n){var i=Ao(e[t],null),r=Ao(n[t],null);return null!=i&&null!=r&&i===r}function Do(t){return Ao(t,\"\")}function Ao(t,e){return null==t?e:U(t)?t:j(t)||Z(t)?t+\"\":e}function ko(t){var e=t.name;return!(!e||!e.indexOf(xo))}function Lo(t){return t&&null!=t.id&&0===Do(t.id).indexOf(_o)}function Po(t,e){return null!=e.dataIndexInside?e.dataIndexInside:null!=e.dataIndex?Y(e.dataIndex)?z(e.dataIndex,(function(e){return t.indexOfRawIndex(e)})):t.indexOfRawIndex(e.dataIndex):null!=e.name?Y(e.name)?z(e.name,(function(e){return t.indexOfName(e)})):t.indexOfName(e.name):void 0}function Oo(){var t=\"__ec_inner_\"+Ro++;return function(e){return e[t]||(e[t]={})}}var Ro=po();function No(t,e,n){var i=Eo(e,n),r=i.mainTypeSpecified,o=i.queryOptionMap,a=i.others,s=n?n.defaultMainType:null;return!r&&s&&o.set(s,{}),o.each((function(e,i){var r=Bo(t,i,e,{useDefault:s===i,enableAll:!n||null==n.enableAll||n.enableAll,enableNone:!n||null==n.enableNone||n.enableNone});a[i+\"Models\"]=r.models,a[i+\"Model\"]=r.models[0]})),a}function Eo(t,e){var n;if(U(t)){var i={};i[t+\"Index\"]=0,n=i}else n=t;var r=yt(),o={},a=!1;return E(n,(function(t,n){if(\"dataIndex\"!==n&&\"dataIndexInside\"!==n){var i=n.match(/^(\\w+)(Index|Id|Name)$/)||[],s=i[1],l=(i[2]||\"\").toLowerCase();if(s&&l&&!(e&&e.includeMainTypes&&P(e.includeMainTypes,s)<0))a=a||!!s,(r.get(s)||r.set(s,{}))[l]=t}else o[n]=t})),{mainTypeSpecified:a,queryOptionMap:r,others:o}}var zo={useDefault:!0,enableAll:!1,enableNone:!1},Vo={useDefault:!1,enableAll:!0,enableNone:!0};function Bo(t,e,n,i){i=i||zo;var r=n.index,o=n.id,a=n.name,s={models:null,specified:null!=r||null!=o||null!=a};if(!s.specified){var l=void 0;return s.models=i.useDefault&&(l=t.getComponent(e))?[l]:[],s}return\"none\"===r||!1===r?(lt(i.enableNone,'`\"none\"` or `false` is not a valid value on index option.'),s.models=[],s):(\"all\"===r&&(lt(i.enableAll,'`\"all\"` is not a valid value on index option.'),r=o=a=null),s.models=t.queryComponents({mainType:e,index:r,id:o,name:a}),s)}function Fo(t,e,n){t.setAttribute?t.setAttribute(e,n):t[e]=n}function Go(t,e){var n=yt(),i=[];return E(t,(function(t){var r=e(t);(n.get(r)||(i.push(r),n.set(r,[]))).push(t)})),{keys:i,buckets:n}}function Wo(t,e,n,i,r){var o=null==e||\"auto\"===e;if(null==i)return i;if(j(i))return Zr(f=mo(n||0,i,r),o?Math.max(qr(n||0),qr(i)):e);if(U(i))return r<1?n:i;for(var a=[],s=n,l=i,u=Math.max(s?s.length:0,l.length),h=0;h<u;++h){var c=t.getDimensionInfo(h);if(c&&\"ordinal\"===c.type)a[h]=(r<1&&s?s:l)[h];else{var p=s&&s[h]?s[h]:0,d=l[h],f=mo(p,d,r);a[h]=Zr(f,o?Math.max(qr(p),qr(d)):e)}}return a}var Ho=\"___EC__COMPONENT__CONTAINER___\",Yo=\"___EC__EXTENDED_CLASS___\";function Xo(t){var e={main:\"\",sub:\"\"};if(t){var n=t.split(\".\");e.main=n[0]||\"\",e.sub=n[1]||\"\"}return e}function Uo(t,e){t.$constructor=t,t.extend=function(t){var e,i,r=this;return X(i=r)&&/^class\\s/.test(Function.prototype.toString.call(i))?e=function(t){function e(){return t.apply(this,arguments)||this}return n(e,t),e}(r):(e=function(){(t.$constructor||r).apply(this,arguments)},O(e,this)),A(e.prototype,t),e[Yo]=!0,e.extend=this.extend,e.superCall=qo,e.superApply=Ko,e.superClass=r,e}}function Zo(t,e){t.extend=e.extend}var jo=Math.round(10*Math.random());function qo(t,e){for(var n=[],i=2;i<arguments.length;i++)n[i-2]=arguments[i];return this.superClass.prototype[e].apply(t,n)}function Ko(t,e,n){return this.superClass.prototype[e].apply(t,n)}function $o(t){var e={};t.registerClass=function(t){var n,i=t.type||t.prototype.type;if(i){lt(/^[a-zA-Z0-9_]+([.][a-zA-Z0-9_]+)?$/.test(n=i),'componentType \"'+n+'\" illegal'),t.prototype.type=i;var r=Xo(i);if(r.sub){if(r.sub!==Ho){var o=function(t){var n=e[t.main];n&&n[Ho]||((n=e[t.main]={})[Ho]=!0);return n}(r);o[r.sub]=t}}else e[r.main]=t}return t},t.getClass=function(t,n,i){var r=e[t];if(r&&r[Ho]&&(r=n?r[n]:null),i&&!r)throw new Error(n?\"Component \"+t+\".\"+(n||\"\")+\" is used but not imported.\":t+\".type should be specified.\");return r},t.getClassesByMainType=function(t){var n=Xo(t),i=[],r=e[n.main];return r&&r[Ho]?E(r,(function(t,e){e!==Ho&&i.push(t)})):i.push(r),i},t.hasClass=function(t){var n=Xo(t);return!!e[n.main]},t.getAllClassMainTypes=function(){var t=[];return E(e,(function(e,n){t.push(n)})),t},t.hasSubTypes=function(t){var n=Xo(t),i=e[n.main];return i&&i[Ho]}}function Jo(t,e){for(var n=0;n<t.length;n++)t[n][1]||(t[n][1]=t[n][0]);return e=e||!1,function(n,i,r){for(var o={},a=0;a<t.length;a++){var s=t[a][1];if(!(i&&P(i,s)>=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<t.pending.length;e++){var n=t.pending[e],i=n.cb;i&&i(this,n.cbPayload),n.hostEl.dirty()}t.pending.length=0}function oa(t){return t&&t.width&&t.height}var aa=/\\{([a-zA-Z0-9_]+)\\|([^}]*)\\}/g;function sa(t,e,n,i,r){if(!e)return\"\";var o=(t+\"\").split(\"\\n\");r=la(e,n,i,r);for(var a=0,s=o.length;a<s;a++)o[a]=ua(o[a],r);return o.join(\"\\n\")}function la(t,e,n,i){var r=A({},i=i||{});r.font=e,n=rt(n,\"...\"),r.maxIterations=rt(i.maxIterations,2);var o=r.minChar=rt(i.minChar,0);r.cnCharWidth=xr(\"国\",e);var a=r.ascCharWidth=xr(\"a\",e);r.placeholder=rt(i.placeholder,\"\");for(var s=t=Math.max(0,t-1),l=0;l<o&&s>=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;o<a&&r<e;o++){var s=t.charCodeAt(o);r+=0<=s&&s<=127?n:i}return o}var ca=function(){},pa=function(t){this.tokens=[],t&&(this.tokens=t)},da=function(){this.width=0,this.height=0,this.contentWidth=0,this.contentHeight=0,this.outerWidth=0,this.outerHeight=0,this.lines=[]};function fa(t,e,n,i,r){var o,a,s=\"\"===e,l=r&&n.rich[r]||{},u=t.lines,h=l.font||n.font,c=!1;if(i){var p=l.padding,d=p?p[1]+p[3]:0;if(null!=l.width&&\"auto\"!==l.width){var f=Ir(l.width,i.width)+d;u.length>0&&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<o.length;y++){var v=o[y],m=new ca;if(m.styleName=r,m.text=v,m.isLineHolder=!v&&!s,\"number\"==typeof l.width?m.width=l.width:m.width=a?a[y]:xr(v,h),y||c)u.push(new pa([m]));else{var x=(u[u.length-1]||(u[0]=new pa)).tokens,_=x.length;1===_&&x[0].isLineHolder?x[0]=m:(v||!_||s)&&x.push(m)}}}var ga=V(\",&?/;] \".split(\"\"),(function(t,e){return t[e]=!0,t}),{});function ya(t){return!function(t){var e=t.charCodeAt(0);return e>=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;c<t.length;c++){var p=t.charAt(c);if(\"\\n\"!==p){var d=xr(p,e),f=!i&&!ya(p);(o.length?h+d>n: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;i<n.length;i++){var r=n[i];\"style\"===r?this.useStyle(e[r]):t.prototype.attrKV.call(this,r,e[r])}this.style||this.useStyle({})},e.prototype.beforeBrush=function(){},e.prototype.afterBrush=function(){},e.prototype.innerBeforeBrush=function(){},e.prototype.innerAfterBrush=function(){},e.prototype.shouldBePainted=function(t,e,n,i){var r=this.transform;if(this.ignore||this.invisible||0===this.style.opacity||this.culling&&function(t,e,n){Ma.copy(t.getBoundingRect()),t.transform&&Ma.applyTransform(t.transform);return Ia.width=e,Ia.height=n,!Ma.intersect(Ia)}(this,t,e)||r&&!r[0]&&!r[3])return!1;if(n&&this.__clipPaths)for(var o=0;o<this.__clipPaths.length;++o)if(this.__clipPaths[o].isZeroArea())return!1;if(i&&this.parent)for(var a=this.parent;a;){if(a.ignore)return!1;a=a.parent}return!0},e.prototype.contain=function(t,e){return this.rectContain(t,e)},e.prototype.traverse=function(t,e){t.call(e,this)},e.prototype.rectContain=function(t,e){var n=this.transformCoordToLocal(t,e);return this.getBoundingRect().contain(n[0],n[1])},e.prototype.getPaintRect=function(){var t=this._paintRect;if(!this._paintRect||this.__dirty){var e=this.transform,n=this.getBoundingRect(),i=this.style,r=i.shadowBlur||0,o=i.shadowOffsetX||0,a=i.shadowOffsetY||0;t=this._paintRect||(this._paintRect=new ze(0,0,0,0)),e?ze.applyTransform(t,n,e):t.copy(n),(r||o||a)&&(t.width+=2*r+Math.abs(o),t.height+=2*r+Math.abs(a),t.x=Math.min(t.x,t.x+o-r),t.y=Math.min(t.y,t.y+a-r));var s=this.dirtyRectTolerance;t.isZero()||(t.x=Math.floor(t.x-s),t.y=Math.floor(t.y-s),t.width=Math.ceil(t.width+1+2*s),t.height=Math.ceil(t.height+1+2*s))}return t},e.prototype.setPrevPaintRect=function(t){t?(this._prevPaintRect=this._prevPaintRect||new ze(0,0,0,0),this._prevPaintRect.copy(t)):this._prevPaintRect=null},e.prototype.getPrevPaintRect=function(){return this._prevPaintRect},e.prototype.animateStyle=function(t){return this.animate(\"style\",t)},e.prototype.updateDuringAnimation=function(t){\"style\"===t?this.dirtyStyle():this.markRedraw()},e.prototype.attrKV=function(e,n){\"style\"!==e?t.prototype.attrKV.call(this,e,n):this.style?this.setStyle(n):this.useStyle(n)},e.prototype.setStyle=function(t,e){return\"string\"==typeof t?this.style[t]=e:A(this.style,t),this.dirtyStyle(),this},e.prototype.dirtyStyle=function(t){t||this.markRedraw(),this.__dirty|=2,this._rect&&(this._rect=null)},e.prototype.dirty=function(){this.dirtyStyle()},e.prototype.styleChanged=function(){return!!(2&this.__dirty)},e.prototype.styleUpdated=function(){this.__dirty&=-3},e.prototype.createStyle=function(t){return mt(xa,t)},e.prototype.useStyle=function(t){t[ma]||(t=this.createStyle(t)),this.__inHover?this.__hoverStyle=t:this.style=t,this.dirtyStyle()},e.prototype.isStyleObject=function(t){return t[ma]},e.prototype._innerSaveToNormal=function(e){t.prototype._innerSaveToNormal.call(this,e);var n=this._normalState;e.style&&!n.style&&(n.style=this._mergeStyle(this.createStyle(),this.style)),this._savePrimaryToNormal(e,n,ba)},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.style?o?r?s=n.style:(s=this._mergeStyle(this.createStyle(),i.style),this._mergeStyle(s,n.style)):(s=this._mergeStyle(this.createStyle(),r?this.style:i.style),this._mergeStyle(s,n.style)):l&&(s=i.style),s)if(o){var u=this.style;if(this.style=this.createStyle(l?{}:u),l)for(var h=G(u),c=0;c<h.length;c++){(d=h[c])in s&&(s[d]=s[d],this.style[d]=u[d])}var p=G(s);for(c=0;c<p.length;c++){var d=p[c];this.style[d]=this.style[d]}this._transitionState(e,{style:s},a,this.getAnimationStyleProps())}else this.useStyle(s);var f=this.__inHover?wa:ba;for(c=0;c<f.length;c++){d=f[c];n&&null!=n[d]?this[d]=n[d]:l&&null!=i[d]&&(this[d]=i[d])}},e.prototype._mergeStates=function(e){for(var n,i=t.prototype._mergeStates.call(this,e),r=0;r<e.length;r++){var o=e[r];o.style&&(n=n||{},this._mergeStyle(n,o.style))}return n&&(i.style=n),i},e.prototype._mergeStyle=function(t,e){return A(t,e),t},e.prototype.getAnimationStyleProps=function(){return _a},e.initDefaultProps=((i=e.prototype).type=\"displayable\",i.invisible=!1,i.z=0,i.z2=0,i.zlevel=0,i.culling=!1,i.cursor=\"pointer\",i.rectHover=!1,i.incremental=!1,i._rect=null,i.dirtyRectTolerance=0,void(i.__dirty=3)),e}(Pr),Ma=new ze(0,0,0,0),Ia=new ze(0,0,0,0);var Ta=Math.min,Ca=Math.max,Da=Math.sin,Aa=Math.cos,ka=2*Math.PI,La=Mt(),Pa=Mt(),Oa=Mt();function Ra(t,e,n){if(0!==t.length){for(var i=t[0],r=i[0],o=i[0],a=i[1],s=i[1],l=1;l<t.length;l++)i=t[l],r=Ta(r,i[0]),o=Ca(o,i[0]),a=Ta(a,i[1]),s=Ca(s,i[1]);e[0]=r,e[1]=a,n[0]=o,n[1]=s}}function Na(t,e,n,i,r,o){r[0]=Ta(t,n),r[1]=Ta(e,i),o[0]=Ca(t,n),o[1]=Ca(e,i)}var Ea=[],za=[];function Va(t,e,n,i,r,o,a,s,l,u){var h=bn,c=mn,p=h(t,n,r,a,Ea);l[0]=1/0,l[1]=1/0,u[0]=-1/0,u[1]=-1/0;for(var d=0;d<p;d++){var f=c(t,n,r,a,Ea[d]);l[0]=Ta(f,l[0]),u[0]=Ca(f,u[0])}p=h(e,i,o,s,za);for(d=0;d<p;d++){var g=c(e,i,o,s,za[d]);l[1]=Ta(g,l[1]),u[1]=Ca(g,u[1])}l[0]=Ta(t,l[0]),u[0]=Ca(t,u[0]),l[0]=Ta(a,l[0]),u[0]=Ca(a,u[0]),l[1]=Ta(e,l[1]),u[1]=Ca(e,u[1]),l[1]=Ta(s,l[1]),u[1]=Ca(s,u[1])}function Ba(t,e,n,i,r,o,a,s){var l=Cn,u=In,h=Ca(Ta(l(t,n,r),1),0),c=Ca(Ta(l(e,i,o),1),0),p=u(t,n,r,h),d=u(e,i,o,c);a[0]=Ta(t,r,p),a[1]=Ta(e,o,d),s[0]=Ca(t,r,p),s[1]=Ca(e,o,d)}function Fa(t,e,n,i,r,o,a,s,l){var u=Ht,h=Yt,c=Math.abs(r-o);if(c%ka<1e-4&&c>1e-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:r<o&&a&&(r+=ka),a){var p=o;o=r,r=p}for(var d=0;d<o;d+=Math.PI/2)d>r&&(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&&n<r&&(r=n-(ts-is(r-n))),t[0]=n,t[1]=r}var os=function(){function t(t){this.dpr=1,this._xi=0,this._yi=0,this._x0=0,this._y0=0,this._len=0,t&&(this._saveData=!1),this._saveData&&(this.data=[])}return t.prototype.increaseVersion=function(){this._version++},t.prototype.getVersion=function(){return this._version},t.prototype.setScale=function(t,e,n){(n=n||0)>0&&(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;n<e;n++)this.data[n]=t[n];this._len=e},t.prototype.appendPath=function(t){t instanceof Array||(t=[t]);for(var e=t.length,n=0,i=this._len,r=0;r<e;r++)n+=t[r].len();es&&this.data instanceof Float32Array&&(this.data=new Float32Array(i+n));for(r=0;r<e;r++)for(var o=t[r].data,a=0;a<o.length;a++)this.data[i++]=o[a];this._len=i},t.prototype.addData=function(t,e,n,i,r,o,a,s,l){if(this._saveData){var u=this.data;this._len+arguments.length>u.length&&(this._expandData(),u=this.data);for(var h=0;h<arguments.length;h++)u[this._len++]=arguments[h]}},t.prototype._drawPendingPt=function(){this._pendingPtDist>0&&(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;e<this._len;e++)t[e]=this.data[e];this.data=t}},t.prototype.toStatic=function(){if(this._saveData){this._drawPendingPt();var t=this.data;t instanceof Array&&(t.length=this._len,es&&this._len>11&&(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;t<this._len;){var a=e[t++],s=1===t;switch(s&&(r=n=e[t],o=i=e[t+1]),a){case Ga.M:n=r=e[t++],i=o=e[t++],Ua[0]=r,Ua[1]=o,Za[0]=r,Za[1]=o;break;case Ga.L:Na(n,i,e[t],e[t+1],Ua,Za),n=e[t++],i=e[t++];break;case Ga.C:Va(n,i,e[t++],e[t++],e[t++],e[t++],e[t],e[t+1],Ua,Za),n=e[t++],i=e[t++];break;case Ga.Q:Ba(n,i,e[t++],e[t++],e[t],e[t+1],Ua,Za),n=e[t++],i=e[t++];break;case Ga.A:var l=e[t++],u=e[t++],h=e[t++],c=e[t++],p=e[t++],d=e[t++]+p;t+=1;var f=!e[t++];s&&(r=Ka(p)*h+l,o=$a(p)*c+u),Fa(l,u,h,c,p,d,f,Ua,Za),n=Ka(d)*h+l,i=$a(d)*c+u;break;case Ga.R:Na(r=n=e[t++],o=i=e[t++],r+e[t++],o+e[t++],Ua,Za);break;case Ga.Z:n=r,i=o}Ht(Ya,Ya,Ua),Yt(Xa,Xa,Za)}return 0===t&&(Ya[0]=Ya[1]=Xa[0]=Xa[1]=0),new ze(Ya[0],Ya[1],Xa[0]-Ya[0],Xa[1]-Ya[1])},t.prototype._calculateLength=function(){var t=this.data,e=this._len,n=this._ux,i=this._uy,r=0,o=0,a=0,s=0;this._pathSegLen||(this._pathSegLen=[]);for(var l=this._pathSegLen,u=0,h=0,c=0;c<e;){var p=t[c++],d=1===c;d&&(a=r=t[c],s=o=t[c+1]);var f=-1;switch(p){case Ga.M:r=a=t[c++],o=s=t[c++];break;case Ga.L:var g=t[c++],y=(x=t[c++])-o;(Ja(A=g-r)>n||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;_<g;){var b=p[_++],w=1===_;switch(w&&(n=r=p[_],i=o=p[_+1]),b!==Ga.L&&x>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||a<e-s&&a<i-s||o>t+s&&o>n+s||o<t-s&&o<n-s)return!1;if(t===n)return Math.abs(o-t)<=s/2;var u=(l=(e-i)/(t-n))*o-a+(t*i-n*e)/(t-n);return u*u/(l*l+1)<=s/2*s/2}function ss(t,e,n,i,r,o,a,s,l,u,h){if(0===l)return!1;var c=l;return!(h>e+c&&h>i+c&&h>o+c&&h>s+c||h<e-c&&h<i-c&&h<o-c&&h<s-c||u>t+c&&u>n+c&&u>r+c&&u>a+c||u<t-c&&u<n-c&&u<r-c&&u<a-c)&&Sn(t,e,n,i,r,o,a,s,u,h,null)<=c/2}function ls(t,e,n,i,r,o,a,s,l){if(0===a)return!1;var u=a;return!(l>e+u&&l>i+u&&l>o+u||l<e-u&&l<i-u&&l<o-u||s>t+u&&s>n+u&&s>r+u||s<t-u&&s<n-u&&s<r-u)&&An(t,e,n,i,r,o,s,l,null)<=u/2}var us=2*Math.PI;function hs(t){return(t%=us)<0&&(t+=us),t}var cs=2*Math.PI;function ps(t,e,n,i,r,o,a,s,l){if(0===a)return!1;var u=a;s-=t,l-=e;var h=Math.sqrt(s*s+l*l);if(h-u>n||h+u<n)return!1;if(Math.abs(i-r)%cs<1e-4)return!0;if(o){var c=i;i=hs(r),r=hs(c)}else i=hs(i),r=hs(r);i>r&&(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||o<e&&o<i)return 0;if(i===e)return 0;var a=(o-e)/(i-e),s=i<e?1:-1;1!==a&&0!==a||(s=i<e?.5:-.5);var l=a*(n-t)+t;return l===r?1/0:l>r?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||u<e&&u<i&&u<o&&u<s)return 0;var h,c=_n(e,i,o,s,u,ys);if(0===c)return 0;for(var p=0,d=-1,f=void 0,g=void 0,y=0;y<c;y++){var v=ys[y],m=0===v||1===v?.5:1;mn(t,n,r,a,v)<l||(d<0&&(d=bn(e,i,o,s,vs),vs[1]<vs[0]&&d>1&&(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?v<vs[0]?p+=f<e?m:-m:v<vs[1]?p+=g<f?m:-m:p+=s<g?m:-m:v<vs[0]?p+=f<e?m:-m:p+=s<f?m:-m)}return p}function xs(t,e,n,i,r,o,a,s){if(s>e&&s>i&&s>o||s<e&&s<i&&s<o)return 0;var l=function(t,e,n,i,r){var o=t-2*e+n,a=2*(e-t),s=t-i,l=0;if(yn(o))vn(a)&&(h=-s/a)>=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;p<l;p++){var d=0===ys[p]||1===ys[p]?.5:1;In(t,n,r,ys[p])<a||(ys[p]<u?h+=c<e?d:-d:h+=o<c?d:-d)}return h}d=0===ys[0]||1===ys[0]?.5:1;return In(t,n,r,ys[0])<a?0:o<e?d:-d}function _s(t,e,n,i,r,o,a,s){if((s-=e)>n||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;y<h;){var v=u[y++],m=1===y;switch(v===fs.M&&y>1&&(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<Ms.length;++s)r[Ms[s]]=this[Ms[s]];r.__dirty|=1}else this._decalEl&&(this._decalEl=null)},e.prototype.getDecalElement=function(){return this._decalEl},e.prototype._init=function(e){var n=G(e);this.shape=this.getDefaultShape();var i=this.getDefaultStyle();i&&this.useStyle(i);for(var r=0;r<n.length;r++){var o=n[r],a=e[o];\"style\"===o?this.style?A(this.style,a):this.useStyle(a):\"shape\"===o?A(this.shape,a):t.prototype.attrKV.call(this,o,a)}this.style||this.useStyle({})},e.prototype.getDefaultStyle=function(){return null},e.prototype.getDefaultShape=function(){return{}},e.prototype.canBeInsideText=function(){return this.hasFill()},e.prototype.getInsideTextFill=function(){var t=this.style.fill;if(\"none\"!==t){if(U(t)){var e=oi(t,0);return e>.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;c<h.length;c++){var p=h[c];\"object\"==typeof s[p]?this.shape[p]=s[p]:u[p]=s[p]}this._transitionState(e,{shape:u},a)}else this.shape=s,this.dirtyShape()},e.prototype._mergeStates=function(e){for(var n,i=t.prototype._mergeStates.call(this,e),r=0;r<e.length;r++){var o=e[r];o.shape&&(n=n||{},this._mergeStyle(n,o.shape))}return n&&(i.shape=n),i},e.prototype.getAnimationStyleProps=function(){return Ss},e.prototype.isZeroArea=function(){return!1},e.extend=function(t){var i=function(e){function i(n){var i=e.call(this,n)||this;return t.init&&t.init.call(i,n),i}return n(i,e),i.prototype.getDefaultStyle=function(){return T(t.style)},i.prototype.getDefaultShape=function(){return T(t.shape)},i}(e);for(var r in t)\"function\"==typeof t[r]&&(i.prototype[r]=t[r]);return i},e.initDefaultProps=((i=e.prototype).type=\"path\",i.strokeContainThreshold=5,i.segmentIgnoreThreshold=0,i.subPixelOptimize=!1,i.autoBatch=!1,void(i.__dirty=7)),e}(Sa),Ts=k({strokeFirst:!0,font:a,x:0,y:0,textAlign:\"left\",textBaseline:\"top\",miterLimit:2},ws),Cs=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),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.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;e<this._children.length;e++){var n=this._children[e];n.zlevel=this.zlevel,n.z=this.z,n.z2=this.z2,n.culling=this.culling,n.cursor=this.cursor,n.invisible=this.invisible}},e.prototype.updateTransform=function(){var e=this.innerTransformable;e?(e.updateTransform(),e.transform&&(this.transform=e.transform)):t.prototype.updateTransform.call(this)},e.prototype.getLocalTransform=function(e){var n=this.innerTransformable;return n?n.getLocalTransform(e):t.prototype.getLocalTransform.call(this,e)},e.prototype.getComputedTransform=function(){return this.__hostTarget&&(this.__hostTarget.getComputedTransform(),this.__hostTarget.updateInnerText(!0)),t.prototype.getComputedTransform.call(this)},e.prototype._updateSubTexts=function(){var t;this._childCursor=0,Zs(t=this.style),E(t.rich,Zs),this.style.rich?this._updateRichTexts():this._updatePlainTexts(),this._children.length=this._childCursor,this.styleUpdated()},e.prototype.addSelfToZr=function(e){t.prototype.addSelfToZr.call(this,e);for(var n=0;n<this._children.length;n++)this._children[n].__zr=e},e.prototype.removeSelfFromZr=function(e){t.prototype.removeSelfFromZr.call(this,e);for(var n=0;n<this._children.length;n++)this._children[n].__zr=null},e.prototype.getBoundingRect=function(){if(this.styleChanged()&&this._updateSubTexts(),!this._rect){for(var t=new ze(0,0,0,0),e=this._children,n=[],i=null,r=0;r<e.length;r++){var o=e[r],a=o.getBoundingRect(),s=o.getLocalTransform(n);s?(t.copy(a),t.applyTransform(s),(i=i||t.clone()).union(t)):(i=i||a.clone()).union(a)}this._rect=i||t}return this._rect},e.prototype.setDefaultTextStyle=function(t){this._defaultStyle=t||Vs},e.prototype.setTextContent=function(t){0},e.prototype._mergeStyle=function(t,e){if(!e)return t;var n=e.rich,i=t.rich||n&&{};return A(t,e),n&&i?(this._mergeRich(i,n),t.rich=i):i&&(t.rich=i),t},e.prototype._mergeRich=function(t,e){for(var n=G(e),i=0;i<n.length;i++){var r=n[i];t[r]=t[r]||{},A(t[r],e[r])}},e.prototype.getAnimationStyleProps=function(){return Bs},e.prototype._getOrCreateChild=function(t){var e=this._children[this._childCursor];return e&&e instanceof t||(e=new t),this._children[this._childCursor++]=e,e.__zr=this.__zr,e.parent=this,e},e.prototype._updatePlainTexts=function(){var t=this.style,e=t.font||a,n=t.padding,i=function(t,e){null!=t&&(t+=\"\");var n,i=e.overflow,r=e.padding,o=e.font,a=\"truncate\"===i,s=Mr(o),l=rt(e.lineHeight,s),u=!!e.backgroundColor,h=\"truncate\"===e.lineOverflow,c=e.width,p=(n=null==c||\"break\"!==i&&\"breakAll\"!==i?t?t.split(\"\\n\"):[]:t?va(t,e.font,c,\"breakAll\"===i,0).lines:[]).length*l,d=rt(e.height,p);if(p>d&&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;y<n.length;y++)n[y]=ua(n[y],g);var v=d,m=0;for(y=0;y<n.length;y++)m=Math.max(xr(n[y],o),m);null==c&&(c=m);var x=m;return r&&(v+=r[0]+r[2],x+=r[1]+r[3],c+=r[1]+r[3]),u&&(x=c),{lines:n,height:d,outerWidth:x,outerHeight:v,lineHeight:l,calculatedLineHeight:s,contentWidth:m,contentHeight:p,width:c}}($s(t),t),r=Js(t),o=!!t.backgroundColor,s=i.outerHeight,l=i.outerWidth,u=i.contentWidth,h=i.lines,c=i.lineHeight,p=this._defaultStyle,d=t.x||0,f=t.y||0,g=t.align||p.align||\"left\",y=t.verticalAlign||p.verticalAlign||\"top\",v=d,m=Sr(f,i.contentHeight,y);if(r||n){var x=wr(d,l,g),_=Sr(f,s,y);r&&this._renderBackground(t,t,x,_,l,s)}m+=c/2,n&&(v=Ks(d,g,n),\"top\"===y?m+=n[0]:\"bottom\"===y&&(m-=n[2]));for(var b=0,w=!1,S=(qs(\"fill\"in t?t.fill:(w=!0,p.fill))),M=(js(\"stroke\"in t?t.stroke:o||p.autoStroke&&!w?null:(b=2,p.stroke))),I=t.textShadowBlur>0,T=null!=t.width&&(\"truncate\"===t.overflow||\"break\"===t.overflow||\"breakAll\"===t.overflow),C=i.calculatedLineHeight,D=0;D<h.length;D++){var A=this._getOrCreateChild(Cs),k=A.createStyle();A.useStyle(k),k.text=h[D],k.x=v,k.y=m,g&&(k.textAlign=g),k.textBaseline=\"middle\",k.opacity=t.opacity,k.strokeFirst=!0,I&&(k.shadowBlur=t.textShadowBlur||0,k.shadowColor=t.textShadowColor||\"transparent\",k.shadowOffsetX=t.textShadowOffsetX||0,k.shadowOffsetY=t.textShadowOffsetY||0),k.stroke=M,k.fill=S,M&&(k.lineWidth=t.lineWidth||b,k.lineDash=t.lineDash,k.lineDashOffset=t.lineDashOffset||0),k.font=e,Xs(k,t),m+=c,T&&A.setBoundingRect(new ze(wr(k.x,t.width,k.textAlign),Sr(k.y,C,k.textBaseline),u,C))}},e.prototype._updateRichTexts=function(){var t=this.style,e=function(t,e){var n=new da;if(null!=t&&(t+=\"\"),!t)return n;for(var i,r=e.width,o=e.height,a=e.overflow,s=\"break\"!==a&&\"breakAll\"!==a||null==r?null:{width:r,accumWidth:0,breakAll:\"breakAll\"===a},l=aa.lastIndex=0;null!=(i=aa.exec(t));){var u=i.index;u>l&&fa(n,t.substring(l,u),e,s),fa(n,i[2],e,s,i[1]),l=aa.lastIndex}l<t.length&&fa(n,t.substring(l,t.length),e,s);var h=[],c=0,p=0,d=e.padding,f=\"truncate\"===a,g=\"truncate\"===e.lineOverflow;function y(t,e,n){t.width=e,t.lineHeight=n,c+=n,p=Math.max(p,e)}t:for(var v=0;v<n.lines.length;v++){for(var m=n.lines[v],x=0,_=0,b=0;b<m.tokens.length;b++){var w=(P=m.tokens[b]).styleName&&e.rich[P.styleName]||{},S=P.textPadding=w.padding,M=S?S[1]+S[3]:0,I=P.font=w.font||e.font;P.contentHeight=Mr(I);var T=rt(w.height,P.contentHeight);if(P.innerHeight=T,S&&(T+=S[0]+S[2]),P.height=T,P.lineHeight=ot(w.lineHeight,e.lineHeight,T),P.align=w&&w.align||e.align,P.verticalAlign=w&&w.verticalAlign||\"middle\",g&&null!=o&&c+P.lineHeight>o){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<P.width?!D||L<M?(P.text=\"\",P.width=P.contentWidth=0):(P.text=sa(P.text,L-M,I,e.ellipsis,{minChar:e.truncateMinChar}),P.width=P.contentWidth=xr(P.text,I)):P.contentWidth=xr(P.text,I)}P.width+=M,_+=P.width,w&&(x=Math.max(x,P.lineHeight))}y(m,_,x)}for(n.outerWidth=n.width=rt(r,p),n.outerHeight=n.height=rt(o,c),n.contentHeight=c,n.contentWidth=p,d&&(n.outerWidth+=d[1]+d[3],n.outerHeight+=d[0]+d[2]),v=0;v<h.length;v++){var P,O=(P=h[v]).percentWidth;P.width=parseInt(O,10)/100*n.width}return n}($s(t),t),n=e.width,i=e.outerWidth,r=e.outerHeight,o=t.padding,a=t.x||0,s=t.y||0,l=this._defaultStyle,u=t.align||l.align,h=t.verticalAlign||l.verticalAlign,c=wr(a,i,u),p=Sr(s,r,h),d=c,f=p;o&&(d+=o[3],f+=o[0]);var g=d+n;Js(t)&&this._renderBackground(t,t,c,p,i,r);for(var y=!!t.backgroundColor,v=0;v<e.lines.length;v++){for(var m=e.lines[v],x=m.tokens,_=x.length,b=m.lineHeight,w=m.width,S=0,M=d,I=g,T=_-1,C=void 0;S<_&&(!(C=x[S]).align||\"left\"===C.align);)this._placeToken(C,t,b,f,M,\"left\",y),w-=C.width,M+=C.width,S++;for(;T>=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<Hs.length;n++){var i=Hs[n],r=e[i];null!=r&&(t[i]=r)}}function Us(t){return null!=t.fontSize||t.fontFamily||t.fontWeight}function Zs(t){if(t){t.font=Fs.makeFont(t);var e=t.align;\"middle\"===e&&(e=\"center\"),t.align=null==e||Gs[e]?e:\"left\";var n=t.verticalAlign;\"center\"===n&&(n=\"middle\"),t.verticalAlign=null==n||Ws[n]?n:\"top\",t.padding&&(t.padding=st(t.padding))}}function js(t,e){return null==t||e<=0||\"transparent\"===t||\"none\"===t?null:t.image||t.colorStops?\"#000\":t}function qs(t){return null==t||\"none\"===t?null:t.image||t.colorStops?\"#000\":t}function Ks(t,e,n){return\"right\"===e?t-n[1]:\"center\"===e?t+n[3]/2-n[1]/2:t+n[3]}function $s(t){var e=t.text;return null!=e&&(e+=\"\"),e}function Js(t){return!!(t.backgroundColor||t.lineHeight||t.borderWidth&&t.borderColor)}var Qs=Oo(),tl=function(t,e,n,i){if(i){var r=Qs(i);r.dataIndex=n,r.dataType=e,r.seriesIndex=t,\"group\"===i.type&&i.traverse((function(i){var r=Qs(i);r.seriesIndex=t,r.dataIndex=n,r.dataType=e}))}},el=1,nl={},il=Oo(),rl=Oo(),ol=[\"emphasis\",\"blur\",\"select\"],al=[\"normal\",\"emphasis\",\"blur\",\"select\"],sl=10,ll=\"highlight\",ul=\"downplay\",hl=\"select\",cl=\"unselect\",pl=\"toggleSelect\";function dl(t){return null!=t&&\"none\"!==t}var fl=new En(100);function gl(t){if(U(t)){var e=fl.get(t);return e||(e=$n(t,-.1),fl.put(t,e)),e}if(Q(t)){var n=A({},t);return n.colorStops=z(t.colorStops,(function(t){return{offset:t.offset,color:$n(t.color,-.1)}})),n}return t}function yl(t,e,n){t.onHoverStateChange&&(t.hoverState||0)!==n&&t.onHoverStateChange(e),t.hoverState=n}function vl(t){yl(t,\"emphasis\",2)}function ml(t){2===t.hoverState&&yl(t,\"normal\",0)}function xl(t){yl(t,\"blur\",1)}function _l(t){1===t.hoverState&&yl(t,\"normal\",0)}function bl(t){t.selected=!0}function wl(t){t.selected=!1}function Sl(t,e,n){e(t,n)}function Ml(t,e,n){Sl(t,e,n),t.isGroup&&t.traverse((function(t){Sl(t,e,n)}))}function Il(t,e){switch(e){case\"emphasis\":t.hoverState=2;break;case\"normal\":t.hoverState=0;break;case\"blur\":t.hoverState=1;break;case\"select\":t.selected=!0}}function Tl(t,e){var n=this.states[t];if(this.style){if(\"emphasis\"===t)return function(t,e,n,i){var r=n&&P(n,\"select\")>=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;a<e.length;a++){var s=e[a],l=r[s];o[s]=null==l?i&&i[s]:l}for(a=0;a<t.animators.length;a++){var u=t.animators[a];u.__fromStateTransition&&u.__fromStateTransition.indexOf(n)<0&&\"style\"===u.targetName&&u.saveTo(o,e)}return o}(t,[\"opacity\"],e,{opacity:1}),a=(n=n||{}).style||{};return null==a.opacity&&(n=A({},n),a=A({opacity:i?r:.1*o.opacity},a),n.style=a),n}(this,t,n);if(\"select\"===t)return function(t,e,n){if(n&&null==n.z2){n=A({},n);var i=t.z2SelectLift;n.z2=t.z2+(null!=i?i:9)}return n}(this,0,n)}return n}function Cl(t){t.stateProxy=Tl;var e=t.getTextContent(),n=t.getTextGuideLine();e&&(e.stateProxy=Tl),n&&(n.stateProxy=Tl)}function Dl(t,e){!El(t,e)&&!t.__highByOuter&&Ml(t,vl)}function Al(t,e){!El(t,e)&&!t.__highByOuter&&Ml(t,ml)}function kl(t,e){t.__highByOuter|=1<<(e||0),Ml(t,vl)}function Ll(t,e){!(t.__highByOuter&=~(1<<(e||0)))&&Ml(t,ml)}function Pl(t){Ml(t,xl)}function Ol(t){Ml(t,_l)}function Rl(t){Ml(t,bl)}function Nl(t){Ml(t,wl)}function El(t,e){return t.__highDownSilentOnTouch&&e.zrByTouch}function zl(t){var e=t.getModel(),n=[],i=[];e.eachComponent((function(e,r){var o=rl(r),a=\"series\"===e,s=a?t.getViewOfSeriesModel(r):t.getViewOfComponentModel(r);!a&&i.push(s),o.isBlured&&(s.group.traverse((function(t){_l(t)})),a&&n.push(r)),o.isBlured=!1})),E(i,(function(t){t&&t.toggleBlurSeries&&t.toggleBlurSeries(n,!1,e)}))}function Vl(t,e,n,i){var r=i.getModel();function o(t,e){for(var n=0;n<e.length;n++){var i=t.getItemGraphicEl(e[n]);i&&Ol(i)}}if(n=n||\"coordinateSystem\",null!=t&&e&&\"none\"!==e){var a=r.getSeriesByIndex(t),s=a.coordinateSystem;s&&s.master&&(s=s.master);var l=[];r.eachSeries((function(t){var r=a===t,u=t.coordinateSystem;if(u&&u.master&&(u=u.master),!(\"series\"===n&&!r||\"coordinateSystem\"===n&&!(u&&s?u===s:r)||\"series\"===e&&r)){if(i.getViewOfSeriesModel(t).group.traverse((function(t){t.__highByOuter&&r&&\"self\"===e||xl(t)})),N(e))o(t.getData(),e);else if(q(e))for(var h=G(e),c=0;c<h.length;c++)o(t.getData(h[c]),e[h[c]]);l.push(t),rl(t).isBlured=!0}})),r.eachComponent((function(t,e){if(\"series\"!==t){var n=i.getViewOfComponentModel(e);n&&n.toggleBlurSeries&&n.toggleBlurSeries(l,!0,r)}}))}}function Bl(t,e,n){if(null!=t&&null!=e){var i=n.getModel().getComponent(t,e);if(i){rl(i).isBlured=!0;var r=n.getViewOfComponentModel(i);r&&r.focusBlurEnabled&&r.group.traverse((function(t){xl(t)}))}}}function Fl(t,e,n,i){var r={focusSelf:!1,dispatchers:null};if(null==t||\"series\"===t||null==e||null==n)return r;var o=i.getModel().getComponent(t,e);if(!o)return r;var a=i.getViewOfComponentModel(o);if(!a||!a.findHighDownDispatchers)return r;for(var s,l=a.findHighDownDispatchers(n),u=0;u<l.length;u++)if(\"self\"===Qs(l[u]).focus){s=!0;break}return{focusSelf:s,dispatchers:l}}function Gl(t){E(t.getAllData(),(function(e){var n=e.data,i=e.type;n.eachItemGraphicEl((function(e,n){t.isSelected(n,i)?Rl(e):Nl(e)}))}))}function Wl(t){var e=[];return t.eachSeries((function(t){E(t.getAllData(),(function(n){n.data;var i=n.type,r=t.getSelectedDataIndices();if(r.length>0){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;r<Ul.length;r++){var o=Ul[r],a=e.getModel([o,n]);t.ensureState(o).style=i?i(a):a[Zl[n]]()}}function ql(t,e){var n=!1===e,i=t;t.highDownSilentOnTouch&&(i.__highDownSilentOnTouch=t.highDownSilentOnTouch),n&&!i.__highDownDispatcher||(i.__highByOuter=i.__highByOuter||0,i.__highDownDispatcher=!n)}function Kl(t){return!(!t||!t.__highDownDispatcher)}function $l(t){var e=t.type;return e===hl||e===cl||e===pl}function Jl(t){var e=t.type;return e===ll||e===ul}var Ql=os.CMD,tu=[[],[],[]],eu=Math.sqrt,nu=Math.atan2;function iu(t,e){if(e){var n,i,r,o,a,s,l=t.data,u=t.len(),h=Ql.M,c=Ql.C,p=Ql.L,d=Ql.R,f=Ql.A,g=Ql.Q;for(r=0,o=0;r<u;){switch(n=l[r++],o=r,i=0,n){case h:case p:i=1;break;case c:i=3;break;case g:i=2;break;case f:var y=e[4],v=e[5],m=eu(e[0]*e[0]+e[1]*e[1]),x=eu(e[2]*e[2]+e[3]*e[3]),_=nu(-e[1]/x,e[0]/m);l[r]*=m,l[r++]+=y,l[r]*=x,l[r++]+=v,l[r++]*=m,l[r++]*=x,l[r++]+=_,l[r++]+=_,o=r+=2;break;case d:s[0]=l[r++],s[1]=l[r++],Wt(s,s,e),l[o++]=s[0],l[o++]=s[1],s[0]+=l[r++],s[1]+=l[r++],Wt(s,s,e),l[o++]=s[0],l[o++]=s[1]}for(a=0;a<i;a++){var b=tu[a];b[0]=l[r++],b[1]=l[r++],Wt(b,b,e),l[o++]=b[0],l[o++]=b[1]}}t.increaseVersion()}}var ru=Math.sqrt,ou=Math.sin,au=Math.cos,su=Math.PI;function lu(t){return Math.sqrt(t[0]*t[0]+t[1]*t[1])}function uu(t,e){return(t[0]*e[0]+t[1]*e[1])/(lu(t)*lu(e))}function hu(t,e){return(t[0]*e[1]<t[1]*e[0]?-1:1)*Math.acos(uu(t,e))}function cu(t,e,n,i,r,o,a,s,l,u,h){var c=l*(su/180),p=au(c)*(t-n)/2+ou(c)*(e-i)/2,d=-1*ou(c)*(t-n)/2+au(c)*(e-i)/2,f=p*p/(a*a)+d*d/(s*s);f>1&&(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;u<l.length;u++){for(var h=l[u],c=h.charAt(0),p=void 0,d=h.match(du)||[],f=d.length,g=0;g<f;g++)d[g]=parseFloat(d[g]);for(var y=0;y<f;){var v=void 0,m=void 0,x=void 0,_=void 0,b=void 0,w=void 0,S=void 0,M=i,I=r,T=void 0,C=void 0;switch(c){case\"l\":i+=d[y++],r+=d[y++],p=s.L,e.addData(p,i,r);break;case\"L\":i=d[y++],r=d[y++],p=s.L,e.addData(p,i,r);break;case\"m\":i+=d[y++],r+=d[y++],p=s.M,e.addData(p,i,r),o=i,a=r,c=\"l\";break;case\"M\":i=d[y++],r=d[y++],p=s.M,e.addData(p,i,r),o=i,a=r,c=\"L\";break;case\"h\":i+=d[y++],p=s.L,e.addData(p,i,r);break;case\"H\":i=d[y++],p=s.L,e.addData(p,i,r);break;case\"v\":r+=d[y++],p=s.L,e.addData(p,i,r);break;case\"V\":r=d[y++],p=s.L,e.addData(p,i,r);break;case\"C\":p=s.C,e.addData(p,d[y++],d[y++],d[y++],d[y++],d[y++],d[y++]),i=d[y-2],r=d[y-1];break;case\"c\":p=s.C,e.addData(p,d[y++]+i,d[y++]+r,d[y++]+i,d[y++]+r,d[y++]+i,d[y++]+r),i+=d[y-2],r+=d[y-1];break;case\"S\":v=i,m=r,T=e.len(),C=e.data,n===s.C&&(v+=i-C[T-4],m+=r-C[T-3]),p=s.C,M=d[y++],I=d[y++],i=d[y++],r=d[y++],e.addData(p,v,m,M,I,i,r);break;case\"s\":v=i,m=r,T=e.len(),C=e.data,n===s.C&&(v+=i-C[T-4],m+=r-C[T-3]),p=s.C,M=i+d[y++],I=r+d[y++],i+=d[y++],r+=d[y++],e.addData(p,v,m,M,I,i,r);break;case\"Q\":M=d[y++],I=d[y++],i=d[y++],r=d[y++],p=s.Q,e.addData(p,M,I,i,r);break;case\"q\":M=d[y++]+i,I=d[y++]+r,i+=d[y++],r+=d[y++],p=s.Q,e.addData(p,M,I,i,r);break;case\"T\":v=i,m=r,T=e.len(),C=e.data,n===s.Q&&(v+=i-C[T-4],m+=r-C[T-3]),i=d[y++],r=d[y++],p=s.Q,e.addData(p,v,m,i,r);break;case\"t\":v=i,m=r,T=e.len(),C=e.data,n===s.Q&&(v+=i-C[T-4],m+=r-C[T-3]),i+=d[y++],r+=d[y++],p=s.Q,e.addData(p,v,m,i,r);break;case\"A\":x=d[y++],_=d[y++],b=d[y++],w=d[y++],S=d[y++],cu(M=i,I=r,i=d[y++],r=d[y++],w,S,x,_,b,p=s.A,e);break;case\"a\":x=d[y++],_=d[y++],b=d[y++],w=d[y++],S=d[y++],cu(M=i,I=r,i+=d[y++],r+=d[y++],w,S,x,_,b,p=s.A,e)}}\"z\"!==c&&\"Z\"!==c||(p=s.Z,e.addData(p),i=o,r=a),n=p}return e.toStatic(),e}(t),i=A({},e);return i.buildPath=function(t){if(gu(t)){t.setData(n.data),(e=t.getContext())&&t.rebuildPath(e,1)}else{var e=t;n.rebuildPath(e,1)}},i.applyTransform=function(t){iu(n,t),this.dirtyShape()},i}function vu(t,e){return new fu(yu(t,e))}function mu(t,e){e=e||{};var n=new Is;return t.shape&&n.setShape(t.shape),n.setStyle(t.style),e.bakeTransform?iu(n.path,t.getComputedTransform()):e.toLocal?n.setLocalTransform(t.getComputedTransform()):n.copyTransform(t),n.buildPath=t.buildPath,n.applyTransform=n.applyTransform,n.z=t.z,n.z2=t.z2,n.zlevel=t.zlevel,n}var xu=function(){this.cx=0,this.cy=0,this.r=0},_u=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new xu},e.prototype.buildPath=function(t,e){t.moveTo(e.cx+e.r,e.cy),t.arc(e.cx,e.cy,e.r,0,2*Math.PI)},e}(Is);_u.prototype.type=\"circle\";var bu=function(){this.cx=0,this.cy=0,this.rx=0,this.ry=0},wu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new bu},e.prototype.buildPath=function(t,e){var n=.5522848,i=e.cx,r=e.cy,o=e.rx,a=e.ry,s=o*n,l=a*n;t.moveTo(i-o,r),t.bezierCurveTo(i-o,r-l,i-s,r-a,i,r-a),t.bezierCurveTo(i+s,r-a,i+o,r-l,i+o,r),t.bezierCurveTo(i+o,r+l,i+s,r+a,i,r+a),t.bezierCurveTo(i-s,r+a,i-o,r+l,i-o,r),t.closePath()},e}(Is);wu.prototype.type=\"ellipse\";var Su=Math.PI,Mu=2*Su,Iu=Math.sin,Tu=Math.cos,Cu=Math.acos,Du=Math.atan2,Au=Math.abs,ku=Math.sqrt,Lu=Math.max,Pu=Math.min,Ou=1e-4;function Ru(t,e,n,i,r,o,a){var s=t-n,l=e-i,u=(a?o:-o)/ku(s*s+l*l),h=u*l,c=-u*s,p=t+h,d=e+c,f=n+h,g=i+c,y=(p+f)/2,v=(d+g)/2,m=f-p,x=g-d,_=m*m+x*x,b=r-o,w=p*g-f*d,S=(x<0?-1:1)*ku(Lu(0,b*b*_-w*w)),M=(w*x-m*S)/_,I=(-w*m-x*S)/_,T=(w*x+m*S)/_,C=(-w*m+x*S)/_,D=M-y,A=I-v,k=T-y,L=C-v;return D*D+A*A>k*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),p<Su)){var z=function(t,e,n,i,r,o,a,s){var l=n-t,u=i-e,h=a-r,c=s-o,p=c*l-h*u;if(!(p*p<Ou))return[t+(p=(h*(e-o)-c*(t-r))/p)*l,e+p*u]}(k,L,D,A,T,C,P,O);if(z){var V=k-z[0],B=L-z[1],F=T-z[0],G=C-z[1],W=1/Iu(Cu((V*F+B*G)/(ku(V*V+B*B)*ku(F*F+G*G)))/2),H=ku(z[0]*z[0]+z[1]*z[1]);M=Pu(w,(i-H)/(W+1)),I=Pu(S,(r-H)/(W-1))}}}if(R)if(M>Ou){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),M<w&&X===U?t.arc(u+Z.cx,h+Z.cy,M,Du(Z.y0,Z.x0),Du(j.y0,j.x0),!c):(X>0&&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),I<S&&X===U?t.arc(u+Z.cx,h+Z.cy,I,Du(Z.y0,Z.x0),Du(j.y0,j.x0),!c):(U>0&&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;p<d;p++)Ht(a,a,t[p]),Yt(s,s,t[p]);Ht(a,a,i[0]),Yt(s,s,i[1])}for(p=0,d=t.length;p<d;p++){var f=t[p];if(n)r=t[p?p-1:d-1],o=t[(p+1)%d];else{if(0===p||p===d-1){l.push(Tt(t[p]));continue}r=t[p-1],o=t[p+1]}kt(u,o,r),Nt(u,u,e);var g=zt(f,r),y=zt(f,o),v=g+y;0!==v&&(g/=v,y/=v),Nt(h,u,-g),Nt(c,u,y);var m=Dt([],f,h),x=Dt([],f,c);i&&(Yt(m,m,a),Ht(m,m,s),Yt(x,x,a),Ht(x,x,s)),l.push(m),l.push(x)}return n&&l.push(l.shift()),l}(r,i,n,e.smoothConstraint);t.moveTo(r[0][0],r[0][1]);for(var a=r.length,s=0;s<(n?a:a-1);s++){var l=o[2*s],u=o[2*s+1],h=r[(s+1)%a];t.bezierCurveTo(l[0],l[1],u[0],u[1],h[0],h[1])}}else{t.moveTo(r[0][0],r[0][1]);s=1;for(var c=r.length;s<c;s++)t.lineTo(r[s][0],r[s][1])}n&&t.closePath()}}Bu.prototype.type=\"ring\";var Gu=function(){this.points=null,this.smooth=0,this.smoothConstraint=null},Wu=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Gu},e.prototype.buildPath=function(t,e){Fu(t,e,!0)},e}(Is);Wu.prototype.type=\"polygon\";var Hu=function(){this.points=null,this.percent=1,this.smooth=0,this.smoothConstraint=null},Yu=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 Hu},e.prototype.buildPath=function(t,e){Fu(t,e,!1)},e}(Is);Yu.prototype.type=\"polyline\";var Xu={},Uu=function(){this.x1=0,this.y1=0,this.x2=0,this.y2=0,this.percent=1},Zu=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 Uu},e.prototype.buildPath=function(t,e){var n,i,r,o;if(this.subPixelOptimize){var a=Ps(Xu,e,this.style);n=a.x1,i=a.y1,r=a.x2,o=a.y2}else n=e.x1,i=e.y1,r=e.x2,o=e.y2;var s=e.percent;0!==s&&(t.moveTo(n,i),s<1&&(r=n*(1-s)+r*s,o=i*(1-s)+o*s),t.lineTo(r,o))},e.prototype.pointAt=function(t){var e=this.shape;return[e.x1*(1-t)+e.x2*t,e.y1*(1-t)+e.y2*t]},e}(Is);Zu.prototype.type=\"line\";var ju=[],qu=function(){this.x1=0,this.y1=0,this.x2=0,this.y2=0,this.cpx1=0,this.cpy1=0,this.percent=1};function Ku(t,e,n){var i=t.cpx2,r=t.cpy2;return null!=i||null!=r?[(n?xn:mn)(t.x1,t.cpx1,t.cpx2,t.x2,e),(n?xn:mn)(t.y1,t.cpy1,t.cpy2,t.y2,e)]:[(n?Tn:In)(t.x1,t.cpx1,t.x2,e),(n?Tn:In)(t.y1,t.cpy1,t.y2,e)]}var $u=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 qu},e.prototype.buildPath=function(t,e){var n=e.x1,i=e.y1,r=e.x2,o=e.y2,a=e.cpx1,s=e.cpy1,l=e.cpx2,u=e.cpy2,h=e.percent;0!==h&&(t.moveTo(n,i),null==l||null==u?(h<1&&(Dn(n,a,r,h,ju),a=ju[1],r=ju[2],Dn(i,s,o,h,ju),s=ju[1],o=ju[2]),t.quadraticCurveTo(a,s,r,o)):(h<1&&(wn(n,a,l,r,h,ju),a=ju[1],l=ju[2],r=ju[3],wn(i,s,u,o,h,ju),s=ju[1],u=ju[2],o=ju[3]),t.bezierCurveTo(a,s,l,u,r,o)))},e.prototype.pointAt=function(t){return Ku(this.shape,t,!1)},e.prototype.tangentAt=function(t){var e=Ku(this.shape,t,!0);return Et(e,e)},e}(Is);$u.prototype.type=\"bezier-curve\";var Ju=function(){this.cx=0,this.cy=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},Qu=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 Ju},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r,0),o=e.startAngle,a=e.endAngle,s=e.clockwise,l=Math.cos(o),u=Math.sin(o);t.moveTo(l*r+n,u*r+i),t.arc(n,i,r,o,a,!s)},e}(Is);Qu.prototype.type=\"arc\";var th=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=\"compound\",e}return n(e,t),e.prototype._updatePathDirty=function(){for(var t=this.shape.paths,e=this.shapeChanged(),n=0;n<t.length;n++)e=e||t[n].shapeChanged();e&&this.dirtyShape()},e.prototype.beforeBrush=function(){this._updatePathDirty();for(var t=this.shape.paths||[],e=this.getGlobalScale(),n=0;n<t.length;n++)t[n].path||t[n].createPathProxy(),t[n].path.setScale(e[0],e[1],t[n].segmentIgnoreThreshold)},e.prototype.buildPath=function(t,e){for(var n=e.paths||[],i=0;i<n.length;i++)n[i].buildPath(t,n[i].shape,!0)},e.prototype.afterBrush=function(){for(var t=this.shape.paths||[],e=0;e<t.length;e++)t[e].pathUpdated()},e.prototype.getBoundingRect=function(){return this._updatePathDirty.call(this),Is.prototype.getBoundingRect.call(this)},e}(Is),eh=function(){function t(t){this.colorStops=t||[]}return t.prototype.addColorStop=function(t,e){this.colorStops.push({offset:t,color:e})},t}(),nh=function(t){function e(e,n,i,r,o,a){var s=t.call(this,o)||this;return s.x=null==e?0:e,s.y=null==n?0:n,s.x2=null==i?1:i,s.y2=null==r?0:r,s.type=\"linear\",s.global=a||!1,s}return n(e,t),e}(eh),ih=function(t){function e(e,n,i,r,o){var a=t.call(this,r)||this;return a.x=null==e?.5:e,a.y=null==n?.5:n,a.r=null==i?.5:i,a.type=\"radial\",a.global=o||!1,a}return n(e,t),e}(eh),rh=[0,0],oh=[0,0],ah=new De,sh=new De,lh=function(){function t(t,e){this._corners=[],this._axes=[],this._origin=[0,0];for(var n=0;n<4;n++)this._corners[n]=new De;for(n=0;n<2;n++)this._axes[n]=new De;t&&this.fromBoundingRect(t,e)}return t.prototype.fromBoundingRect=function(t,e){var n=this._corners,i=this._axes,r=t.x,o=t.y,a=r+t.width,s=o+t.height;if(n[0].set(r,o),n[1].set(a,o),n[2].set(a,s),n[3].set(r,s),e)for(var l=0;l<4;l++)n[l].transform(e);De.sub(i[0],n[1],n[0]),De.sub(i[1],n[3],n[0]),i[0].normalize(),i[1].normalize();for(l=0;l<2;l++)this._origin[l]=i[l].dot(n[0])},t.prototype.intersect=function(t,e){var n=!0,i=!e;return ah.set(1/0,1/0),sh.set(0,0),!this._intersectCheckOneSide(this,t,ah,sh,i,1)&&(n=!1,i)||!this._intersectCheckOneSide(t,this,ah,sh,i,-1)&&(n=!1,i)||i||De.copy(e,n?ah:sh),n},t.prototype._intersectCheckOneSide=function(t,e,n,i,r,o){for(var a=!0,s=0;s<2;s++){var l=this._axes[s];if(this._getProjMinMaxOnAxis(s,t._corners,rh),this._getProjMinMaxOnAxis(s,e._corners,oh),rh[1]<oh[0]||rh[0]>oh[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()&&(u<h?De.scale(i,l,-u*o):De.scale(i,l,h*o))}else if(n){u=Math.abs(oh[0]-rh[1]),h=Math.abs(rh[0]-oh[1]);Math.min(u,h)<n.len()&&(u<h?De.scale(n,l,u*o):De.scale(n,l,-h*o))}}return a},t.prototype._getProjMinMaxOnAxis=function(t,e,n){for(var i=this._axes[t],r=this._origin,o=e[0].dot(i)+r[t],a=o,s=o,l=1;l<e.length;l++){var u=e[l].dot(i)+r[t];a=Math.min(u,a),s=Math.max(u,s)}n[0]=a,n[1]=s},t}(),uh=[],hh=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.notClear=!0,e.incremental=!0,e._displayables=[],e._temporaryDisplayables=[],e._cursor=0,e}return n(e,t),e.prototype.traverse=function(t,e){t.call(e,this)},e.prototype.useStyle=function(){this.style={}},e.prototype.getCursor=function(){return this._cursor},e.prototype.innerAfterBrush=function(){this._cursor=this._displayables.length},e.prototype.clearDisplaybles=function(){this._displayables=[],this._temporaryDisplayables=[],this._cursor=0,this.markRedraw(),this.notClear=!1},e.prototype.clearTemporalDisplayables=function(){this._temporaryDisplayables=[]},e.prototype.addDisplayable=function(t,e){e?this._temporaryDisplayables.push(t):this._displayables.push(t),this.markRedraw()},e.prototype.addDisplayables=function(t,e){e=e||!1;for(var n=0;n<t.length;n++)this.addDisplayable(t[n],e)},e.prototype.getDisplayables=function(){return this._displayables},e.prototype.getTemporalDisplayables=function(){return this._temporaryDisplayables},e.prototype.eachPendingDisplayable=function(t){for(var e=this._cursor;e<this._displayables.length;e++)t&&t(this._displayables[e]);for(e=0;e<this._temporaryDisplayables.length;e++)t&&t(this._temporaryDisplayables[e])},e.prototype.update=function(){this.updateTransform();for(var t=this._cursor;t<this._displayables.length;t++){(e=this._displayables[t]).parent=this,e.update(),e.parent=null}for(t=0;t<this._temporaryDisplayables.length;t++){var e;(e=this._temporaryDisplayables[t]).parent=this,e.update(),e.parent=null}},e.prototype.getBoundingRect=function(){if(!this._rect){for(var t=new ze(1/0,1/0,-1/0,-1/0),e=0;e<this._displayables.length;e++){var n=this._displayables[e],i=n.getBoundingRect().clone();n.needLocalTransform()&&i.applyTransform(n.getLocalTransform(uh)),t.union(i)}this._rect=t}return this._rect},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e);if(this.getBoundingRect().contain(n[0],n[1]))for(var i=0;i<this._displayables.length;i++){if(this._displayables[i].contain(t,e))return!0}return!1},e}(Sa),ch=Oo();function ph(t,e,n,i,r){var o;if(e&&e.ecModel){var a=e.ecModel.getUpdatePayload();o=a&&a.animation}var s=\"update\"===t;if(e&&e.isAnimationEnabled()){var l=void 0,u=void 0,h=void 0;return i?(l=rt(i.duration,200),u=rt(i.easing,\"cubicOut\"),h=0):(l=e.getShallow(s?\"animationDurationUpdate\":\"animationDuration\"),u=e.getShallow(s?\"animationEasingUpdate\":\"animationEasing\"),h=e.getShallow(s?\"animationDelayUpdate\":\"animationDelay\")),o&&(null!=o.duration&&(l=o.duration),null!=o.easing&&(u=o.easing),null!=o.delay&&(h=o.delay)),X(h)&&(h=h(n,r)),X(l)&&(l=l(n)),{duration:l||0,delay:h,easing:u}}return null}function dh(t,e,n,i,r,o,a){var s,l=!1;X(r)?(a=o,o=r,r=null):q(r)&&(o=r.cb,a=r.during,l=r.isFrom,s=r.removeOpt,r=r.dataIndex);var u=\"leave\"===t;u||e.stopAnimation(\"leave\");var h=ph(t,i,r,u?s||{}:null,i&&i.getAnimationDelayParams?i.getAnimationDelayParams(e,r):null);if(h&&h.duration>0){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;e<t.animators.length;e++){if(\"leave\"===t.animators[e].scope)return!0}return!1}function vh(t,e,n,i,r,o){yh(t)||dh(\"leave\",t,e,n,i,r,o)}function mh(t,e,n,i){t.removeTextContent(),t.removeTextGuideLine(),vh(t,{style:{opacity:0}},e,n,i)}function xh(t,e,n){function i(){t.parent&&t.parent.remove(t)}t.isGroup?t.traverse((function(t){t.isGroup||mh(t,e,n,i)})):mh(t,e,n,i)}function _h(t){ch(t).oldStyle=t.style}var bh=Math.max,wh=Math.min,Sh={};function Mh(t){return Is.extend(t)}var Ih=function(t,e){var i=yu(t,e);return function(t){function e(e){var n=t.call(this,e)||this;return n.applyTransform=i.applyTransform,n.buildPath=i.buildPath,n}return n(e,t),e}(fu)};function Th(t,e){return Ih(t,e)}function Ch(t,e){Sh[t]=e}function Dh(t){if(Sh.hasOwnProperty(t))return Sh[t]}function Ah(t,e,n,i){var r=vu(t,e);return n&&(\"center\"===i&&(n=Lh(n,r.getBoundingRect())),Oh(r,n)),r}function kh(t,e,n){var i=new ks({style:{image:t,x:e.x,y:e.y,width:e.width,height:e.height},onload:function(t){if(\"center\"===n){var r={width:t.width,height:t.height};i.setStyle(Lh(e,r))}}});return i}function Lh(t,e){var n,i=e.width/e.height,r=t.height*i;return n=r<=t.width?t.height:(r=t.width)/i,{x:t.x+t.width/2-r/2,y:t.y+t.height/2-n/2,width:r,height:n}}var Ph=function(t,e){for(var n=[],i=t.length,r=0;r<i;r++){var o=t[r];n.push(o.getUpdatedPathProxy(!0))}var a=new Is(e);return a.createPathProxy(),a.buildPath=function(t){if(gu(t)){t.appendPath(n);var e=t.getContext();e&&t.rebuildPath(e,1)}},a};function Oh(t,e){if(t.applyTransform){var n=t.getBoundingRect().calculateTransform(e);t.applyTransform(n)}}function Rh(t,e){return Ps(t,t,{lineWidth:e}),t}var Nh=Rs;function Eh(t,e){for(var n=xe([]);t&&t!==e;)be(n,t.getLocalTransform(),n),t=t.parent;return n}function zh(t,e,n){return e&&!N(e)&&(e=gr.getLocalTransform(e)),n&&(e=Ie([],e)),Wt([],t,e)}function Vh(t,e,n){var i=0===e[4]||0===e[5]||0===e[0]?1:Math.abs(2*e[4]/e[0]),r=0===e[4]||0===e[5]||0===e[2]?1:Math.abs(2*e[4]/e[2]),o=[\"left\"===t?-i:\"right\"===t?i:0,\"top\"===t?-r:\"bottom\"===t?r:0];return o=zh(o,e,n),Math.abs(o[0])>Math.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<r.length;o++){var s=r[o];if(Xh(t,e,n,i,s[0],s[1],a[0],a[1]))return!0;a=s}}function Xh(t,e,n,i,r,o,a,s){var l,u=n-t,h=i-e,c=a-r,p=s-o,d=Uh(c,p,u,h);if((l=d)<=1e-6&&l>=-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<t.length;n++)jh(t[n],e);else jh(t,e)}Ch(\"circle\",_u),Ch(\"ellipse\",wu),Ch(\"sector\",zu),Ch(\"ring\",Bu),Ch(\"polygon\",Wu),Ch(\"polyline\",Yu),Ch(\"rect\",zs),Ch(\"line\",Zu),Ch(\"bezierCurve\",$u),Ch(\"arc\",Qu);var Kh=Object.freeze({__proto__:null,updateProps:fh,initProps:gh,removeElement:vh,removeElementWithFadeOut:xh,isElementRemoved:yh,extendShape:Mh,extendPath:Th,registerShape:Ch,getShapeClass:Dh,makePath:Ah,makeImage:kh,mergePath:Ph,resizePath:Oh,subPixelOptimizeLine:Rh,subPixelOptimizeRect:function(t){return Os(t.shape,t.shape,t.style),t},subPixelOptimize:Nh,getTransform:Eh,applyTransform:zh,transformDirection:Vh,groupTransition:Fh,clipPointsByRect:Gh,clipRectByRect:Wh,createIcon:Hh,linePolygonIntersect:Yh,lineLineIntersect:Xh,setTooltipConfig:Zh,traverseElements:qh,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,OrientedBoundingRect:lh,Point:De,Path:Is}),$h={};function Jh(t,e){for(var n=0;n<ol.length;n++){var i=ol[n],r=e[i],o=t.ensureState(i);o.style=o.style||{},o.style.text=r}var a=t.currentStates.slice();t.clearStates(!0),t.setStyle({text:e.normal}),t.useStates(a,!0)}function Qh(t,e,n){var i,r=t.labelFetcher,o=t.labelDataIndex,a=t.labelDimIndex,s=e.normal;r&&(i=r.getFormattedLabel(o,\"normal\",null,a,s&&s.get(\"formatter\"),null!=n?{interpolatedValue:n}:null)),null==i&&(i=X(t.defaultText)?t.defaultText(o,t,n):t.defaultText);for(var l={normal:i},u=0;u<ol.length;u++){var h=ol[u],c=e[h];l[h]=rt(r?r.getFormattedLabel(o,h,null,a,c&&c.get(\"formatter\")):null,i)}return l}function tc(t,e,n,i){n=n||$h;for(var r=t instanceof Fs,o=!1,a=0;a<al.length;a++){if((p=e[al[a]])&&p.getShallow(\"show\")){o=!0;break}}var s=r?t:t.getTextContent();if(o){r||(s||(s=new Fs,t.setTextContent(s)),t.stateProxy&&(s.stateProxy=t.stateProxy));var l=Qh(n,e),u=e.normal,h=!!u.getShallow(\"show\"),c=nc(u,i&&i.normal,n,!1,!r);c.text=l.normal,r||t.setTextConfig(ic(u,n,!1));for(a=0;a<ol.length;a++){var p,d=ol[a];if(p=e[d]){var f=s.ensureState(d),g=!!rt(p.getShallow(\"show\"),h);if(g!==h&&(f.ignore=!g),f.style=nc(p,i&&i[d],n,!0,!r),f.style.text=l[d],!r)t.ensureState(d).textConfig=ic(p,n,!0)}}s.silent=!!u.getShallow(\"silent\"),null!=s.style.x&&(c.x=s.style.x),null!=s.style.y&&(c.y=s.style.y),s.ignore=!h,s.useStyle(c),s.dirty(),n.enableTextSetter&&(uc(s).setLabelText=function(t){var i=Qh(n,e,t);Jh(s,i)})}else s&&(s.ignore=!0);t.dirty()}function ec(t,e){e=e||\"label\";for(var n={normal:t.getModel(e)},i=0;i<ol.length;i++){var r=ol[i];n[r]=t.getModel([r,e])}return n}function nc(t,e,n,i,r){var o={};return function(t,e,n,i,r){n=n||$h;var o,a=e.ecModel,s=a&&a.option.textStyle,l=function(t){var e;for(;t&&t!==t.ecModel;){var n=(t.option||$h).rich;if(n){e=e||{};for(var i=G(n),r=0;r<i.length;r++){e[i[r]]=1}}t=t.parentModel}return e}(e);if(l)for(var u in o={},l)if(l.hasOwnProperty(u)){var h=e.getModel([\"rich\",u]);sc(o[u]={},h,s,n,i,r,!1,!0)}o&&(t.rich=o);var c=e.get(\"overflow\");c&&(t.overflow=c);var p=e.get(\"minMargin\");null!=p&&(t.margin=p);sc(t,e,s,n,i,r,!0,!1)}(o,t,n,i,r),e&&A(o,e),o}function ic(t,e,n){e=e||{};var i,r={},o=t.getShallow(\"rotate\"),a=rt(t.getShallow(\"distance\"),n?null:5),s=t.getShallow(\"offset\");return\"outside\"===(i=t.getShallow(\"position\")||(n?null:\"inside\"))&&(i=e.defaultOutsidePosition||\"top\"),null!=i&&(r.position=i),null!=s&&(r.offset=s),null!=o&&(o*=Math.PI/180,r.rotation=o),null!=a&&(r.distance=a),r.outsideFill=\"inherit\"===t.get(\"color\")?e.inheritColor||null:\"auto\",r}var rc=[\"fontStyle\",\"fontWeight\",\"fontSize\",\"fontFamily\",\"textShadowColor\",\"textShadowBlur\",\"textShadowOffsetX\",\"textShadowOffsetY\"],oc=[\"align\",\"lineHeight\",\"width\",\"height\",\"tag\",\"verticalAlign\",\"ellipsis\"],ac=[\"padding\",\"borderWidth\",\"borderRadius\",\"borderDashOffset\",\"backgroundColor\",\"borderColor\",\"shadowColor\",\"shadowBlur\",\"shadowOffsetX\",\"shadowOffsetY\"];function sc(t,e,n,i,r,o,a,s){n=!r&&n||$h;var l=i&&i.inheritColor,u=e.getShallow(\"color\"),h=e.getShallow(\"textBorderColor\"),c=rt(e.getShallow(\"opacity\"),n.opacity);\"inherit\"!==u&&\"auto\"!==u||(u=l||null),\"inherit\"!==h&&\"auto\"!==h||(h=l||null),o||(u=u||n.color,h=h||n.textBorderColor),null!=u&&(t.fill=u),null!=h&&(t.stroke=h);var p=rt(e.getShallow(\"textBorderWidth\"),n.textBorderWidth);null!=p&&(t.lineWidth=p);var d=rt(e.getShallow(\"textBorderType\"),n.textBorderType);null!=d&&(t.lineDash=d);var f=rt(e.getShallow(\"textBorderDashOffset\"),n.textBorderDashOffset);null!=f&&(t.lineDashOffset=f),r||null!=c||s||(c=i&&i.defaultOpacity),null!=c&&(t.opacity=c),r||o||null==t.fill&&i.inheritColor&&(t.fill=i.inheritColor);for(var g=0;g<rc.length;g++){var y=rc[g];null!=(m=rt(e.getShallow(y),n[y]))&&(t[y]=m)}for(g=0;g<oc.length;g++){y=oc[g];null!=(m=e.getShallow(y))&&(t[y]=m)}if(null==t.verticalAlign){var v=e.getShallow(\"baseline\");null!=v&&(t.verticalAlign=v)}if(!a||!i.disableBox){for(g=0;g<ac.length;g++){var m;y=ac[g];null!=(m=e.getShallow(y))&&(t[y]=m)}var x=e.getShallow(\"borderType\");null!=x&&(t.borderDash=x),\"auto\"!==t.backgroundColor&&\"inherit\"!==t.backgroundColor||!l||(t.backgroundColor=l),\"auto\"!==t.borderColor&&\"inherit\"!==t.borderColor||!l||(t.borderColor=l)}}function lc(t,e){var n=e&&e.getModel(\"textStyle\");return ut([t.fontStyle||n&&n.getShallow(\"fontStyle\")||\"\",t.fontWeight||n&&n.getShallow(\"fontWeight\")||\"\",(t.fontSize||n&&n.getShallow(\"fontSize\")||12)+\"px\",t.fontFamily||n&&n.getShallow(\"fontFamily\")||\"sans-serif\"].join(\" \"))}var uc=Oo();function hc(t,e,n,i){if(t){var r=uc(t);r.prevValue=r.value,r.value=n;var o=e.normal;r.valueAnimation=o.get(\"valueAnimation\"),r.valueAnimation&&(r.precision=o.get(\"precision\"),r.defaultInterpolatedText=i,r.statesModels=e)}}function cc(t,e,n,i,r){var o=uc(t);if(o.valueAnimation&&o.prevValue!==o.value){var a=o.defaultInterpolatedText,s=rt(o.interpolatedValue,o.prevValue),l=o.value;t.percent=0,(null==o.prevValue?gh:fh)(t,{percent:1},i,e,null,(function(i){var u=Wo(n,o.precision,s,l,i);o.interpolatedValue=1===i?null:u;var h=Qh({labelDataIndex:e,labelFetcher:r,defaultText:a?a(u):u+\"\"},o.statesModels,u);Jh(t,h)}))}}var pc,dc,fc=[\"textStyle\",\"color\"],gc=[\"fontStyle\",\"fontWeight\",\"fontSize\",\"fontFamily\",\"padding\",\"lineHeight\",\"rich\",\"width\",\"height\",\"overflow\"],yc=new Fs,vc=function(){function t(){}return t.prototype.getTextColor=function(t){var e=this.ecModel;return this.getShallow(\"color\")||(!t&&e?e.get(fc):null)},t.prototype.getFont=function(){return lc({fontStyle:this.getShallow(\"fontStyle\"),fontWeight:this.getShallow(\"fontWeight\"),fontSize:this.getShallow(\"fontSize\"),fontFamily:this.getShallow(\"fontFamily\")},this.ecModel)},t.prototype.getTextRect=function(t){for(var e={text:t,verticalAlign:this.getShallow(\"verticalAlign\")||this.getShallow(\"baseline\")},n=0;n<gc.length;n++)e[gc[n]]=this.getShallow(gc[n]);return yc.useStyle(e),yc.update(),yc.getBoundingRect()},t}(),mc=[[\"lineWidth\",\"width\"],[\"stroke\",\"color\"],[\"opacity\"],[\"shadowBlur\"],[\"shadowOffsetX\"],[\"shadowOffsetY\"],[\"shadowColor\"],[\"lineDash\",\"type\"],[\"lineDashOffset\",\"dashOffset\"],[\"lineCap\",\"cap\"],[\"lineJoin\",\"join\"],[\"miterLimit\"]],xc=Jo(mc),_c=function(){function t(){}return t.prototype.getLineStyle=function(t){return xc(this,t)},t}(),bc=[[\"fill\",\"color\"],[\"stroke\",\"borderColor\"],[\"lineWidth\",\"borderWidth\"],[\"opacity\"],[\"shadowBlur\"],[\"shadowOffsetX\"],[\"shadowOffsetY\"],[\"shadowColor\"],[\"lineDash\",\"borderType\"],[\"lineDashOffset\",\"borderDashOffset\"],[\"lineCap\",\"borderCap\"],[\"lineJoin\",\"borderJoin\"],[\"miterLimit\",\"borderMiterLimit\"]],wc=Jo(bc),Sc=function(){function t(){}return t.prototype.getItemStyle=function(t,e){return wc(this,t,e)},t}(),Mc=function(){function t(t,e,n){this.parentModel=e,this.ecModel=n,this.option=t}return t.prototype.init=function(t,e,n){for(var i=[],r=3;r<arguments.length;r++)i[r-3]=arguments[r]},t.prototype.mergeOption=function(t,e){C(this.option,t,!0)},t.prototype.get=function(t,e){return null==t?this.option:this._doGet(this.parsePath(t),!e&&this.parentModel)},t.prototype.getShallow=function(t,e){var n=this.option,i=null==n?n:n[t];if(null==i&&!e){var r=this.parentModel;r&&(i=r.getShallow(t))}return i},t.prototype.getModel=function(e,n){var i=null!=e,r=i?this.parsePath(e):null;return new t(i?this._doGet(r):this.option,n=n||this.parentModel&&this.parentModel.getModel(this.resolveParentPath(r)),this.ecModel)},t.prototype.isEmpty=function(){return null==this.option},t.prototype.restoreData=function(){},t.prototype.clone=function(){return new(0,this.constructor)(T(this.option))},t.prototype.parsePath=function(t){return\"string\"==typeof t?t.split(\".\"):t},t.prototype.resolveParentPath=function(t){return t},t.prototype.isAnimationEnabled=function(){if(!r.node&&this.option){if(null!=this.option.animation)return!!this.option.animation;if(this.parentModel)return this.parentModel.isAnimationEnabled()}},t.prototype._doGet=function(t,e){var n=this.option;if(!t)return n;for(var i=0;i<t.length&&(!t[i]||null!=(n=n&&\"object\"==typeof n?n[t[i]]:null));i++);return null==n&&e&&(n=e._doGet(this.resolveParentPath(t),e.parentModel)),n},t}();Uo(Mc),pc=Mc,dc=[\"__\\0is_clz\",jo++].join(\"_\"),pc.prototype[dc]=!0,pc.isInstance=function(t){return!(!t||!t[dc])},R(Mc,_c),R(Mc,Sc),R(Mc,ta),R(Mc,vc);var Ic=Math.round(10*Math.random());function Tc(t){return[t||\"\",Ic++].join(\"_\")}function Cc(t,e){return C(C({},t,!0),e,!0)}var Dc=\"ZH\",Ac=\"EN\",kc=Ac,Lc={},Pc={},Oc=r.domSupported&&(document.documentElement.lang||navigator.language||navigator.browserLanguage).toUpperCase().indexOf(Dc)>-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<r.length;o++){var a=yp[o];t=t.replace(vp(a),vp(a,0))}for(var s=0;s<i;s++)for(var l=0;l<r.length;l++){var u=e[s][r[l]];t=t.replace(vp(yp[l],s),n?re(u):u)}return t}function xp(t,e){var n=U(t)?{color:t,extraCssText:e}:t||{},i=n.color,r=n.type;e=n.extraCssText;var o=n.renderMode||\"html\";return i?\"html\"===o?\"subItem\"===r?'<span style=\"display:inline-block;vertical-align:middle;margin-right:8px;margin-left:3px;border-radius:4px;width:4px;height:4px;background-color:'+re(i)+\";\"+(e||\"\")+'\"></span>':'<span style=\"display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:'+re(i)+\";\"+(e||\"\")+'\"></span>':{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<n.length;c++){var p=n[c];if(!s(o,p)&&s(t,p)){o[p]=t[p];break}}return o}return u}function s(t,e){return t.hasOwnProperty(e)}function l(t,e){return null!=t[e]&&\"auto\"!==t[e]}function u(t,e,n){wp(t,(function(t){e[t]=n[t]}))}u(Mp[0],t,r),u(Mp[1],t,o)}function Lp(t){return Pp({},t)}function Pp(t,e){return e&&t&&wp(Sp,(function(n){e.hasOwnProperty(n)&&(t[n]=e[n])})),t}var Op=Oo(),Rp=function(t){function e(e,n,i){var r=t.call(this,e,n,i)||this;return r.uid=Tc(\"ec_cpt_model\"),r}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n)},e.prototype.mergeDefaultAndTheme=function(t,e){var n=Ap(this),i=n?Lp(t):{};C(t,e.getTheme().get(this.mainType)),C(t,this.getDefaultOption()),n&&kp(t,i,n)},e.prototype.mergeOption=function(t,e){C(this.option,t,!0);var n=Ap(this);n&&kp(this.option,t,n)},e.prototype.optionUpdated=function(t,e){},e.prototype.getDefaultOption=function(){var t=this.constructor;if(!function(t){return!(!t||!t[Yo])}(t))return t.defaultOption;var e=Op(this);if(!e.defaultOption){for(var n=[],i=t;i;){var r=i.prototype.defaultOption;r&&n.push(r),i=i.superClass}for(var o={},a=n.length-1;a>=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;i<n;i++)t.push(e+i)}function f(t){var e=t.dimsDef;return e?e.length:1}return E(t,(function(t,e){var n=t.name,r=f(t);if(null==o){var a=p.valueWayDim;d(i[n],a,r),d(l,a,r),p.valueWayDim+=r}else if(o===e)d(i[n],0,r),d(s,0,r);else{a=p.categoryWayDim;d(i[n],a,r),d(l,a,r),p.categoryWayDim+=r}})),s.length&&(i.itemName=s),l.length&&(i.seriesName=l),i}function Jp(t,e,n){var i={};if(!Qp(t))return i;var r,o=e.sourceFormat,a=e.dimensionsDefine;o!==Gp&&o!==Wp||E(a,(function(t,e){\"name\"===(q(t)?t.name:t)&&(r=e)}));var s=function(){for(var t={},i={},s=[],l=0,u=Math.min(5,n);l<u;l++){var h=ed(e.data,o,e.seriesLayoutBy,a,e.startIndex,l);s.push(h);var c=h===qp;if(c&&null==t.v&&l!==r&&(t.v=l),(null==t.n||t.n===t.v||!c&&s[t.n]===qp)&&(t.n=l),p(t)&&s[t.n]!==qp)return t;c||(h===jp&&null==i.v&&l!==r&&(i.v=l),null!=i.n&&i.n!==i.v||(i.n=l))}function p(t){return null!=t.v&&null!=t.n}return p(t)?t:p(i)?i:null}();if(s){i.value=[s.v];var l=null!=r?r:s.n;i.itemName=[l],i.seriesName=[l]}return i}function Qp(t){if(!t.get(\"data\",!0))return Bo(t.ecModel,\"dataset\",{index:t.get(\"datasetIndex\",!0),id:t.get(\"datasetId\",!0)},zo).models[0]}function td(t,e){return ed(t.data,t.sourceFormat,t.seriesLayoutBy,t.dimensionsDefine,t.startIndex,e)}function ed(t,e,n,i,r,o){var a,s,l;if($(t))return qp;if(i){var u=i[o];q(u)?(s=u.name,l=u.type):U(u)&&(s=u)}if(null!=l)return\"ordinal\"===l?Zp:qp;if(e===Fp){var h=t;if(n===Up){for(var c=h[o],p=0;p<(c||[]).length&&p<5;p++)if(null!=(a=m(c[r+p])))return a}else for(p=0;p<h.length&&p<5;p++){var d=h[r+p];if(d&&null!=(a=m(d[o])))return a}}else if(e===Gp){var f=t;if(!s)return qp;for(p=0;p<f.length&&p<5;p++){if((y=f[p])&&null!=(a=m(y[s])))return a}}else if(e===Wp){if(!s)return qp;if(!(c=t[s])||$(c))return qp;for(p=0;p<c.length&&p<5;p++)if(null!=(a=m(c[p])))return a}else if(e===Bp){var g=t;for(p=0;p<g.length&&p<5;p++){var y,v=Mo(y=g[p]);if(!Y(v))return qp;if(null!=(a=m(v[o])))return a}}function m(t){var e=U(t);return null!=t&&isFinite(t)&&\"\"!==t?e?jp:qp:e&&\"-\"!==t?Zp:void 0}return qp}var nd=yt();var id,rd,od,ad=Oo(),sd=Oo(),ld=function(){function t(){}return t.prototype.getColorFromPalette=function(t,e,n){var i=bo(this.get(\"color\",!0)),r=this.get(\"colorLayer\",!0);return hd(this,ad,i,r,t,e,n)},t.prototype.clearColorPalette=function(){!function(t,e){e(t).paletteIdx=0,e(t).paletteNameMap={}}(this,ad)},t}();function ud(t,e,n,i){var r=bo(t.get([\"aria\",\"decal\",\"decals\"]));return hd(t,sd,r,null,e,n,i)}function hd(t,e,n,i,r,o,a){var s=e(o=o||t),l=s.paletteIdx||0,u=s.paletteNameMap=s.paletteNameMap||{};if(u.hasOwnProperty(r))return u[r];var h=null!=a&&i?function(t,e){for(var n=t.length,i=0;i<n;i++)if(t[i].length>e)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<n.length;r++)if(n[r])return n[r]}},e.prototype.queryComponents=function(t){var e=t.mainType;if(!e)return[];var n,i=t.index,r=t.id,o=t.name,a=this._componentsMap.get(e);return a&&a.length?(null!=i?(n=[],E(bo(i),(function(t){a[t]&&n.push(a[t])}))):n=null!=r?dd(\"id\",r,a):null!=o?dd(\"name\",o,a):B(a,(function(t){return!!t})),fd(n,t)):[]},e.prototype.findComponents=function(t){var e,n,i,r,o,a=t.query,s=t.mainType,l=(n=s+\"Index\",i=s+\"Id\",r=s+\"Name\",!(e=a)||null==e[n]&&null==e[i]&&null==e[r]?null:{mainType:s,index:e[n],id:e[i],name:e[r]}),u=l?this.queryComponents(l):B(this._componentsMap.get(s),(function(t){return!!t}));return o=fd(u,t),t.filter?B(o,t.filter):o},e.prototype.eachComponent=function(t,e,n){var i=this._componentsMap;if(X(t)){var r=e,o=t;i.each((function(t,e){for(var n=0;t&&n<t.length;n++){var i=t[n];i&&o.call(r,e,i,i.componentIndex)}}))}else for(var a=U(t)?i.get(t):q(t)?this.findComponents(t):null,s=0;a&&s<a.length;s++){var l=a[s];l&&e.call(n,l,l.componentIndex)}},e.prototype.getSeriesByName=function(t){var e=Ao(t,null);return B(this._componentsMap.get(\"series\"),(function(t){return!!t&&null!=e&&t.name===e}))},e.prototype.getSeriesByIndex=function(t){return this._componentsMap.get(\"series\")[t]},e.prototype.getSeriesByType=function(t){return B(this._componentsMap.get(\"series\"),(function(e){return!!e&&e.subType===t}))},e.prototype.getSeries=function(){return B(this._componentsMap.get(\"series\"),(function(t){return!!t}))},e.prototype.getSeriesCount=function(){return this._componentsCount.get(\"series\")},e.prototype.eachSeries=function(t,e){rd(this),E(this._seriesIndices,(function(n){var i=this._componentsMap.get(\"series\")[n];t.call(e,i,n)}),this)},e.prototype.eachRawSeries=function(t,e){E(this._componentsMap.get(\"series\"),(function(n){n&&t.call(e,n,n.componentIndex)}))},e.prototype.eachSeriesByType=function(t,e,n){rd(this),E(this._seriesIndices,(function(i){var r=this._componentsMap.get(\"series\")[i];r.subType===t&&e.call(n,r,i)}),this)},e.prototype.eachRawSeriesByType=function(t,e,n){return E(this.getSeriesByType(t),e,n)},e.prototype.isSeriesFiltered=function(t){return rd(this),null==this._seriesIndicesMap.get(t.componentIndex)},e.prototype.getCurrentSeriesIndices=function(){return(this._seriesIndices||[]).slice()},e.prototype.filterSeries=function(t,e){rd(this);var n=[];E(this._seriesIndices,(function(i){var r=this._componentsMap.get(\"series\")[i];t.call(e,r,i)&&n.push(i)}),this),this._seriesIndices=n,this._seriesIndicesMap=yt(n)},e.prototype.restoreData=function(t){id(this);var e=this._componentsMap,n=[];e.each((function(t,e){Rp.hasClass(e)&&n.push(e)})),Rp.topologicalTravel(n,Rp.getAllClassMainTypes(),(function(n){E(e.get(n),(function(e){!e||\"series\"===n&&function(t,e){if(e){var n=e.seriesIndex,i=e.seriesId,r=e.seriesName;return null!=n&&t.componentIndex!==n||null!=i&&t.id!==i||null!=r&&t.name!==r}}(e,t)||e.restoreData()}))}))},e.internalField=(id=function(t){var e=t._seriesIndices=[];E(t._componentsMap.get(\"series\"),(function(t){t&&e.push(t.componentIndex)})),t._seriesIndicesMap=yt(e)},rd=function(t){},void(od=function(t,e){t.option={},t.option[cd]=1,t._componentsMap=yt({series:[]}),t._componentsCount=yt();var n=e.aria;q(n)&&null==n.enabled&&(n.enabled=!0),function(t,e){var n=t.color&&!t.colorLayer;E(e,(function(e,i){\"colorLayer\"===i&&n||Rp.hasClass(i)||(\"object\"==typeof e?t[i]=t[i]?C(t[i],e,!1):T(e):null==t[i]&&(t[i]=e))}))}(e,t._theme.option),C(e,zp,!1),t._mergeOption(e,null)})),e}(Mc);function dd(t,e,n){if(Y(e)){var i=yt();return E(e,(function(t){null!=t&&(null!=Ao(t,null)&&i.set(t,!0))})),B(n,(function(e){return e&&i.get(e[t])}))}var r=Ao(e,null);return B(n,(function(e){return e&&null!=r&&e[t]===r}))}function fd(t,e){return e.hasOwnProperty(\"subType\")?B(t,(function(t){return t&&t.subType===e.subType})):t}function gd(t){var e=yt();return t&&E(bo(t.replaceMerge),(function(t){e.set(t,!0)})),{replaceMergeMainTypeMap:e}}R(pd,ld);var yd=[\"getDom\",\"getZr\",\"getWidth\",\"getHeight\",\"getDevicePixelRatio\",\"dispatchAction\",\"isSSR\",\"isDisposed\",\"on\",\"off\",\"getDataURL\",\"getConnectedDataURL\",\"getOption\",\"getId\",\"updateLabelLayout\"],vd=function(t){E(yd,(function(e){this[e]=W(t[e],t)}),this)},md={},xd=function(){function t(){this._coordinateSystems=[]}return t.prototype.create=function(t,e){var n=[];E(md,(function(i,r){var o=i.create(t,e);n=n.concat(o||[])})),this._coordinateSystems=n},t.prototype.update=function(t,e){E(this._coordinateSystems,(function(n){n.update&&n.update(t,e)}))},t.prototype.getCoordinateSystems=function(){return this._coordinateSystems.slice()},t.register=function(t,e){md[t]=e},t.get=function(t){return md[t]},t}(),_d=/^(min|max)?(.+)$/,bd=function(){function t(t){this._timelineOptions=[],this._mediaList=[],this._currentMediaIndices=[],this._api=t}return t.prototype.setOption=function(t,e,n){t&&(E(bo(t.series),(function(t){t&&t.data&&$(t.data)&&ct(t.data)})),E(bo(t.dataset),(function(t){t&&t.source&&$(t.source)&&ct(t.source)}))),t=T(t);var i=this._optionBackup,r=function(t,e,n){var i,r,o=[],a=t.baseOption,s=t.timeline,l=t.options,u=t.media,h=!!t.media,c=!!(l||s||a&&a.timeline);a?(r=a).timeline||(r.timeline=s):((c||h)&&(t.options=t.media=null),r=t);h&&Y(u)&&E(u,(function(t){t&&t.option&&(t.query?o.push(t):i||(i=t))}));function p(t){E(e,(function(e){e(t,n)}))}return p(r),E(l,(function(t){return p(t)})),E(o,(function(t){return p(t.option)})),{baseOption:r,timelineOptions:l||[],mediaDefault:i,mediaList:o}}(t,e,!i);this._newBaseOption=r.baseOption,i?(r.timelineOptions.length&&(i.timelineOptions=r.timelineOptions),r.mediaList.length&&(i.mediaList=r.mediaList),r.mediaDefault&&(i.mediaDefault=r.mediaDefault)):this._optionBackup=r},t.prototype.mountOption=function(t){var e=this._optionBackup;return this._timelineOptions=e.timelineOptions,this._mediaList=e.mediaList,this._mediaDefault=e.mediaDefault,this._currentMediaIndices=[],T(t?e.baseOption:this._newBaseOption)},t.prototype.getTimelineOption=function(t){var e,n=this._timelineOptions;if(n.length){var i=t.getComponent(\"timeline\");i&&(e=T(n[i.getCurrentIndex()]))}return e},t.prototype.getMediaOption=function(t){var e,n,i=this._api.getWidth(),r=this._api.getHeight(),o=this._mediaList,a=this._mediaDefault,s=[],l=[];if(!o.length&&!a)return l;for(var u=0,h=o.length;u<h;u++)wd(o[u].query,i,r)&&s.push(u);return!s.length&&a&&(s=[-1]),s.length&&(e=s,n=this._currentMediaIndices,e.join(\",\")!==n.join(\",\"))&&(l=z(s,(function(t){return T(-1===t?a.option:o[t].option)}))),this._currentMediaIndices=s,l},t}();function wd(t,e,n){var i={width:e,height:n,aspectratio:e/n},r=!0;return E(t,(function(t,e){var n=e.match(_d);if(n&&n[1]&&n[2]){var o=n[1],a=n[2].toLowerCase();(function(t,e,n){return\"min\"===n?t>=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<i;n++){var r=Id[n],o=e.normal,a=e.emphasis;o&&o[r]&&(t[r]=t[r]||{},t[r].normal?C(t[r].normal,o[r]):t[r].normal=o[r],o[r]=null),a&&a[r]&&(t[r]=t[r]||{},t[r].emphasis?C(t[r].emphasis,a[r]):t[r].emphasis=a[r],a[r]=null)}}function Cd(t,e,n){if(t&&t[e]&&(t[e].normal||t[e].emphasis)){var i=t[e].normal,r=t[e].emphasis;i&&(n?(t[e].normal=t[e].emphasis=null,k(t[e],i)):t[e]=i),r&&(t.emphasis=t.emphasis||{},t.emphasis[e]=r,r.focus&&(t.emphasis.focus=r.focus),r.blurScope&&(t.emphasis.blurScope=r.blurScope))}}function Dd(t){Cd(t,\"itemStyle\"),Cd(t,\"lineStyle\"),Cd(t,\"areaStyle\"),Cd(t,\"label\"),Cd(t,\"labelLine\"),Cd(t,\"upperLabel\"),Cd(t,\"edgeLabel\")}function Ad(t,e){var n=Md(t)&&t[e],i=Md(n)&&n.textStyle;if(i){0;for(var r=0,o=So.length;r<o;r++){var a=So[r];i.hasOwnProperty(a)&&(n[a]=i[a])}}}function kd(t){t&&(Dd(t),Ad(t,\"label\"),t.emphasis&&Ad(t.emphasis,\"label\"))}function Ld(t){return Y(t)?t:t?[t]:[]}function Pd(t){return(Y(t)?t[0]:t)||{}}function Od(t,e){Sd(Ld(t.series),(function(t){Md(t)&&function(t){if(Md(t)){Td(t),Dd(t),Ad(t,\"label\"),Ad(t,\"upperLabel\"),Ad(t,\"edgeLabel\"),t.emphasis&&(Ad(t.emphasis,\"label\"),Ad(t.emphasis,\"upperLabel\"),Ad(t.emphasis,\"edgeLabel\"));var e=t.markPoint;e&&(Td(e),kd(e));var n=t.markLine;n&&(Td(n),kd(n));var i=t.markArea;i&&kd(i);var r=t.data;if(\"graph\"===t.type){r=r||t.nodes;var o=t.links||t.edges;if(o&&!$(o))for(var a=0;a<o.length;a++)kd(o[a]);E(t.categories,(function(t){Dd(t)}))}if(r&&!$(r))for(a=0;a<r.length;a++)kd(r[a]);if((e=t.markPoint)&&e.data){var s=e.data;for(a=0;a<s.length;a++)kd(s[a])}if((n=t.markLine)&&n.data){var l=n.data;for(a=0;a<l.length;a++)Y(l[a])?(kd(l[a][0]),kd(l[a][1])):kd(l[a])}\"gauge\"===t.type?(Ad(t,\"axisLabel\"),Ad(t,\"title\"),Ad(t,\"detail\")):\"treemap\"===t.type?(Cd(t.breadcrumb,\"itemStyle\"),E(t.levels,(function(t){Dd(t)}))):\"tree\"===t.type&&Dd(t.leaves)}}(t)}));var n=[\"xAxis\",\"yAxis\",\"radiusAxis\",\"angleAxis\",\"singleAxis\",\"parallelAxis\",\"radar\"];e&&n.push(\"valueAxis\",\"categoryAxis\",\"logAxis\",\"timeAxis\"),Sd(n,(function(e){Sd(Ld(t[e]),(function(t){t&&(Ad(t,\"axisLabel\"),Ad(t.axisPointer,\"label\"))}))})),Sd(Ld(t.parallel),(function(t){var e=t&&t.parallelAxisDefault;Ad(e,\"axisLabel\"),Ad(e&&e.axisPointer,\"label\")})),Sd(Ld(t.calendar),(function(t){Cd(t,\"itemStyle\"),Ad(t,\"dayLabel\"),Ad(t,\"monthLabel\"),Ad(t,\"yearLabel\")})),Sd(Ld(t.radar),(function(t){Ad(t,\"name\"),t.name&&null==t.axisName&&(t.axisName=t.name,delete t.name),null!=t.nameGap&&null==t.axisNameGap&&(t.axisNameGap=t.nameGap,delete t.nameGap)})),Sd(Ld(t.geo),(function(t){Md(t)&&(kd(t),Sd(Ld(t.regions),(function(t){kd(t)})))})),Sd(Ld(t.timeline),(function(t){kd(t),Cd(t,\"label\"),Cd(t,\"itemStyle\"),Cd(t,\"controlStyle\",!0);var e=t.data;Y(e)&&E(e,(function(t){q(t)&&(Cd(t,\"label\"),Cd(t,\"itemStyle\"))}))})),Sd(Ld(t.toolbox),(function(t){Cd(t,\"iconStyle\"),Sd(t.feature,(function(t){Cd(t,\"iconStyle\")}))})),Ad(Pd(t.axisPointer),\"label\"),Ad(Pd(t.tooltip).axisPointer,\"label\")}function Rd(t){t&&E(Nd,(function(e){e[0]in t&&!(e[1]in t)&&(t[e[1]]=t[e[0]])}))}var Nd=[[\"x\",\"left\"],[\"y\",\"top\"],[\"x2\",\"right\"],[\"y2\",\"bottom\"]],Ed=[\"grid\",\"geo\",\"parallel\",\"legend\",\"toolbox\",\"title\",\"visualMap\",\"dataZoom\",\"timeline\"],zd=[[\"borderRadius\",\"barBorderRadius\"],[\"borderColor\",\"barBorderColor\"],[\"borderWidth\",\"barBorderWidth\"]];function Vd(t){var e=t&&t.itemStyle;if(e)for(var n=0;n<zd.length;n++){var i=zd[n][1],r=zd[n][0];null!=e[i]&&(e[r]=e[i])}}function Bd(t){t&&\"edge\"===t.alignTo&&null!=t.margin&&null==t.edgeDistance&&(t.edgeDistance=t.margin)}function Fd(t){t&&t.downplay&&!t.blur&&(t.blur=t.downplay)}function Gd(t,e){if(t)for(var n=0;n<t.length;n++)e(t[n]),t[n]&&Gd(t[n].children,e)}function Wd(t,e){Od(t,e),t.series=bo(t.series),E(t.series,(function(t){if(q(t)){var e=t.type;if(\"line\"===e)null!=t.clipOverflow&&(t.clip=t.clipOverflow);else if(\"pie\"===e||\"gauge\"===e){if(null!=t.clockWise&&(t.clockwise=t.clockWise),Bd(t.label),(r=t.data)&&!$(r))for(var n=0;n<r.length;n++)Bd(r[n]);null!=t.hoverOffset&&(t.emphasis=t.emphasis||{},(t.emphasis.scaleSize=null)&&(t.emphasis.scaleSize=t.hoverOffset))}else if(\"gauge\"===e){var i=function(t,e){for(var n=e.split(\",\"),i=t,r=0;r<n.length&&null!=(i=i&&i[n[r]]);r++);return i}(t,\"pointer.color\");null!=i&&function(t,e,n,i){for(var r,o=e.split(\",\"),a=t,s=0;s<o.length-1;s++)null==a[r=o[s]]&&(a[r]={}),a=a[r];(i||null==a[o[s]])&&(a[o[s]]=n)}(t,\"itemStyle.color\",i)}else if(\"bar\"===e){var r;if(Vd(t),Vd(t.backgroundStyle),Vd(t.emphasis),(r=t.data)&&!$(r))for(n=0;n<r.length;n++)\"object\"==typeof r[n]&&(Vd(r[n]),Vd(r[n]&&r[n].emphasis))}else if(\"sunburst\"===e){var o=t.highlightPolicy;o&&(t.emphasis=t.emphasis||{},t.emphasis.focus||(t.emphasis.focus=o)),Fd(t),Gd(t.data,Fd)}else\"graph\"===e||\"sankey\"===e?function(t){t&&null!=t.focusNodeAdjacency&&(t.emphasis=t.emphasis||{},null==t.emphasis.focus&&(t.emphasis.focus=\"adjacency\"))}(t):\"map\"===e&&(t.mapType&&!t.map&&(t.map=t.mapType),t.mapLocation&&k(t,t.mapLocation));null!=t.hoverAnimation&&(t.emphasis=t.emphasis||{},t.emphasis&&null==t.emphasis.scale&&(t.emphasis.scale=t.hoverAnimation)),Rd(t)}})),t.dataRange&&(t.visualMap=t.dataRange),E(Ed,(function(e){var n=t[e];n&&(Y(n)||(n=[n]),E(n,(function(t){Rd(t)})))}))}function Hd(t){E(t,(function(e,n){var i=[],r=[NaN,NaN],o=[e.stackResultDimension,e.stackedOverDimension],a=e.data,s=e.isStackedByIndex,l=e.seriesModel.get(\"stackStrategy\")||\"samesign\";a.modify(o,(function(o,u,h){var c,p,d=a.get(e.stackedDimension,h);if(isNaN(d))return r;s?p=a.getRawIndex(h):c=a.get(e.stackedByDimension,h);for(var f=NaN,g=n-1;g>=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;n<e.length;n++){var i=e[n];null==i.type&&td(this,n)===Zp&&(i.type=\"ordinal\")}};function Kd(t){return t instanceof qd}function $d(t,e,n){n=n||Qd(t);var i=e.seriesLayoutBy,r=function(t,e,n,i,r){var o,a;if(!t)return{dimensionsDefine:tf(r),startIndex:a,dimensionsDetectedCount:o};if(e===Fp){var s=t;\"auto\"===i||null==i?ef((function(t){null!=t&&\"-\"!==t&&(U(t)?null==a&&(a=1):a=0)}),n,s,10):a=j(i)?i:i?1:0,r||1!==a||(r=[],ef((function(t,e){r[e]=null!=t?t+\"\":\"\"}),n,s,1/0)),o=r?r.length:n===Up?s.length:s[0]?s[0].length:null}else if(e===Gp)r||(r=function(t){var e,n=0;for(;n<t.length&&!(e=t[n++]););if(e)return G(e)}(t));else if(e===Wp)r||(r=[],E(t,(function(t,e){r.push(e)})));else if(e===Bp){var l=Mo(t[0]);o=Y(l)&&l.length||1}return{startIndex:a,dimensionsDefine:tf(r),dimensionsDetectedCount:o}}(t,n,i,e.sourceHeader,e.dimensions);return new qd({data:t,sourceFormat:n,seriesLayoutBy:i,dimensionsDefine:r.dimensionsDefine,startIndex:r.startIndex,dimensionsDetectedCount:r.dimensionsDetectedCount,metaRawOption:T(e)})}function Jd(t){return new qd({data:t,sourceFormat:$(t)?Hp:Bp})}function Qd(t){var e=Yp;if($(t))e=Hp;else if(Y(t)){0===t.length&&(e=Fp);for(var n=0,i=t.length;n<i;n++){var r=t[n];if(null!=r){if(Y(r)){e=Fp;break}if(q(r)){e=Gp;break}}}}else if(q(t))for(var o in t)if(_t(t,o)&&N(t[o])){e=Wp;break}return e}function tf(t){if(t){var e=yt();return z(t,(function(t,n){var i={name:(t=q(t)?t:{name:t}).name,displayName:t.displayName,type:t.type};if(null==i.name)return i;i.name+=\"\",null==i.displayName&&(i.displayName=i.name);var r=e.get(i.name);return r?i.name+=\"-\"+r.count++:e.set(i.name,{count:1}),i}))}}function ef(t,e,n,i){if(e===Up)for(var r=0;r<n.length&&r<i;r++)t(n[r]?n[r][0]:null,r);else{var o=n[0]||[];for(r=0;r<o.length&&r<i;r++)t(o[r],r)}}function nf(t){var e=t.sourceFormat;return e===Gp||e===Wp}var rf=function(){function t(t,e){var n=Kd(t)?t:Jd(t);this._source=n;var i=this._data=n.data;n.sourceFormat===Hp&&(this._offset=0,this._dimSize=e,this._data=i),jd(this,i,n)}return t.prototype.getSource=function(){return this._source},t.prototype.count=function(){return 0},t.prototype.getItem=function(t,e){},t.prototype.appendData=function(t){},t.prototype.clean=function(){},t.protoInitialize=function(){var e=t.prototype;e.pure=!1,e.persistent=!0}(),t.internalField=function(){var t;jd=function(t,r,o){var a=o.sourceFormat,s=o.seriesLayoutBy,l=o.startIndex,u=o.dimensionsDefine,h=Zd[ff(a,s)];if(A(t,h),a===Hp)t.getItem=e,t.count=i,t.fillStorage=n;else{var c=sf(a,s);t.getItem=W(c,null,r,l,u);var p=hf(a,s);t.count=W(p,null,r,l,u)}};var e=function(t,e){t-=this._offset,e=e||[];for(var n=this._data,i=this._dimSize,r=i*t,o=0;o<i;o++)e[o]=n[r+o];return e},n=function(t,e,n,i){for(var r=this._data,o=this._dimSize,a=0;a<o;a++){for(var s=i[a],l=null==s[0]?1/0:s[0],u=null==s[1]?-1/0:s[1],h=e-t,c=n[a],p=0;p<h;p++){var d=r[p*o+a];c[t+p]=d,d<l&&(l=d),d>u&&(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<t.length;e++)this._data.push(t[e])}(t={})[Fp+\"_\"+Xp]={pure:!0,appendData:r},t[Fp+\"_\"+Up]={pure:!0,appendData:function(){throw new Error('Do not support appendData when set seriesLayoutBy: \"row\".')}},t[Gp]={pure:!0,appendData:r},t[Wp]={pure:!0,appendData:function(t){var e=this._data;E(t,(function(t,n){for(var i=e[n]||(e[n]=[]),r=0;r<(t||[]).length;r++)i.push(t[r])}))}},t[Bp]={appendData:r},t[Hp]={persistent:!1,pure:!0,appendData:function(t){this._data=t},clean:function(){this._offset+=this.count(),this._data=null}},Zd=t}(),t}(),of=function(t,e,n,i){return t[i]},af=((Yd={})[Fp+\"_\"+Xp]=function(t,e,n,i){return t[i+e]},Yd[Fp+\"_\"+Up]=function(t,e,n,i,r){i+=e;for(var o=r||[],a=t,s=0;s<a.length;s++){var l=a[s];o[s]=l?l[i]:null}return o},Yd[Gp]=of,Yd[Wp]=function(t,e,n,i,r){for(var o=r||[],a=0;a<n.length;a++){var s=n[a].name;0;var l=t[s];o[a]=l?l[i]:null}return o},Yd[Bp]=of,Yd);function sf(t,e){var n=af[ff(t,e)];return n}var lf=function(t,e,n){return t.length},uf=((Xd={})[Fp+\"_\"+Xp]=function(t,e,n){return Math.max(0,t.length-e)},Xd[Fp+\"_\"+Up]=function(t,e,n){var i=t[0];return i?Math.max(0,i.length-e):0},Xd[Gp]=lf,Xd[Wp]=function(t,e,n){var i=n[0].name;var r=t[i];return r?r.length:0},Xd[Bp]=lf,Xd);function hf(t,e){var n=uf[ff(t,e)];return n}var cf=function(t,e,n){return t[e]},pf=((Ud={})[Fp]=cf,Ud[Gp]=function(t,e,n){return t[n]},Ud[Wp]=cf,Ud[Bp]=function(t,e,n){var i=Mo(t);return i instanceof Array?i[e]:i},Ud[Hp]=cf,Ud);function df(t){var e=pf[t];return e}function ff(t,e){return t===Fp?t+\"_\"+e:t}function gf(t,e,n){if(t){var i=t.getRawDataItem(e);if(null!=i){var r=t.getStore(),o=r.getSource().sourceFormat;if(null!=n){var a=t.getDimensionIndex(n),s=r.getDimensionProperty(a);return df(o)(i,a,s)}var l=i;return o===Bp&&(l=Mo(i)),l}}}var yf=/\\{@(.+?)\\}/g,vf=function(){function t(){}return t.prototype.getDataParams=function(t,e){var n=this.getData(e),i=this.getRawValue(t,e),r=n.getRawIndex(t),o=n.getName(t),a=n.getRawDataItem(t),s=n.getItemVisual(t,\"style\"),l=s&&s[n.getItemVisual(t,\"drawType\")||\"fill\"],u=s&&s.stroke,h=this.mainType,c=\"series\"===h,p=n.userOutput&&n.userOutput.get();return{componentType:h,componentSubType:this.subType,componentIndex:this.componentIndex,seriesType:c?this.subType:null,seriesIndex:this.seriesIndex,seriesId:c?this.id:null,seriesName:c?this.name:null,name:o,dataIndex:r,data:a,dataType:e,value:i,color:l,borderColor:u,dimensionNames:p?p.fullDimensions:null,encode:p?p.encode:null,$vars:[\"seriesName\",\"name\",\"value\"]}},t.prototype.getFormattedLabel=function(t,e,n,i,r,o){e=e||\"normal\";var a=this.getData(n),s=this.getDataParams(t,n);(o&&(s.value=o.interpolatedValue),null!=i&&Y(s.value)&&(s.value=s.value[i]),r)||(r=a.getItemModel(t).get(\"normal\"===e?[\"label\",\"formatter\"]:[e,\"label\",\"formatter\"]));return X(r)?(s.status=e,s.dimensionIndex=i,r(s)):U(r)?mp(r,s).replace(yf,(function(e,n){var i=n.length,r=n;\"[\"===r.charAt(0)&&\"]\"===r.charAt(i-1)&&(r=+r.slice(1,i-1));var s=gf(a,t,r);if(o&&Y(o.interpolatedValue)){var l=a.getDimensionIndex(r);l>=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||p<d)){var f=this._progress;if(Y(f))for(var g=0;g<f.length;g++)this._doProgress(f[g],p,d,l,u);else this._doProgress(f,p,d,l,u)}this._dueIndex=d;var y=null!=this._settedOutputEnd?this._settedOutputEnd:d;0,this._outputDueEnd=y}else this._dueIndex=this._outputDueEnd=null!=this._settedOutputEnd?this._settedOutputEnd:this._dueEnd;return this.unfinished()},t.prototype.dirty=function(){this._dirty=!0,this._onDirty&&this._onDirty(this.context)},t.prototype._doProgress=function(t,e,n,i,r){bf.reset(e,n,i,r),this._callingProgress=t,this._callingProgress({start:e,end:n,count:n-e,next:bf.next},this.context)},t.prototype._doReset=function(t){var e,n;this._dueIndex=this._outputDueEnd=this._dueEnd=0,this._settedOutputEnd=null,!t&&this._reset&&((e=this._reset(this.context))&&e.progress&&(n=e.forceFirstProgress,e=e.progress),Y(e)&&!e.length&&(e=null)),this._progress=e,this._modBy=this._modDataCount=null;var i=this._downstream;return i&&i.dirty(),n},t.prototype.unfinished=function(){return this._progress&&this._dueIndex<this._dueEnd},t.prototype.pipe=function(t){(this._downstream!==t||this._dirty)&&(this._downstream=t,t._upstream=this,t.dirty())},t.prototype.dispose=function(){this._disposed||(this._upstream&&(this._upstream._downstream=null),this._downstream&&(this._downstream._upstream=null),this._dirty=!1,this._disposed=!0)},t.prototype.getUpstream=function(){return this._upstream},t.prototype.getDownstream=function(){return this._downstream},t.prototype.setOutputEnd=function(t){this._outputDueEnd=this._settedOutputEnd=t},t}(),bf=function(){var t,e,n,i,r,o={reset:function(l,u,h,c){e=l,t=u,n=h,i=c,r=Math.ceil(i/n),o.next=n>1&&i>0?s:a}};return o;function a(){return e<t?e++:null}function s(){var o=e%r*n+Math.ceil(e/r),a=e>=t?null:o<i?o:e;return e++,a}}();function wf(t,e){var n=e&&e.type;return\"ordinal\"===n?t:(\"time\"!==n||j(t)||null==t||\"-\"===t||(t=+ro(t)),null==t||\"\"===t?NaN:+t)}var Sf=yt({number:function(t){return parseFloat(t)},time:function(t){return+ro(t)},trim:function(t){return U(t)?ut(t):t}});function Mf(t){return Sf.get(t)}var If={lt:function(t,e){return t<e},lte:function(t,e){return t<=e},gt:function(t,e){return t>e},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 n<i?this._resultLT:n>i?-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;o<a;o++)r.push(n[o].slice());return r}if(e===Gp){for(r=[],o=0,a=n.length;o<a;o++)r.push(A({},n[o]));return r}}function Of(t,e,n){if(null!=n)return j(n)||!isNaN(n)&&!_t(e,n)?t[n]:_t(e,n)?e[n]:void 0}function Rf(t){return T(t)}var Nf=yt();function Ef(t,e,n,i){var r=\"\";e.length||vo(r),q(t)||vo(r);var o=t.type,a=Nf.get(o);a||vo(r);var s=z(e,(function(t){return function(t,e){var n=new kf,i=t.data,r=n.sourceFormat=t.sourceFormat,o=t.startIndex,a=\"\";t.seriesLayoutBy!==Xp&&vo(a);var s=[],l={},u=t.dimensionsDefine;if(u)E(u,(function(t,e){var n=t.name,i={index:e,name:n,displayName:t.displayName};if(s.push(i),null!=n){var r=\"\";_t(l,n)&&vo(r),l[n]=i}}));else for(var h=0;h<t.dimensionsDetectedCount;h++)s.push({index:h});var c=sf(r,Xp);e.__isBuiltIn&&(n.getRawDataItem=function(t){return c(i,o,s,t)},n.getRawData=W(Lf,null,t)),n.cloneRawData=W(Pf,null,t);var p=hf(r,Xp);n.count=W(p,null,i,o,s);var d=df(r);n.retrieveValue=function(t,e){var n=c(i,o,s,t);return f(n,e)};var f=n.retrieveValueFromItem=function(t,e){if(null!=t){var n=s[e];return n?d(t,e,n.name):void 0}};return n.getDimensionInfo=W(Of,null,s,l),n.cloneAllDimensionInfo=W(Rf,null,s),n}(t,a)})),l=bo(a.transform({upstream:s[0],upstreamList:s,config:T(t.config)}));return z(l,(function(t,n){var i,r=\"\";q(t)||vo(r),t.data||vo(r),zf(Qd(t.data))||vo(r);var o=e[0];if(o&&0===n&&!t.dimensions){var a=o.startIndex;a&&(t.data=o.data.slice(0,a).concat(t.data)),i={seriesLayoutBy:Xp,sourceHeader:a,dimensions:o.metaRawOption.dimensions}}else i={seriesLayoutBy:Xp,sourceHeader:0,dimensions:t.dimensions};return $d(t.data,i,null)}))}function zf(t){return t===Fp||t===Gp}var Vf,Bf=\"undefined\",Ff=typeof Uint32Array===Bf?Array:Uint32Array,Gf=typeof Uint16Array===Bf?Array:Uint16Array,Wf=typeof Int32Array===Bf?Array:Int32Array,Hf=typeof Float64Array===Bf?Array:Float64Array,Yf={float:Hf,int:Wf,ordinal:Array,number:Array,time:Hf};function Xf(t){return t>65535?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;u<s;u++)l[u]=a[u];t[e]=l}}else t[e]=new o(i)}var Zf=function(){function t(){this._chunks=[],this._rawExtent=[],this._extent=[],this._count=0,this._rawCount=0,this._calcDimNameToIdx=yt()}return t.prototype.initData=function(t,e,n){this._provider=t,this._chunks=[],this._indices=null,this.getRawIndex=this._getRawIdxIdentity;var i=t.getSource(),r=this.defaultDimValueGetter=Vf[i.sourceFormat];this._dimValueGetter=n||r,this._rawExtent=[];nf(i);this._dimensions=z(e,(function(t){return{type:t.type,property:t.property}})),this._initDataFromProvider(0,t.count())},t.prototype.getProvider=function(){return this._provider},t.prototype.getSource=function(){return this._provider.getSource()},t.prototype.ensureCalculationDimension=function(t,e){var n=this._calcDimNameToIdx,i=this._dimensions,r=n.get(t);if(null!=r){if(i[r].type===e)return r}else r=i.length;return i[r]={type:e},n.set(t,r),this._chunks[r]=new Yf[e||\"float\"](this._rawCount),this._rawExtent[r]=[1/0,-1/0],r},t.prototype.collectOrdinalMeta=function(t,e){var n=this._chunks[t],i=this._dimensions[t],r=this._rawExtent,o=i.ordinalOffset||0,a=n.length;0===o&&(r[t]=[1/0,-1/0]);for(var s=r[t],l=o;l<a;l++){var u=n[l]=e.parseAndCollect(n[l]);isNaN(u)||(s[0]=Math.min(u,s[0]),s[1]=Math.max(u,s[1]))}i.ordinalMeta=e,i.ordinalOffset=a,i.type=\"ordinal\"},t.prototype.getOrdinalMeta=function(t){return this._dimensions[t].ordinalMeta},t.prototype.getDimensionProperty=function(t){var e=this._dimensions[t];return e&&e.property},t.prototype.appendData=function(t){var e=this._provider,n=this.count();e.appendData(t);var i=e.count();return e.persistent||(i+=n),n<i&&this._initDataFromProvider(n,i,!0),[n,i]},t.prototype.appendValues=function(t,e){for(var n=this._chunks,i=this._dimensions,r=i.length,o=this._rawExtent,a=this.count(),s=a+Math.max(t.length,e||0),l=0;l<r;l++){Uf(n,l,(d=i[l]).type,s,!0)}for(var u=[],h=a;h<s;h++)for(var c=h-a,p=0;p<r;p++){var d=i[p],f=Vf.arrayRows.call(this,t[c]||u,d.property,c,p);n[p][h]=f;var g=o[p];f<g[0]&&(g[0]=f),f>g[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;u<a;u++){var h=o[u];s[u]||(s[u]=[1/0,-1/0]),Uf(r,u,h.type,e,n)}if(i.fillStorage)i.fillStorage(t,e,r,s);else for(var c=[],p=t;p<e;p++){c=i.getItem(p,c);for(var d=0;d<a;d++){var f=r[d],g=this._dimValueGetter(c,l[d],p,d);f[p]=g;var y=s[d];g<y[0]&&(y[0]=g),g>y[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<this._count))return NaN;var n=this._chunks[t];return n?n[this.getRawIndex(e)]:NaN},t.prototype.getValues=function(t,e){var n=[],i=[];if(null==e){e=t,t=[];for(var r=0;r<this._dimensions.length;r++)i.push(r)}else i=t;r=0;for(var o=i.length;r<o;r++)n.push(this.get(i[r],e));return n},t.prototype.getByRawIndex=function(t,e){if(!(e>=0&&e<this._rawCount))return NaN;var n=this._chunks[t];return n?n[e]:NaN},t.prototype.getSum=function(t){var e=0;if(this._chunks[t])for(var n=0,i=this.count();n<i;n++){var r=this.get(t,n);isNaN(r)||(e+=r)}return e},t.prototype.getMedian=function(t){var e=[];this.each([t],(function(t){isNaN(t)||e.push(t)}));var n=e.sort((function(t,e){return t-e})),i=this.count();return 0===i?0:i%2==1?n[(i-1)/2]:(n[i/2]+n[i/2-1])/2},t.prototype.indexOfRawIndex=function(t){if(t>=this._rawCount||t<0)return-1;if(!this._indices)return t;var e=this._indices,n=e[t];if(null!=n&&n<this._count&&n===t)return t;for(var i=0,r=this._count-1;i<=r;){var o=(i+r)/2|0;if(e[o]<t)i=o+1;else{if(!(e[o]>t))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<u;l++){var h=e-i[this.getRawIndex(l)],c=Math.abs(h);c<=n&&((c<o||c===o&&h>=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<i;r++)t[r]=e[r]}else t=new n(e.buffer,0,i)}else{t=new(n=Xf(this._rawCount))(this.count());for(r=0;r<t.length;r++)t[r]=r}return t},t.prototype.filter=function(t,e){if(!this._count)return this;for(var n=this.clone(),i=n.count(),r=new(Xf(n._rawCount))(i),o=[],a=t.length,s=0,l=t[0],u=n._chunks,h=0;h<i;h++){var c=void 0,p=n.getRawIndex(h);if(0===a)c=e(h);else if(1===a){c=e(u[l][p],h)}else{for(var d=0;d<a;d++)o[d]=u[t[d]][p];o[d]=h,c=e.apply(null,o)}c&&(r[s++]=p)}return s<i&&(n._indices=r),n._count=s,n._extent=[],n._updateGetRawIdx(),n},t.prototype.selectRange=function(t){var e=this.clone(),n=e._count;if(!n)return this;var i=G(t),r=i.length;if(!r)return this;var o=e.count(),a=new(Xf(e._rawCount))(o),s=0,l=i[0],u=t[l][0],h=t[l][1],c=e._chunks,p=!1;if(!e._indices){var d=0;if(1===r){for(var f=c[i[0]],g=0;g<n;g++){((x=f[g])>=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<n;g++){var x=f[g],_=y[g];(x>=u&&x<=h||isNaN(x))&&(_>=v&&_<=m||isNaN(_))&&(a[s++]=d),d++}p=!0}}if(!p)if(1===r)for(g=0;g<o;g++){var b=e.getRawIndex(g);((x=c[i[0]][b])>=u&&x<=h||isNaN(x))&&(a[s++]=b)}else for(g=0;g<o;g++){for(var w=!0,S=(b=e.getRawIndex(g),0);S<r;S++){var M=i[S];((x=c[M][b])<t[M][0]||x>t[M][1])&&(w=!1)}w&&(a[s++]=e.getRawIndex(g))}return s<o&&(e._indices=a),e._count=s,e._extent=[],e._updateGetRawIdx(),e},t.prototype.map=function(t,e){var n=this.clone(t);return this._updateDims(n,t,e),n},t.prototype.modify=function(t,e){this._updateDims(this,t,e)},t.prototype._updateDims=function(t,e,n){for(var i=t._chunks,r=[],o=e.length,a=t.count(),s=[],l=t._rawExtent,u=0;u<e.length;u++)l[e[u]]=[1/0,-1/0];for(var h=0;h<a;h++){for(var c=t.getRawIndex(h),p=0;p<o;p++)s[p]=i[e[p]][c];s[o]=h;var d=n&&n.apply(null,s);if(null!=d){\"object\"!=typeof d&&(r[0]=d,d=r);for(u=0;u<d.length;u++){var f=e[u],g=d[u],y=l[f],v=i[f];v&&(v[c]=g),g<y[0]&&(y[0]=g),g>y[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;p<s-1;p+=u){for(var d=Math.min(p+u,s-1),f=Math.min(p+2*u,s),g=(f+d)/2,y=0,v=d;v<f;v++){var m=a[I=this.getRawIndex(v)];isNaN(m)||(y+=m)}y/=f-d;var x=p,_=Math.min(p+u,s),b=p-1,w=a[h];n=-1,r=x;var S=-1,M=0;for(v=x;v<_;v++){var I;m=a[I=this.getRawIndex(v)];isNaN(m)?(M++,S<0&&(S=I)):(i=Math.abs((b-g)*(m-w)-(b-v)*(y-w)))>n&&(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;d<u;d+=s){s>u-d&&(s=u-d,a.length=s);for(var f=0;f<s;f++){var g=this.getRawIndex(d+f);a[f]=l[g]}var y=n(a),v=this.getRawIndex(Math.min(d+i(a,y)||0,u-1));l[v]=y,y<h[0]&&(h[0]=y),y>h[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();r<o;r++){var a=this.getRawIndex(r);switch(n){case 0:e(r);break;case 1:e(i[t[0]][a],r);break;case 2:e(i[t[0]][a],i[t[1]][a],r);break;default:for(var s=0,l=[];s<n;s++)l[s]=i[t[s]][a];l[s]=r,e.apply(null,l)}}},t.prototype.getDataExtent=function(t){var e=this._chunks[t],n=[1/0,-1/0];if(!e)return n;var i,r=this.count();if(!this._indices)return this._rawExtent[t].slice();if(i=this._extent[t])return i.slice();for(var o=(i=n)[0],a=i[1],s=0;s<r;s++){var l=e[this.getRawIndex(s)];l<o&&(o=l),l>a&&(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<i.length;r++)n.push(i[r][e]);return n},t.prototype.clone=function(e,n){var i,r,o=new t,a=this._chunks,s=e&&V(e,(function(t,e){return t[e]=!0,t}),{});if(s)for(var l=0;l<a.length;l++)o._chunks[l]=s[l]?(i=a[l],r=void 0,(r=i.constructor)===Array?i.slice():new r(i)):a[l];else o._chunks=a;return this._copyCommonProps(o),n||(o._indices=this._cloneIndices()),o._updateGetRawIdx(),o},t.prototype._copyCommonProps=function(t){t._count=this._count,t._rawCount=this._rawCount,t._provider=this._provider,t._dimensions=this._dimensions,t._extent=T(this._extent),t._rawExtent=T(this._rawExtent)},t.prototype._cloneIndices=function(){if(this._indices){var t=this._indices.constructor,e=void 0;if(t===Array){var n=this._indices.length;e=new t(n);for(var i=0;i<n;i++)e[i]=this._indices[i]}else e=new t(this._indices);return e}return null},t.prototype._getRawIdxIdentity=function(t){return t},t.prototype._getRawIdx=function(t){return t<this._count&&t>=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;a<s;a++)e=Ef(i[a],e),a!==s-1&&(e.length=Math.max(e.length,1));return e}(i,s,n.componentIndex):null!=r&&(e=[(a=s[0],new qd({data:a.data,sourceFormat:a.sourceFormat,seriesLayoutBy:a.seriesLayoutBy,dimensionsDefine:T(a.dimensionsDefine),startIndex:a.startIndex,dimensionsDetectedCount:a.dimensionsDetectedCount}))]),{sourceList:e,upstreamSignList:l}},t.prototype._isDirty=function(){if(this._dirty)return!0;for(var t=this._getUpstreamSourceManagers(),e=0;e<t.length;e++){var n=t[e];if(n._isDirty()||this._upstreamSignList[e]!==n._getVersionSign())return!0}},t.prototype.getSource=function(t){t=t||0;var e=this._sourceList[t];if(!e){var n=this._getUpstreamSourceManagers();return n[0]&&n[0].getSource(t)}return e},t.prototype.getSharedDataStore=function(t){var e=t.makeStoreSchema();return this._innerGetDataStore(e.dimensions,t.source,e.hash)},t.prototype._innerGetDataStore=function(t,e,n){var i=this._storeList,r=i[0];r||(r=i[0]={});var o=r[n];if(!o){var a=this._getUpstreamSourceManagers()[0];Kf(this._sourceHost)&&a?o=a._innerGetDataStore(t,e,n):(o=new Zf).initData(new rf(e,t.length),t),r[n]=o}return o},t.prototype._getUpstreamSourceManagers=function(){var t=this._sourceHost;if(Kf(t)){var e=Qp(t);return e?[e.getSourceManager()]:[]}return z(function(t){return t.get(\"transform\",!0)||t.get(\"fromTransformResult\",!0)?Bo(t.ecModel,\"dataset\",{index:t.get(\"fromDatasetIndex\",!0),id:t.get(\"fromDatasetId\",!0)},zo).models:[]}(t),(function(t){return t.getSourceManager()}))},t.prototype._getSourceMetaRawOption=function(){var t,e,n,i=this._sourceHost;if(Kf(i))t=i.get(\"seriesLayoutBy\",!0),e=i.get(\"sourceHeader\",!0),n=i.get(\"dimensions\",!0);else if(!this._getUpstreamSourceManagers().length){var r=i;t=r.get(\"seriesLayoutBy\",!0),e=r.get(\"sourceHeader\",!0),n=r.get(\"dimensions\",!0)}return{seriesLayoutBy:t,sourceHeader:e,dimensions:n}},t}();function qf(t){t.option.transform&&ct(t.option.transform)}function Kf(t){return\"series\"===t.mainType}function $f(t){throw new Error(t)}var Jf=\"line-height:1\";function Qf(t,e){var n=t.color||\"#6e7079\",i=t.fontSize||12,r=t.fontWeight||\"400\",o=t.color||\"#464646\",a=t.fontSize||14,s=t.fontWeight||\"900\";return\"html\"===e?{nameStyle:\"font-size:\"+re(i+\"\")+\"px;color:\"+re(n)+\";font-weight:\"+re(r+\"\"),valueStyle:\"font-size:\"+re(a+\"\")+\"px;color:\"+re(o)+\";font-weight:\"+re(s+\"\")}:{nameStyle:{fontSize:i,fill:n,fontWeight:r},valueStyle:{fontSize:a,fill:o,fontWeight:s}}}var tg=[0,10,20,30],eg=[\"\",\"\\n\",\"\\n\\n\",\"\\n\\n\\n\"];function ng(t,e){return e.type=t,e}function ig(t){return\"section\"===t.type}function rg(t){return ig(t)?ag:sg}function og(t){if(ig(t)){var e=0,n=t.blocks.length,i=n>1||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('<div style=\"'+f+\";\"+Jf+';\">'+re(d)+\"</div>\"+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'<span style=\"'+n+\";\"+(e?\"margin-left:2px\":\"\")+'\">'+re(t)+\"</span>\"}(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],'<span style=\"'+o+\";\"+i+'\">'+z(t,(function(t){return re(t)})).join(\"&nbsp;&nbsp;\")+\"</span>\"}(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'<div style=\"'+(\"margin: \"+e+\"px 0 0\")+\";\"+Jf+';\">'+t+'<div style=\"clear:both\"></div></div>'}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;n<t.length;n++)t[n]&&t[n].label&&wo(t[n],\"label\",e)},e.prototype.getInitialData=function(t,e){},e.prototype.appendData=function(t){this.getRawData().appendData(t.data)},e.prototype.getData=function(t){var e=Ig(this);if(e){var n=e.context.data;return null==t?n:n.getLinkedData(t)}return gg(this).data},e.prototype.getAllData=function(){var t=this.getData();return t&&t.getLinkedDataAll?t.getLinkedDataAll():[{data:t}]},e.prototype.setData=function(t){var e=Ig(this);if(e){var n=e.context;n.outputData=t,e!==this.dataTask&&(n.data=t)}gg(this).data=t},e.prototype.getEncode=function(){var t=this.get(\"encode\",!0);if(t)return yt(t)},e.prototype.getSourceManager=function(){return gg(this).sourceManager},e.prototype.getSource=function(){return this.getSourceManager().getSource()},e.prototype.getRawData=function(){return gg(this).dataBeforeProcessed},e.prototype.getColorBy=function(){return this.get(\"colorBy\")||\"series\"},e.prototype.isColorBySeries=function(){return\"series\"===this.getColorBy()},e.prototype.getBaseAxis=function(){var t=this.coordinateSystem;return t&&t.getBaseAxis&&t.getBaseAxis()},e.prototype.formatTooltip=function(t,e,n){return fg({series:this,dataIndex:t,multipleSeries:e})},e.prototype.isAnimationEnabled=function(){var t=this.ecModel;if(r.node&&(!t||!t.ssr))return!1;var e=this.getShallow(\"animation\");return e&&this.getData().count()>this.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<t.length;o++){var a=yg(r,t[o]);n[a]=!1,this._selectedDataIndicesMap[a]=-1}}},e.prototype.toggleSelect=function(t,e){for(var n=[],i=0;i<t.length;i++)n[0]=t[i],this.isSelected(t[i],e)?this.unselect(n,e):this.select(n,e)},e.prototype.getSelectedDataIndices=function(){if(\"all\"===this.option.selectedMap)return[].slice.call(this.getData().getIndices());for(var t=this._selectedDataIndicesMap,e=G(t),n=[],i=0;i<e.length;i++){var r=t[e[i]];r>=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;l<a;l++){var u=e[l];s[c=yg(t,u)]=!0,this._selectedDataIndicesMap[c]=t.getRawIndex(u)}}else if(\"single\"===o||!0===o){var h=e[a-1],c=yg(t,h);r.selectedMap=((n={})[c]=!0,n),this._selectedDataIndicesMap=((i={})[c]=t.getRawIndex(h),i)}},e.prototype._initSelectedMapFromData=function(t){if(!this.option.selectedMap){var e=[];t.hasItemOption&&t.each((function(n){var i=t.getRawDataItem(n);i&&i.selected&&e.push(n)})),e.length>0&&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<arguments.length;p++)t[p]=arguments[p];i=(new Date).getTime(),o=this,a=t;var d=s||e,f=s||n;s=null,r=i-(f?l:u)-d,clearTimeout(h),f?h=setTimeout(c,d):r>=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;o<e.end;o++)r.dataEach(i,o);else r&&r.progress&&r.progress(e,i)}}function sy(t){return t.data.count()}function ly(t){uy=null;try{t(hy,cy)}catch(t){}return uy}var uy,hy={},cy={};function py(t,e){for(var n in e.prototype)t[n]=bt}py(hy,pd),py(cy,vd),hy.eachSeriesByType=hy.eachRawSeriesByType=function(t){uy=t},hy.eachComponent=function(t){\"series\"===t.mainType&&t.subType&&(uy=t.subType)};var dy=[\"#37A2DA\",\"#32C5E9\",\"#67E0E3\",\"#9FE6B8\",\"#FFDB5C\",\"#ff9f7f\",\"#fb7293\",\"#E062AE\",\"#E690D1\",\"#e7bcf3\",\"#9d96f5\",\"#8378EA\",\"#96BFFF\"],fy={color:dy,colorLayer:[[\"#37A2DA\",\"#ffd85c\",\"#fd7b5f\"],[\"#37A2DA\",\"#67E0E3\",\"#FFDB5C\",\"#ff9f7f\",\"#E062AE\",\"#9d96f5\"],[\"#37A2DA\",\"#32C5E9\",\"#9FE6B8\",\"#FFDB5C\",\"#ff9f7f\",\"#fb7293\",\"#e7bcf3\",\"#8378EA\",\"#96BFFF\"],dy]},gy=\"#B9B8CE\",yy=\"#100C2A\",vy=function(){return{axisLine:{lineStyle:{color:gy}},splitLine:{lineStyle:{color:\"#484753\"}},splitArea:{areaStyle:{color:[\"rgba(255,255,255,0.02)\",\"rgba(255,255,255,0.05)\"]}},minorSplitLine:{lineStyle:{color:\"#20203B\"}}}},my=[\"#4992ff\",\"#7cffb2\",\"#fddd60\",\"#ff6e76\",\"#58d9f9\",\"#05c091\",\"#ff8a45\",\"#8d48e3\",\"#dd79ff\"],xy={darkMode:!0,color:my,backgroundColor:yy,axisPointer:{lineStyle:{color:\"#817f91\"},crossStyle:{color:\"#817f91\"},label:{color:\"#fff\"}},legend:{textStyle:{color:gy}},textStyle:{color:gy},title:{textStyle:{color:\"#EEF1FA\"},subtextStyle:{color:\"#B9B8CE\"}},toolbox:{iconStyle:{borderColor:gy}},dataZoom:{borderColor:\"#71708A\",textStyle:{color:gy},brushStyle:{color:\"rgba(135,163,206,0.3)\"},handleStyle:{color:\"#353450\",borderColor:\"#C5CBE3\"},moveHandleStyle:{color:\"#B0B6C3\",opacity:.3},fillerColor:\"rgba(135,163,206,0.2)\",emphasis:{handleStyle:{borderColor:\"#91B7F2\",color:\"#4D587D\"},moveHandleStyle:{color:\"#636D9A\",opacity:.7}},dataBackground:{lineStyle:{color:\"#71708A\",width:1},areaStyle:{color:\"#71708A\"}},selectedDataBackground:{lineStyle:{color:\"#87A3CE\"},areaStyle:{color:\"#87A3CE\"}}},visualMap:{textStyle:{color:gy}},timeline:{lineStyle:{color:gy},label:{color:gy},controlStyle:{color:gy,borderColor:gy}},calendar:{itemStyle:{color:yy},dayLabel:{color:gy},monthLabel:{color:gy},yearLabel:{color:gy}},timeAxis:vy(),logAxis:vy(),valueAxis:vy(),categoryAxis:vy(),line:{symbol:\"circle\"},graph:{color:my},gauge:{title:{color:gy},axisLine:{lineStyle:{color:[[1,\"rgba(207,212,219,0.2)\"]]}},axisLabel:{color:gy},detail:{color:\"#EEF1FA\"}},candlestick:{itemStyle:{color:\"#f64e56\",color0:\"#54ea92\",borderColor:\"#f64e56\",borderColor0:\"#54ea92\"}}};xy.categoryAxis.splitLine.show=!1;var _y=function(){function t(){}return t.prototype.normalizeQuery=function(t){var e={},n={},i={};if(U(t)){var r=Xo(t);e.mainType=r.main||null,e.subType=r.sub||null}else{var o=[\"Index\",\"Name\",\"Id\"],a={name:1,dataIndex:1,dataType:1};E(t,(function(t,r){for(var s=!1,l=0;l<o.length;l++){var u=o[l],h=r.lastIndexOf(u);if(h>0&&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<by.length;a++){var s=by[a],l=t.get(s);X(l)?(o=!0,r[s]=l):i[s]=l}if(i.symbol=i.symbol||t.defaultSymbol,n.setVisual(A({legendIcon:t.legendIcon||i.symbol,symbolKeepAspect:t.get(\"symbolKeepAspect\")},i)),!e.isSeriesFiltered(t)){var u=G(r);return{dataEach:o?function(e,n){for(var i=t.getRawValue(n),o=t.getDataParams(n),a=0;a<u.length;a++){var s=u[a];e.setItemVisual(n,s,r[s](i,o))}}:null}}}}},My={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){if(t.hasSymbolVisual&&!e.isSeriesFiltered(t))return{dataEach:t.getData().hasItemOption?function(t,e){for(var n=t.getItemModel(e),i=0;i<wy.length;i++){var r=wy[i],o=n.getShallow(r,!0);null!=o&&t.setItemVisual(e,r,o)}}:null}}};function Iy(t,e,n){switch(n){case\"color\":return t.getItemVisual(e,\"style\")[t.getVisual(\"drawType\")];case\"opacity\":return t.getItemVisual(e,\"style\").opacity;case\"symbol\":case\"symbolSize\":case\"liftZ\":return t.getItemVisual(e,n)}}function Ty(t,e){switch(e){case\"color\":return t.getVisual(\"style\")[t.getVisual(\"drawType\")];case\"opacity\":return t.getVisual(\"style\").opacity;case\"symbol\":case\"symbolSize\":case\"liftZ\":return t.getVisual(e)}}function Cy(t,e,n,i){switch(n){case\"color\":t.ensureUniqueItemVisual(e,\"style\")[t.getVisual(\"drawType\")]=i,t.setItemVisual(e,\"colorFromPalette\",!1);break;case\"opacity\":t.ensureUniqueItemVisual(e,\"style\").opacity=i;break;case\"symbol\":case\"symbolSize\":case\"liftZ\":t.setItemVisual(e,n,i)}}function Dy(t,e){function n(e,n){var i=[];return e.eachComponent({mainType:\"series\",subType:t,query:n},(function(t){i.push(t.seriesIndex)})),i}E([[t+\"ToggleSelect\",\"toggleSelect\"],[t+\"Select\",\"select\"],[t+\"UnSelect\",\"unselect\"]],(function(t){e(t[0],(function(e,i,r){e=A({},e),r.dispatchAction(A(e,{type:t[1],seriesIndex:n(i,e)}))}))}))}function Ay(t,e,n,i,r){var o=t+e;n.isSilent(o)||i.eachComponent({mainType:\"series\",subType:\"pie\"},(function(t){for(var e=t.seriesIndex,i=t.option.selectedMap,a=r.selected,s=0;s<a.length;s++)if(a[s].seriesIndex===e){var l=t.getData(),u=Po(l,r.fromActionPayload);n.trigger(o,{type:o,seriesId:t.id,name:Y(u)?l.getName(u[0]):l.getName(u),selected:U(i)?i:A({},i)})}}))}function ky(t,e,n){for(var i;t&&(!e(t)||(i=t,!n));)t=t.__hostTarget||t.parent;return i}var Ly=Math.round(9*Math.random()),Py=\"function\"==typeof Object.defineProperty,Oy=function(){function t(){this._id=\"__ec_inner_\"+Ly++}return t.prototype.get=function(t){return this._guard(t)[this._id]},t.prototype.set=function(t,e){var n=this._guard(t);return Py?Object.defineProperty(n,this._id,{value:e,enumerable:!1,configurable:!0}):n[this._id]=e,this},t.prototype.delete=function(t){return!!this.has(t)&&(delete this._guard(t)[this._id],!0)},t.prototype.has=function(t){return!!this._guard(t)[this._id]},t.prototype._guard=function(t){if(t!==Object(t))throw TypeError(\"Value of WeakMap is not a non-null object.\");return t},t}(),Ry=Is.extend({type:\"triangle\",shape:{cx:0,cy:0,width:0,height:0},buildPath:function(t,e){var n=e.cx,i=e.cy,r=e.width/2,o=e.height/2;t.moveTo(n,i-o),t.lineTo(n+r,i+o),t.lineTo(n-r,i+o),t.closePath()}}),Ny=Is.extend({type:\"diamond\",shape:{cx:0,cy:0,width:0,height:0},buildPath:function(t,e){var n=e.cx,i=e.cy,r=e.width/2,o=e.height/2;t.moveTo(n,i-o),t.lineTo(n+r,i),t.lineTo(n,i+o),t.lineTo(n-r,i),t.closePath()}}),Ey=Is.extend({type:\"pin\",shape:{x:0,y:0,width:0,height:0},buildPath:function(t,e){var n=e.x,i=e.y,r=e.width/5*3,o=Math.max(r,e.height),a=r/2,s=a*a/(o-a),l=i-o+a+s,u=Math.asin(s/a),h=Math.cos(u)*a,c=Math.sin(u),p=Math.cos(u),d=.6*a,f=.7*a;t.moveTo(n-h,l+s),t.arc(n,l,a,Math.PI-u,2*Math.PI+u),t.bezierCurveTo(n+h-c*d,l+s+p*d,n,i-f,n,i),t.bezierCurveTo(n,i-f,n-h+c*d,l+s+p*d,n-h,l+s),t.closePath()}}),zy=Is.extend({type:\"arrow\",shape:{x:0,y:0,width:0,height:0},buildPath:function(t,e){var n=e.height,i=e.width,r=e.x,o=e.y,a=i/3*2;t.moveTo(r,o),t.lineTo(r+a,o+n),t.lineTo(r,o+n/4*3),t.lineTo(r-a,o+n),t.lineTo(r,o),t.closePath()}}),Vy={line:function(t,e,n,i,r){r.x1=t,r.y1=e+i/2,r.x2=t+n,r.y2=e+i/2},rect:function(t,e,n,i,r){r.x=t,r.y=e,r.width=n,r.height=i},roundRect:function(t,e,n,i,r){r.x=t,r.y=e,r.width=n,r.height=i,r.r=Math.min(n,i)/4},square:function(t,e,n,i,r){var o=Math.min(n,i);r.x=t,r.y=e,r.width=o,r.height=o},circle:function(t,e,n,i,r){r.cx=t+n/2,r.cy=e+i/2,r.r=Math.min(n,i)/2},diamond:function(t,e,n,i,r){r.cx=t+n/2,r.cy=e+i/2,r.width=n,r.height=i},pin:function(t,e,n,i,r){r.x=t+n/2,r.y=e+i/2,r.width=n,r.height=i},arrow:function(t,e,n,i,r){r.x=t+n/2,r.y=e+i/2,r.width=n,r.height=i},triangle:function(t,e,n,i,r){r.cx=t+n/2,r.cy=e+i/2,r.width=n,r.height=i}},By={};E({line:Zu,rect:zs,roundRect:zs,square:zs,circle:_u,diamond:Ny,pin:Ey,arrow:zy,triangle:Ry},(function(t,e){By[e]=new t}));var Fy=Is.extend({type:\"symbol\",shape:{symbolType:\"\",x:0,y:0,width:0,height:0},calculateTextPosition:function(t,e,n){var i=Tr(t,e,n),r=this.shape;return r&&\"pin\"===r.symbolType&&\"inside\"===e.position&&(i.y=n.y+.4*n.height),i},buildPath:function(t,e,n){var i=e.symbolType;if(\"none\"!==i){var r=By[i];r||(r=By[i=\"rect\"]),Vy[i](e.x,e.y,e.width,e.height,r.shape),r.buildPath(t,r.shape,n)}}});function Gy(t,e){if(\"image\"!==this.type){var n=this.style;this.__isEmptyBrush?(n.stroke=t,n.fill=e||\"#fff\",n.lineWidth=2):\"line\"===this.shape.symbolType?n.stroke=t:n.fill=t,this.markRedraw()}}function Wy(t,e,n,i,r,o,a){var s,l=0===t.indexOf(\"empty\");return l&&(t=t.substr(5,1).toLowerCase()+t.substr(6)),(s=0===t.indexOf(\"image://\")?kh(t.slice(8),new ze(e,n,i,r),a?\"center\":\"cover\"):0===t.indexOf(\"path://\")?Ah(t.slice(7),{},new ze(e,n,i,r),a?\"center\":\"cover\"):new Fy({shape:{symbolType:t,x:e,y:n,width:i,height:r}})).__isEmptyBrush=l,s.setColor=Gy,o&&s.setColor(o),s}function Hy(t){return Y(t)||(t=[+t,+t]),[t[0]||0,t[1]||0]}function Yy(t,e){if(null!=t)return Y(t)||(t=[t,t]),[Ur(t[0],e[0])||0,Ur(rt(t[1],t[0]),e[1])||0]}function Xy(t){return isFinite(t)}function Uy(t,e,n){for(var i=\"radial\"===e.type?function(t,e,n){var i=n.width,r=n.height,o=Math.min(i,r),a=null==e.x?.5:e.x,s=null==e.y?.5:e.y,l=null==e.r?.5:e.r;return e.global||(a=a*i+n.x,s=s*r+n.y,l*=o),a=Xy(a)?a:.5,s=Xy(s)?s:.5,l=l>=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;o<r.length;o++)i.addColorStop(r[o].offset,r[o].color);return i}function Zy(t){return parseInt(t,10)}function jy(t,e,n){var i=[\"width\",\"height\"][e],r=[\"clientWidth\",\"clientHeight\"][e],o=[\"paddingLeft\",\"paddingTop\"][e],a=[\"paddingRight\",\"paddingBottom\"][e];if(null!=n[i]&&\"auto\"!==n[i])return parseFloat(n[i]);var s=document.defaultView.getComputedStyle(t);return(t[r]||Zy(s[i])||Zy(t.style[i]))-(Zy(s[o])||0)-(Zy(s[a])||0)|0}function qy(t){var e,n,i=t.style,r=i.lineDash&&i.lineWidth>0&&(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;s<iv.length;s++){var l=iv[s];(i||e[l]!==n[l])&&(o||(lv(t,r),o=!0),t[l]=t.dpr*(e[l]||0))}return(i||e.shadowColor!==n.shadowColor)&&(o||(lv(t,r),o=!0),t.shadowColor=e.shadowColor||xa.shadowColor),o}function av(t,e,n,i,r){var o=uv(e,r.inHover),a=i?null:n&&uv(n,r.inHover)||{};if(o===a)return!1;var s=ov(t,o,a,i,r);if((i||o.fill!==a.fill)&&(s||(lv(t,r),s=!0),Jy(o.fill)&&(t.fillStyle=o.fill)),(i||o.stroke!==a.stroke)&&(s||(lv(t,r),s=!0),Jy(o.stroke)&&(t.strokeStyle=o.stroke)),(i||o.opacity!==a.opacity)&&(s||(lv(t,r),s=!0),t.globalAlpha=null==o.opacity?1:o.opacity),e.hasStroke()){var l=o.lineWidth/(o.strokeNoScale&&e.getLineScale?e.getLineScale():1);t.lineWidth!==l&&(s||(lv(t,r),s=!0),t.lineWidth=l)}for(var u=0;u<rv.length;u++){var h=rv[u],c=h[0];(i||o[c]!==a[c])&&(s||(lv(t,r),s=!0),t[c]=o[c]||h[1])}return s}function sv(t,e){var n=e.transform,i=t.dpr||1;n?t.setTransform(i*n[0],i*n[1],i*n[2],i*n[3],i*n[4],i*n[5]):t.setTransform(i,0,0,i,0,0)}function lv(t,e){e.batchFill&&t.fill(),e.batchStroke&&t.stroke(),e.batchFill=\"\",e.batchStroke=\"\"}function uv(t,e){return e&&t.__hoverStyle||t.style}function hv(t,e){cv(t,e,{inHover:!1,viewWidth:0,viewHeight:0},!0)}function cv(t,e,n,i){var r=e.transform;if(!e.shouldBePainted(n.viewWidth,n.viewHeight,!1,!1))return e.__dirty&=-2,void(e.__isRendered=!1);var o=e.__clipPaths,s=n.prevElClipPaths,l=!1,u=!1;if(s&&!function(t,e){if(t===e||!t&&!e)return!1;if(!t||!e||t.length!==e.length)return!0;for(var n=0;n<t.length;n++)if(t[n]!==e[n])return!0;return!1}(o,s)||(s&&s.length&&(lv(t,n),t.restore(),u=l=!0,n.prevElClipPaths=null,n.allClipped=!1,n.prevEl=null),o&&o.length&&(lv(t,n),t.save(),function(t,e,n){for(var i=!1,r=0;r<t.length;r++){var o=t[r];i=i||o.isZeroArea(),sv(e,o),e.beginPath(),o.buildPath(e,o.shape),e.clip()}n.allClipped=i}(o,t,n),l=!0),n.prevElClipPaths=o),n.allClipped)e.__isRendered=!1;else{e.beforeBrush&&e.beforeBrush(),e.innerBeforeBrush();var h=n.prevEl;h||(u=l=!0);var c,p,d=e instanceof Is&&e.autoBatch&&function(t){var e=Qy(t),n=$y(t);return!(t.lineDash||!(+e^+n)||e&&\"string\"!=typeof t.fill||n&&\"string\"!=typeof t.stroke||t.strokePercent<1||t.strokeOpacity<1||t.fillOpacity<1)}(e.style);l||(c=r,p=h.transform,c&&p?c[0]!==p[0]||c[1]!==p[1]||c[2]!==p[2]||c[3]!==p[3]||c[4]!==p[4]||c[5]!==p[5]:c||p)?(lv(t,n),sv(t,e)):d||lv(t,n);var f=uv(e,n.inHover);e instanceof Is?(1!==n.lastDrawType&&(u=!0,n.lastDrawType=1),av(t,e,h,u,n),d&&(n.batchFill||n.batchStroke)||t.beginPath(),function(t,e,n,i){var r,o=$y(n),a=Qy(n),s=n.strokePercent,l=s<1,u=!e.path;e.silent&&!l||!u||e.createPathProxy();var h=e.path||Ky,c=e.__dirty;if(!i){var p=n.fill,d=n.stroke,f=a&&!!p.colorStops,g=o&&!!d.colorStops,y=a&&!!p.image,v=o&&!!d.image,m=void 0,x=void 0,_=void 0,b=void 0,w=void 0;(f||g)&&(w=e.getBoundingRect()),f&&(m=c?Uy(t,p,w):e.__canvasFillGradient,e.__canvasFillGradient=m),g&&(x=c?Uy(t,d,w):e.__canvasStrokeGradient,e.__canvasStrokeGradient=x),y&&(_=c||!e.__canvasFillPattern?nv(t,p,e):e.__canvasFillPattern,e.__canvasFillPattern=_),v&&(b=c||!e.__canvasStrokePattern?nv(t,d,e):e.__canvasStrokePattern,e.__canvasStrokePattern=_),f?t.fillStyle=m:y&&(_?t.fillStyle=_:a=!1),g?t.strokeStyle=x:v&&(b?t.strokeStyle=b:o=!1)}var S,M,I=e.getGlobalScale();h.setScale(I[0],I[1],e.segmentIgnoreThreshold),t.setLineDash&&n.lineDash&&(S=(r=qy(e))[0],M=r[1]);var T=!0;(u||4&c)&&(h.setDPR(t.dpr),l?h.setContext(null):(h.setContext(t),T=!1),h.reset(),e.buildPath(h,e.shape,i),h.toStatic(),e.pathUpdated()),T&&h.rebuildPath(t,l?s:1),S&&(t.setLineDash(S),t.lineDashOffset=M),i||(n.strokeFirst?(o&&ev(t,n),a&&tv(t,n)):(a&&tv(t,n),o&&ev(t,n))),S&&t.setLineDash([])}(t,e,f,d),d&&(n.batchFill=f.fill||\"\",n.batchStroke=f.stroke||\"\")):e instanceof Cs?(3!==n.lastDrawType&&(u=!0,n.lastDrawType=3),av(t,e,h,u,n),function(t,e,n){var i,r=n.text;if(null!=r&&(r+=\"\"),r){t.font=n.font||a,t.textAlign=n.textAlign,t.textBaseline=n.textBaseline;var o=void 0,s=void 0;t.setLineDash&&n.lineDash&&(o=(i=qy(e))[0],s=i[1]),o&&(t.setLineDash(o),t.lineDashOffset=s),n.strokeFirst?($y(n)&&t.strokeText(r,n.x,n.y),Qy(n)&&t.fillText(r,n.x,n.y)):(Qy(n)&&t.fillText(r,n.x,n.y),$y(n)&&t.strokeText(r,n.x,n.y)),o&&t.setLineDash([])}}(t,e,f)):e instanceof ks?(2!==n.lastDrawType&&(u=!0,n.lastDrawType=2),function(t,e,n,i,r){ov(t,uv(e,r.inHover),n&&uv(n,r.inHover),i,r)}(t,e,h,u,n),function(t,e,n){var i=e.__image=ia(n.image,e.__image,e,e.onload);if(i&&oa(i)){var r=n.x||0,o=n.y||0,a=e.getWidth(),s=e.getHeight(),l=i.width/i.height;if(null==a&&null!=s?a=s*l:null==s&&null!=a?s=a/l:null==a&&null==s&&(a=i.width,s=i.height),n.sWidth&&n.sHeight){var u=n.sx||0,h=n.sy||0;t.drawImage(i,u,h,n.sWidth,n.sHeight,r,o,a,s)}else if(n.sx&&n.sy){var c=a-(u=n.sx),p=s-(h=n.sy);t.drawImage(i,u,h,c,p,r,o,a,s)}else t.drawImage(i,r,o,a,s)}}(t,e,f)):e.getTemporalDisplayables&&(4!==n.lastDrawType&&(u=!0,n.lastDrawType=4),function(t,e,n){var i=e.getDisplayables(),r=e.getTemporalDisplayables();t.save();var o,a,s={prevElClipPaths:null,prevEl:null,allClipped:!1,viewWidth:n.viewWidth,viewHeight:n.viewHeight,inHover:n.inHover};for(o=e.getCursor(),a=i.length;o<a;o++){(h=i[o]).beforeBrush&&h.beforeBrush(),h.innerBeforeBrush(),cv(t,h,s,o===a-1),h.innerAfterBrush(),h.afterBrush&&h.afterBrush(),s.prevEl=h}for(var l=0,u=r.length;l<u;l++){var h;(h=r[l]).beforeBrush&&h.beforeBrush(),h.innerBeforeBrush(),cv(t,h,s,l===u-1),h.innerAfterBrush(),h.afterBrush&&h.afterBrush(),s.prevEl=h}e.clearTemporalDisplayables(),e.notClear=!0,t.restore()}(t,e,n)),d&&i&&lv(t,n),e.innerAfterBrush(),e.afterBrush&&e.afterBrush(),n.prevEl=e,e.__dirty=0,e.__isRendered=!0}}var pv=new Oy,dv=new En(100),fv=[\"symbol\",\"symbolSize\",\"symbolKeepAspect\",\"color\",\"backgroundColor\",\"dashArrayX\",\"dashArrayY\",\"maxTileWidth\",\"maxTileHeight\"];function gv(t,e){if(\"none\"===t)return null;var n=e.getDevicePixelRatio(),i=e.getZr(),r=\"svg\"===i.painter.type;t.dirty&&pv.delete(t);var o=pv.get(t);if(o)return o;var a=k(t,{symbol:\"rect\",symbolSize:1,symbolKeepAspect:!0,color:\"rgba(0, 0, 0, 0.2)\",backgroundColor:null,dashArrayX:5,dashArrayY:5,rotation:0,maxTileWidth:512,maxTileHeight:512});\"none\"===a.backgroundColor&&(a.backgroundColor=null);var s={repeat:\"repeat\"};return function(t){for(var e,o=[n],s=!0,l=0;l<fv.length;++l){var u=a[fv[l]];if(null!=u&&!Y(u)&&!U(u)&&!j(u)&&\"boolean\"!=typeof u){s=!1;break}o.push(u)}if(s){e=o.join(\",\")+(r?\"-svg\":\"\");var c=dv.get(e);c&&(r?t.svgElement=c:t.image=c)}var p,d=vv(a.dashArrayX),f=function(t){if(!t||\"object\"==typeof t&&0===t.length)return[0,0];if(j(t)){var e=Math.ceil(t);return[e,e]}var n=z(t,(function(t){return Math.ceil(t)}));return t.length%2?n.concat(n):n}(a.dashArrayY),g=yv(a.symbol),y=(b=d,z(b,(function(t){return mv(t)}))),v=mv(f),m=!r&&h.createCanvas(),x=r&&{tag:\"g\",attrs:{},key:\"dcl\",children:[]},_=function(){for(var t=1,e=0,n=y.length;e<n;++e)t=go(t,y[e]);var i=1;for(e=0,n=g.length;e<n;++e)i=go(i,g[e].length);t*=i;var r=v*y.length*g.length;return{width:Math.max(1,Math.min(t,a.maxTileWidth)),height:Math.max(1,Math.min(r,a.maxTileHeight))}}();var b;m&&(m.width=_.width*n,m.height=_.height*n,p=m.getContext(\"2d\"));(function(){p&&(p.clearRect(0,0,m.width,m.height),a.backgroundColor&&(p.fillStyle=a.backgroundColor,p.fillRect(0,0,m.width,m.height)));for(var t=0,e=0;e<f.length;++e)t+=f[e];if(t<=0)return;var o=-v,s=0,l=0,u=0;for(;o<_.height;){if(s%2==0){for(var h=l/2%g.length,c=0,y=0,b=0;c<2*_.width;){var w=0;for(e=0;e<d[u].length;++e)w+=d[u][e];if(w<=0)break;if(y%2==0){var S=.5*(1-a.symbolSize),M=c+d[u][y]*S,I=o+f[s]*S,T=d[u][y]*a.symbolSize,C=f[s]*a.symbolSize,D=b/2%g[h].length;A(M,I,T,C,g[h][D])}c+=d[u][y],++b,++y===d[u].length&&(y=0)}++u===d.length&&(u=0)}o+=f[s],++l,++s===f.length&&(s=0)}function A(t,e,o,s,l){var u=r?1:n,h=Wy(l,t*u,e*u,o*u,s*u,a.color,a.symbolKeepAspect);if(r){var c=i.painter.renderOneToVNode(h);c&&x.children.push(c)}else hv(p,h)}})(),s&&dv.put(e,m||x);t.image=m,t.svgElement=x,t.svgWidth=_.width,t.svgHeight=_.height}(s),s.rotation=a.rotation,s.scaleX=s.scaleY=r?1:1/n,pv.set(t,s),t.dirty=!1,s}function yv(t){if(!t||0===t.length)return[[\"rect\"]];if(U(t))return[[t]];for(var e=!0,n=0;n<t.length;++n)if(!U(t[n])){e=!1;break}if(e)return yv([t]);var i=[];for(n=0;n<t.length;++n)U(t[n])?i.push([t[n]]):i.push(t[n]);return i}function vv(t){if(!t||0===t.length)return[[0,0]];if(j(t))return[[r=Math.ceil(t),r]];for(var e=!0,n=0;n<t.length;++n)if(!j(t[n])){e=!1;break}if(e)return vv([t]);var i=[];for(n=0;n<t.length;++n)if(j(t[n])){var r=Math.ceil(t[n]);i.push([r,r])}else{(r=z(t[n],(function(t){return Math.ceil(t)}))).length%2==1?i.push(r.concat(r)):i.push(r)}return i}function mv(t){for(var e=0,n=0;n<t.length;++n)e+=t[n];return t.length%2==1?2*e:e}var xv=new jt,_v={};function bv(t){return _v[t]}var wv=2e3,Sv=4500,Mv={PROCESSOR:{FILTER:1e3,SERIES_FILTER:800,STATISTIC:5e3},VISUAL:{LAYOUT:1e3,PROGRESSIVE_LAYOUT:1100,GLOBAL:wv,CHART:3e3,POST_CHART_LAYOUT:4600,COMPONENT:4e3,BRUSH:5e3,CHART_ITEM:Sv,ARIA:6e3,DECAL:7e3}},Iv=\"__flagInMainProcess\",Tv=\"__pendingUpdate\",Cv=\"__needsUpdateStatus\",Dv=/^[a-zA-Z0-9_]+$/,Av=\"__connectUpdateStatus\";function kv(t){return function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];if(!this.isDisposed())return Pv(this,t,e);nm(this.id)}}function Lv(t){return function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return Pv(this,t,e)}}function Pv(t,e,n){return n[0]=n[0]&&n[0].toLowerCase(),jt.prototype[e].apply(t,n)}var Ov,Rv,Nv,Ev,zv,Vv,Bv,Fv,Gv,Wv,Hv,Yv,Xv,Uv,Zv,jv,qv,Kv,$v=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e}(jt),Jv=$v.prototype;Jv.on=Lv(\"on\"),Jv.off=Lv(\"off\");var Qv=function(t){function e(e,n,i){var r=t.call(this,new _y)||this;r._chartsViews=[],r._chartsMap={},r._componentsViews=[],r._componentsMap={},r._pendingActions=[],i=i||{},U(n)&&(n=lm[n]),r._dom=e;var o=\"canvas\",a=\"auto\",s=!1,l=r._zr=Gr(e,{renderer:i.renderer||o,devicePixelRatio:i.devicePixelRatio,width:i.width,height:i.height,ssr:i.ssr,useDirtyRect:rt(i.useDirtyRect,s),useCoarsePointer:rt(i.useCoarsePointer,a),pointerSize:i.pointerSize});r._ssr=i.ssr,r._throttledZrFlush=Bg(W(l.flush,l),17),(n=T(n))&&Wd(n,!0),r._theme=n,r._locale=function(t){if(U(t)){var e=Lc[t.toUpperCase()]||{};return t===Dc||t===Ac?T(e):C(T(e),T(Lc[kc]),!1)}return C(T(t),T(Lc[kc]),!1)}(i.locale||Oc),r._coordSysMgr=new xd;var u=r._api=Zv(r);function h(t,e){return t.__prio-e.__prio}return Qe(sm,h),Qe(om,h),r._scheduler=new Jg(r,u,om,sm),r._messageCenter=new $v,r._initEvents(),r.resize=W(r.resize,r),l.animation.on(\"frame\",r._onframe,r),Wv(l,r),Hv(l,r),ct(r),r}return n(e,t),e.prototype._onframe=function(){if(!this._disposed){Kv(this);var t=this._scheduler;if(this[Tv]){var e=this[Tv].silent;this[Iv]=!0;try{Ov(this),Ev.update.call(this,null,this[Tv].updateParams)}catch(t){throw this[Iv]=!1,this[Tv]=null,t}this._zr.flush(),this[Iv]=!1,this[Tv]=null,Fv.call(this,e),Gv.call(this,e)}else if(t.unfinished){var n=1,i=this._model,r=this._api;t.unfinished=!1;do{var o=+new Date;t.performSeriesTasks(i),t.performDataProcessorTasks(i),Vv(this,i),t.performVisualTasks(i),Uv(this,this._model,r,\"remain\",{}),n-=+new Date-o}while(n>0&&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+='<g transform=\"translate('+e+\",\"+n+')\">'+t.dom+\"</g>\"})),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;i<n.length;i++){var r=n[i];\"emphasis\"!==r&&\"blur\"!==r&&\"select\"!==r&&e.push(r)}t.selected&&t.states.select&&e.push(\"select\"),2===t.hoverState&&t.states.emphasis?e.push(\"emphasis\"):1===t.hoverState&&t.states.blur&&e.push(\"blur\"),t.useStates(e)}function i(t,e){if(!t.preventAutoZ){var n=t.get(\"z\")||0,i=t.get(\"zlevel\")||0;e.eachRendered((function(t){return o(t,n,i,-1/0),!0}))}}function o(t,e,n,i){var r=t.getTextContent(),a=t.getTextGuideLine();if(t.isGroup)for(var s=t.childrenRef(),l=0;l<s.length;l++)i=Math.max(o(s[l],e,n,i),i);else t.z=e,t.zlevel=n,i=Math.max(t.z2,i);if(r&&(r.z=e,r.zlevel=n,isFinite(i)&&(r.z2=i+2)),a){var u=t.textGuideLineConfig;a.z=e,a.zlevel=n,isFinite(i)&&(a.z2=i+(u&&u.showAbove?1:-1))}return i}function a(t,e){e.eachRendered((function(t){if(!yh(t)){var e=t.getTextContent(),n=t.getTextGuideLine();t.stateTransition&&(t.stateTransition=null),e&&e.stateTransition&&(e.stateTransition=null),n&&n.stateTransition&&(n.stateTransition=null),t.hasState()?(t.prevStates=t.currentStates,t.clearStates()):t.prevStates&&(t.prevStates=null)}}))}function s(t,n){var i=t.getModel(\"stateAnimation\"),r=t.isAnimationEnabled(),o=i.get(\"duration\"),a=o>0?{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;l<r.length;l++)r[l].__alive=!1;function u(t){var l=t.__requireNewView;t.__requireNewView=!1;var u=\"_ec_\"+t.id+\"_\"+t.type,h=!l&&o[u];if(!h){var c=Xo(t.type),p=e?Tg.getClass(c.main,c.sub):kg.getClass(c.sub);0,(h=new p).init(n,s),o[u]=h,r.push(h),a.add(h.group)}t.__viewId=h.__id=u,h.__alive=!0,h.__model=t,h.group.__ecComponentInfo={mainType:t.mainType,index:t.componentIndex},!e&&i.prepareView(h,t,n,s)}e?n.eachComponent((function(t,e){\"series\"!==t&&u(e)})):n.eachSeries(u);for(l=0;l<r.length;){var h=r[l];h.__alive?l++:(!e&&h.renderTask.dispose(),a.remove(h.group),h.dispose(n,s),r.splice(l,1),o[h.__id]===h&&delete o[h.__id],h.__id=h.group.__ecComponentInfo=null)}},Nv=function(t,e,n,i,r){var o=t._model;if(o.setUpdatePayload(n),i){var a={};a[i+\"Id\"]=n[i+\"Id\"],a[i+\"Index\"]=n[i+\"Index\"],a[i+\"Name\"]=n[i+\"Name\"];var s={mainType:i,query:a};r&&(s.subType=r);var l,u=n.excludeSeriesId;null!=u&&(l=yt(),E(bo(u),(function(t){var e=Ao(t,null);null!=e&&l.set(e,!0)}))),o&&o.eachComponent(s,(function(e){if(!(l&&null!=l.get(e.id)))if(Jl(n))if(e instanceof mg)n.type!==ll||n.notBlur||e.get([\"emphasis\",\"disabled\"])||function(t,e,n){var i=t.seriesIndex,r=t.getData(e.dataType);if(r){var o=Po(r,e);o=(Y(o)?o[0]:o)||0;var a=r.getItemGraphicEl(o);if(!a)for(var s=r.count(),l=0;!a&&l<s;)a=r.getItemGraphicEl(l++);if(a){var u=Qs(a);Vl(i,u.focus,u.blurScope,n)}else{var h=t.get([\"emphasis\",\"focus\"]),c=t.get([\"emphasis\",\"blurScope\"]);null!=h&&Vl(i,h,c,n)}}}(e,n,t._api);else{var i=Fl(e.mainType,e.componentIndex,n.name,t._api),r=i.focusSelf,o=i.dispatchers;n.type===ll&&r&&!n.notBlur&&Bl(e.mainType,e.componentIndex,t._api),o&&E(o,(function(t){n.type===ll?kl(t):Ll(t)}))}else $l(n)&&e instanceof mg&&(!function(t,e,n){if($l(e)){var i=e.dataType,r=Po(t.getData(i),e);Y(r)||(r=[r]),t[e.type===pl?\"toggleSelect\":e.type===hl?\"select\":\"unselect\"](r,i)}}(e,n,t._api),Gl(e),qv(t))}),t),o&&o.eachComponent(s,(function(e){l&&null!=l.get(e.id)||h(t[\"series\"===i?\"_chartsMap\":\"_componentsMap\"][e.__viewId])}),t)}else E([].concat(t._componentsViews).concat(t._chartsViews),h);function h(i){i&&i.__alive&&i[e]&&i[e](i.__model,o,t._api,n)}},Ev={prepareAndUpdate:function(t){Ov(this),Ev.update.call(this,t,{optionChanged:null!=t.newOption})},update:function(e,n){var i=this._model,r=this._api,o=this._zr,a=this._coordSysMgr,s=this._scheduler;if(i){i.setUpdatePayload(e),s.restoreData(i,e),s.performSeriesTasks(i),a.create(i,r),s.performDataProcessorTasks(i,e),Vv(this,i),a.update(i,r),t(i),s.performVisualTasks(i,e),Yv(this,i,r,e,n);var l=i.get(\"backgroundColor\")||\"transparent\",u=i.get(\"darkMode\");o.setBackgroundColor(l),null!=u&&\"auto\"!==u&&o.setDarkMode(u),xv.trigger(\"afterupdate\",i,r)}},updateTransform:function(e){var n=this,i=this._model,r=this._api;if(i){i.setUpdatePayload(e);var o=[];i.eachComponent((function(t,a){if(\"series\"!==t){var s=n.getViewOfComponentModel(a);if(s&&s.__alive)if(s.updateTransform){var l=s.updateTransform(a,i,r,e);l&&l.update&&o.push(s)}else o.push(s)}}));var a=yt();i.eachSeries((function(t){var o=n._chartsMap[t.__viewId];if(o.updateTransform){var s=o.updateTransform(t,i,r,e);s&&s.update&&a.set(t.uid,1)}else a.set(t.uid,1)})),t(i),this._scheduler.performVisualTasks(i,e,{setDirty:!0,dirtyMap:a}),Uv(this,i,r,e,{},a),xv.trigger(\"afterupdate\",i,r)}},updateView:function(e){var n=this._model;n&&(n.setUpdatePayload(e),kg.markUpdateMethod(e,\"updateView\"),t(n),this._scheduler.performVisualTasks(n,e,{setDirty:!0}),Yv(this,n,this._api,e,{}),xv.trigger(\"afterupdate\",n,this._api))},updateVisual:function(e){var n=this,i=this._model;i&&(i.setUpdatePayload(e),i.eachSeries((function(t){t.getData().clearAllVisual()})),kg.markUpdateMethod(e,\"updateVisual\"),t(i),this._scheduler.performVisualTasks(i,e,{visualType:\"visual\",setDirty:!0}),i.eachComponent((function(t,r){if(\"series\"!==t){var o=n.getViewOfComponentModel(r);o&&o.__alive&&o.updateVisual(r,i,n._api,e)}})),i.eachSeries((function(t){n._chartsMap[t.__viewId].updateVisual(t,i,n._api,e)})),xv.trigger(\"afterupdate\",i,this._api))},updateLayout:function(t){Ev.update.call(this,t)}},zv=function(t,e,n,i){if(t._disposed)nm(t.id);else{for(var r,o=t._model,a=t._coordSysMgr.getCoordinateSystems(),s=No(o,n),l=0;l<a.length;l++){var u=a[l];if(u[e]&&null!=(r=u[e](o,s,i)))return r}0}},Vv=function(t,e){var n=t._chartsMap,i=t._scheduler;e.eachSeries((function(t){i.updateStreamModes(t,n[t.__viewId])}))},Bv=function(t,e){var n=this,i=this.getModel(),r=t.type,o=t.escapeConnect,a=im[r],s=a.actionInfo,l=(s.update||\"update\").split(\":\"),u=l.pop(),h=null!=l[0]&&Xo(l[0]);this[Iv]=!0;var c=[t],p=!1;t.batch&&(p=!0,c=z(t.batch,(function(e){return(e=k(A({},e),t)).batch=null,e})));var d,f=[],g=$l(t),y=Jl(t);if(y&&zl(this._api),E(c,(function(e){if((d=(d=a.action(e,n._model,n._api))||A({},e)).type=s.event||d.type,f.push(d),y){var i=Eo(t),r=i.queryOptionMap,o=i.mainTypeSpecified?r.keys()[0]:\"series\";Nv(n,u,e,o),qv(n)}else g?(Nv(n,u,e,\"series\"),qv(n)):h&&Nv(n,u,e,h.main,h.sub)})),\"none\"!==u&&!y&&!g&&!h)try{this[Tv]?(Ov(this),Ev.update.call(this,t),this[Tv]=null):Ev[u].call(this,t)}catch(t){throw this[Iv]=!1,t}if(d=p?{type:s.event||r,escapeConnect:o,batch:f}:f[0],this[Iv]=!1,!e){var v=this._messageCenter;if(v.trigger(d.type,d),g){var m={type:\"selectchanged\",escapeConnect:o,selected:Wl(i),isFromClick:t.isFromClick||!1,fromAction:t.type,fromActionPayload:t};v.trigger(m.type,m)}}},Fv=function(t){for(var e=this._pendingActions;e.length;){var n=e.shift();Bv.call(this,n,t)}},Gv=function(t){!t&&this.trigger(\"updated\")},Wv=function(t,e){t.on(\"rendered\",(function(n){e.trigger(\"rendered\",n),!t.animation.isFinished()||e[Tv]||e._scheduler.unfinished||e._pendingActions.length||e.trigger(\"finished\")}))},Hv=function(t,e){t.on(\"mouseover\",(function(t){var n=ky(t.target,Kl);n&&(!function(t,e,n){var i=Qs(t),r=Fl(i.componentMainType,i.componentIndex,i.componentHighDownName,n),o=r.dispatchers,a=r.focusSelf;o?(a&&Bl(i.componentMainType,i.componentIndex,n),E(o,(function(t){return Dl(t,e)}))):(Vl(i.seriesIndex,i.focus,i.blurScope,n),\"self\"===i.focus&&Bl(i.componentMainType,i.componentIndex,n),Dl(t,e))}(n,t,e._api),qv(e))})).on(\"mouseout\",(function(t){var n=ky(t.target,Kl);n&&(!function(t,e,n){zl(n);var i=Qs(t),r=Fl(i.componentMainType,i.componentIndex,i.componentHighDownName,n).dispatchers;r?E(r,(function(t){return Al(t,e)})):Al(t,e)}(n,t,e._api),qv(e))})).on(\"click\",(function(t){var n=ky(t.target,(function(t){return null!=Qs(t).dataIndex}),!0);if(n){var i=n.selected?\"unselect\":\"select\",r=Qs(n);e._api.dispatchAction({type:i,dataType:r.dataType,dataIndexInside:r.dataIndex,seriesIndex:r.seriesIndex,isFromClick:!0})}}))},Yv=function(t,e,n,i,r){!function(t){var e=[],n=[],i=!1;if(t.eachComponent((function(t,r){var o=r.get(\"zlevel\")||0,a=r.get(\"z\")||0,s=r.getZLevelKey();i=i||!!s,(\"series\"===t?n:e).push({zlevel:o,z:a,idx:r.componentIndex,type:t,key:s})})),i){var r,o,a=e.concat(n);Qe(a,(function(t,e){return t.zlevel===e.zlevel?t.z-e.z:t.zlevel-e.zlevel})),E(a,(function(e){var n=t.getComponent(e.type,e.idx),i=e.zlevel,a=e.key;null!=r&&(i=Math.max(r,i)),a?(i===r&&a!==o&&i++,o=a):o&&(i===r&&i++,o=\"\"),r=i,n.setZLevel(i)}))}}(e),Xv(t,e,n,i,r),E(t._chartsViews,(function(t){t.__alive=!1})),Uv(t,e,n,i,r),E(t._chartsViews,(function(t){t.__alive||t.remove(e,n)}))},Xv=function(t,e,n,r,o,l){E(l||t._componentsViews,(function(t){var o=t.__model;a(o,t),t.render(o,e,n,r),i(o,t),s(o,t)}))},Uv=function(t,e,n,o,l,u){var h=t._scheduler;l=A(l||{},{updatedSeries:e.getSeries()}),xv.trigger(\"series:beforeupdate\",e,n,l);var c=!1;e.eachSeries((function(e){var n=t._chartsMap[e.__viewId];n.__alive=!0;var i=n.renderTask;h.updatePayload(i,o),a(e,n),u&&u.get(e.uid)&&i.dirty(),i.perform(h.getPerformArgs(i))&&(c=!0),n.group.silent=!!e.get(\"silent\"),function(t,e){var n=t.get(\"blendMode\")||null;e.eachRendered((function(t){t.isGroup||(t.style.blend=n)}))}(e,n),Gl(e)})),h.unfinished=c||h.unfinished,xv.trigger(\"series:layoutlabels\",e,n,l),xv.trigger(\"series:transition\",e,n,l),e.eachSeries((function(e){var n=t._chartsMap[e.__viewId];i(e,n),s(e,n)})),function(t,e){var n=t._zr,i=n.storage,o=0;i.traverse((function(t){t.isGroup||o++})),o>e.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<t.length;n++){t[n][Av]=e}}E(rm,(function(n,i){t._messageCenter.on(i,(function(n){if(cm[t.group]&&0!==t[Av]){if(n&&n.escapeConnect)return;var i=t.makeActionFromEvent(n),r=[];E(hm,(function(e){e!==t&&e.group===t.group&&r.push(e)})),e(r,0),E(r,(function(t){1!==t[Av]&&t.dispatchAction(i)})),e(r,2)}}))}))}}(),e}(jt),tm=Qv.prototype;tm.on=kv(\"on\"),tm.off=kv(\"off\"),tm.one=function(t,e,n){var i=this;yo(),this.on.call(this,t,(function n(){for(var r=[],o=0;o<arguments.length;o++)r[o]=arguments[o];e&&e.apply&&e.apply(this,r),i.off(t,n)}),n)};var em=[\"click\",\"dblclick\",\"mouseover\",\"mouseout\",\"mousemove\",\"mousedown\",\"mouseup\",\"globalout\",\"contextmenu\"];function nm(t){0}var im={},rm={},om=[],am=[],sm=[],lm={},um={},hm={},cm={},pm=+new Date-0,dm=+new Date-0,fm=\"_echarts_instance_\";function gm(t){cm[t]=!1}var ym=gm;function vm(t){return hm[function(t,e){return t.getAttribute?t.getAttribute(e):t[e]}(t,fm)]}function mm(t,e){lm[t]=e}function xm(t){P(am,t)<0&&am.push(t)}function _m(t,e){Am(om,t,e,2e3)}function bm(t){Sm(\"afterinit\",t)}function wm(t){Sm(\"afterupdate\",t)}function Sm(t,e){xv.on(t,e)}function Mm(t,e,n){X(e)&&(n=e,e=\"\");var i=q(t)?t.type:[t,t={event:e}][0];t.event=(t.event||i).toLowerCase(),e=t.event,rm[e]||(lt(Dv.test(i)&&Dv.test(e)),im[i]||(im[i]={action:n,actionInfo:t}),rm[e]=i)}function Im(t,e){xd.register(t,e)}function Tm(t,e){Am(sm,t,e,1e3,\"layout\")}function Cm(t,e){Am(sm,t,e,3e3,\"visual\")}var Dm=[];function Am(t,e,n,i,r){if((X(e)||q(e))&&(n=e,e=i),!(P(Dm,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;o<t.length;o++){var a=i[o],s=n[a],l=Em(s);if(l>1){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;a<r.length;a++){var s=r[a],l=n[s],u=i[s],h=Em(l),c=Em(u);if(h>1&&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;p<h;p++)this._remove&&this._remove(l[p]);else this._remove&&this._remove(l)}this._performRestAdd(o,i)},t.prototype._performRestAdd=function(t,e){for(var n=0;n<t.length;n++){var i=t[n],r=e[i],o=Em(r);if(o>1)for(var a=0;a<o;a++)this._add&&this._add(r[a]);else 1===o&&this._add&&this._add(r);e[i]=null}},t.prototype._initIndexMap=function(t,e,n,i){for(var r=this._diffModeMultiple,o=0;o<t.length;o++){var a=\"_ec_\"+this[i](t[o],o);if(r||(n[o]=a),e){var s=e[a],l=Em(s);0===l?(e[a]=o,r&&n.push(a)):1===l?e[a]=[s,o]:s.push(o)}}},t}(),Bm=function(){function t(t,e){this._encode=t,this._schema=e}return t.prototype.get=function(){return{fullDimensions:this._getFullDimensionNames(),encode:this._encode}},t.prototype._getFullDimensionNames=function(){return this._cachedDimNames||(this._cachedDimNames=this._schema?this._schema.makeOutputDimensionNames():[]),this._cachedDimNames},t}();function Fm(t,e){return t.hasOwnProperty(e)||(t[e]=[]),t[e]}function Gm(t){return\"category\"===t?\"ordinal\":\"time\"===t?\"time\":\"float\"}var Wm=function(t){this.otherDims={},null!=t&&A(this,t)},Hm=Oo(),Ym={float:\"f\",int:\"i\",ordinal:\"o\",number:\"n\",time:\"t\"},Xm=function(){function t(t){this.dimensions=t.dimensions,this._dimOmitted=t.dimensionOmitted,this.source=t.source,this._fullDimCount=t.fullDimensionCount,this._updateDimOmitted(t.dimensionOmitted)}return t.prototype.isDimensionOmitted=function(){return this._dimOmitted},t.prototype._updateDimOmitted=function(t){this._dimOmitted=t,t&&(this._dimNameMap||(this._dimNameMap=jm(this.source)))},t.prototype.getSourceDimensionIndex=function(t){return rt(this._dimNameMap.get(t),-1)},t.prototype.getSourceDimension=function(t){var e=this.source.dimensionsDefine;if(e)return e[t]},t.prototype.makeStoreSchema=function(){for(var t=this._fullDimCount,e=nf(this.source),n=!qm(t),i=\"\",r=[],o=0,a=0;o<t;o++){var s=void 0,l=void 0,u=void 0,h=this.dimensions[a];if(h&&h.storeDimIndex===o)s=e?h.name:null,l=h.type,u=h.ordinalMeta,a++;else{var c=this.getSourceDimension(o);c&&(s=e?c.name:null,l=c.type)}r.push({property:s,type:l,ordinalMeta:u}),!e||null==s||h&&h.isCalculationCoord||(i+=n?s.replace(/\\`/g,\"`1\").replace(/\\$/g,\"`2\"):s),i+=\"$\",i+=Ym[l]||\"f\",u&&(i+=u.uid),i+=\"$\"}var p=this.source;return{dimensions:r,hash:[p.seriesLayoutBy,p.startIndex,i].join(\"$$\")}},t.prototype.makeOutputDimensionNames=function(){for(var t=[],e=0,n=0;e<this._fullDimCount;e++){var i=void 0,r=this.dimensions[n];if(r&&r.storeDimIndex===e)r.isCalculationCoord||(i=r.name),n++;else{var o=this.getSourceDimension(e);o&&(i=o.name)}t.push(i)}return t},t.prototype.appendCalculationDimension=function(t){this.dimensions.push(t),t.isCalculationCoord=!0,this._fullDimCount++,this._updateDimOmitted(!0)},t}();function Um(t){return t instanceof Xm}function Zm(t){for(var e=yt(),n=0;n<(t||[]).length;n++){var i=t[n],r=q(i)?i.name:i;null!=r&&null==e.get(r)&&e.set(r,n)}return e}function jm(t){var e=Hm(t);return e.dimNameMap||(e.dimNameMap=Zm(t.dimensionsDefine))}function qm(t){return t>30}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<n.length;u++){var h=n[u],c=U(h)?new Wm({name:h}):h instanceof Wm?h:new Wm(h),p=c.name;c.type=c.type||\"float\",c.coordDim||(c.coordDim=p,c.coordDimIndex=0);var d=c.otherDims=c.otherDims||{};o.push(p),r[p]=c,null!=l[p]&&(s=!0),c.createInvertedIndices&&(a[p]=[]),0===d.itemName&&(this._nameDimIdx=u),0===d.itemId&&(this._idDimIdx=u),i&&(c.storeDimIndex=u)}if(this.dimensions=o,this._dimInfos=r,this._initGetDimensionInfo(s),this.hostModel=e,this._invertedIndicesMap=a,this._dimOmitted){var f=this._dimIdxToName=yt();E(o,(function(t){f.set(r[t].storeDimIndex,t)}))}}return t.prototype.getDimension=function(t){var e=this._recognizeDimIndex(t);if(null==e)return t;if(e=t,!this._dimOmitted)return this.dimensions[e];var n=this._dimIdxToName.get(e);if(null!=n)return n;var i=this._schema.getSourceDimension(e);return i?i.name:void 0},t.prototype.getDimensionIndex=function(t){var e=this._recognizeDimIndex(t);if(null!=e)return e;if(null==t)return-1;var n=this._getDimInfo(t);return n?n.storeDimIndex:this._dimOmitted?this._schema.getSourceDimensionIndex(t):-1},t.prototype._recognizeDimIndex=function(t){if(j(t)||null!=t&&!isNaN(t)&&!this._getDimInfo(t)&&(!this._dimOmitted||this._schema.getSourceDimensionIndex(t)<0))return+t},t.prototype._getStoreDimIndex=function(t){var e=this.getDimensionIndex(t);return e},t.prototype.getDimensionInfo=function(t){return this._getDimInfo(this.getDimension(t))},t.prototype._initGetDimensionInfo=function(t){var e=this._dimInfos;this._getDimInfo=t?function(t){return e.hasOwnProperty(t)?e[t]:void 0}:function(t){return e[t]}},t.prototype.getDimensionsOnCoord=function(){return this._dimSummary.dataDimsOnCoord.slice()},t.prototype.mapDimension=function(t,e){var n=this._dimSummary;if(null==e)return n.encodeFirstDimNotExtra[t];var i=n.encode[t];return i?i[e]:null},t.prototype.mapDimensionsAll=function(t){return(this._dimSummary.encode[t]||[]).slice()},t.prototype.getStore=function(){return this._store},t.prototype.initData=function(t,e,n){var i,r=this;if(t instanceof Zf&&(i=t),!i){var o=this.dimensions,a=Kd(t)||N(t)?new rf(t,o.length):t;i=new Zf;var s=rx(o,(function(t){return{type:r._dimInfos[t].type,property:t}}));i.initData(a,s,n)}this._store=i,this._nameList=(e||[]).slice(),this._idList=[],this._nameRepeatCount={},this._doInit(0,i.count()),this._dimSummary=function(t,e){var n={},i=n.encode={},r=yt(),o=[],a=[],s={};E(t.dimensions,(function(e){var n,l=t.getDimensionInfo(e),u=l.coordDim;if(u){var h=l.coordDimIndex;Fm(i,u)[h]=e,l.isExtraCoord||(r.set(u,1),\"ordinal\"!==(n=l.type)&&\"time\"!==n&&(o[0]=e),Fm(s,u)[h]=t.getDimensionIndex(l.name)),l.defaultTooltip&&a.push(e)}Vp.each((function(t,e){var n=Fm(i,e),r=l.otherDims[e];null!=r&&!1!==r&&(n[r]=l.name)}))}));var l=[],u={};r.each((function(t,e){var n=i[e];u[e]=n[0],l=l.concat(n)})),n.dataDimsOnCoord=l,n.dataDimIndicesOnCoord=z(l,(function(e){return t.getDimensionInfo(e).storeDimIndex})),n.encodeFirstDimNotExtra=u;var h=i.label;h&&h.length&&(o=h.slice());var c=i.tooltip;return c&&c.length?a=c.slice():a.length||(a=o.slice()),i.defaultedLabel=o,i.defaultedTooltip=a,n.userOutput=new Bm(s,e),n}(this,this._schema),this.userOutput=this._dimSummary.userOutput},t.prototype.appendData=function(t){var e=this._store.appendData(t);this._doInit(e[0],e[1])},t.prototype.appendValues=function(t,e){var n=this._store.appendValues(t,e.length),i=n.start,r=n.end,o=this._shouldMakeIdFromName();if(this._updateOrdinalMeta(),e)for(var a=i;a<r;a++){var s=a-i;this._nameList[a]=e[s],o&&nx(this,a)}},t.prototype._updateOrdinalMeta=function(){for(var t=this._store,e=this.dimensions,n=0;n<e.length;n++){var i=this._dimInfos[e[n]];i.ordinalMeta&&t.collectOrdinalMeta(i.storeDimIndex,i.ordinalMeta)}},t.prototype._shouldMakeIdFromName=function(){var t=this._store.getProvider();return null==this._idDimIdx&&t.getSource().sourceFormat!==Hp&&!t.fillStorage},t.prototype._doInit=function(t,e){if(!(t>=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;a<e;a++){var s=n.getItem(a,o);if(!this.hasItemOption&&Io(s)&&(this.hasItemOption=!0),s){var l=s.name;null==i[a]&&null!=l&&(i[a]=Ao(l,null));var u=s.id;null==r[a]&&null!=u&&(r[a]=Ao(u,null))}}if(this._shouldMakeIdFromName())for(a=t;a<e;a++)nx(this,a);Km(this)}},t.prototype.getApproximateExtent=function(t){return this._approximateExtent[t]||this._store.getDataExtent(this._getStoreDimIndex(t))},t.prototype.setApproximateExtent=function(t,e){e=this.getDimension(e),this._approximateExtent[e]=t.slice()},t.prototype.getCalculationInfo=function(t){return this._calculationInfo[t]},t.prototype.setCalculationInfo=function(t,e){ix(t)?A(this._calculationInfo,t):this._calculationInfo[t]=e},t.prototype.getName=function(t){var e=this.getRawIndex(t),n=this._nameList[e];return null==n&&null!=this._nameDimIdx&&(n=Jm(this,this._nameDimIdx,e)),null==n&&(n=\"\"),n},t.prototype._getCategory=function(t,e){var n=this._store.get(t,e),i=this._store.getOrdinalMeta(t);return i?i.categories[n]:n},t.prototype.getId=function(t){return $m(this,this.getRawIndex(t))},t.prototype.count=function(){return this._store.count()},t.prototype.get=function(t,e){var n=this._store,i=this._dimInfos[t];if(i)return n.get(i.storeDimIndex,e)},t.prototype.getByRawIndex=function(t,e){var n=this._store,i=this._dimInfos[t];if(i)return n.getByRawIndex(i.storeDimIndex,e)},t.prototype.getIndices=function(){return this._store.getIndices()},t.prototype.getDataExtent=function(t){return this._store.getDataExtent(this._getStoreDimIndex(t))},t.prototype.getSum=function(t){return this._store.getSum(this._getStoreDimIndex(t))},t.prototype.getMedian=function(t){return this._store.getMedian(this._getStoreDimIndex(t))},t.prototype.getValues=function(t,e){var n=this,i=this._store;return Y(t)?i.getValues(rx(t,(function(t){return n._getStoreDimIndex(t)})),e):i.getValues(t)},t.prototype.hasValue=function(t){for(var e=this._dimSummary.dataDimIndicesOnCoord,n=0,i=e.length;n<i;n++)if(isNaN(this._store.get(e[n],t)))return!1;return!0},t.prototype.indexOfName=function(t){for(var e=0,n=this._store.count();e<n;e++)if(this.getName(e)===t)return e;return-1},t.prototype.getRawIndex=function(t){return this._store.getRawIndex(t)},t.prototype.indexOfRawIndex=function(t){return this._store.indexOfRawIndex(t)},t.prototype.rawIndexOf=function(t,e){var n=t&&this._invertedIndicesMap[t];var i=n[e];return null==i||isNaN(i)?-1:i},t.prototype.indicesOfNearest=function(t,e,n){return this._store.indicesOfNearest(this._getStoreDimIndex(t),e,n)},t.prototype.each=function(t,e,n){X(t)&&(n=e,e=t,t=[]);var i=n||this,r=rx(Qm(t),this._getStoreDimIndex,this);this._store.each(r,i?W(e,i):e)},t.prototype.filterSelf=function(t,e,n){X(t)&&(n=e,e=t,t=[]);var i=n||this,r=rx(Qm(t),this._getStoreDimIndex,this);return this._store=this._store.filter(r,i?W(e,i):e),this},t.prototype.selectRange=function(t){var e=this,n={};return E(G(t),(function(i){var r=e._getStoreDimIndex(i);n[r]=t[i]})),this._store=this._store.selectRange(n),this},t.prototype.mapArray=function(t,e,n){X(t)&&(n=e,e=t,t=[]),n=n||this;var i=[];return this.each(t,(function(){i.push(e&&e.apply(this,arguments))}),n),i},t.prototype.map=function(t,e,n,i){var r=n||i||this,o=rx(Qm(t),this._getStoreDimIndex,this),a=ex(this);return a._store=this._store.map(o,r?W(e,r):e),a},t.prototype.modify=function(t,e,n,i){var r=n||i||this;var o=rx(Qm(t),this._getStoreDimIndex,this);this._store.modify(o,r?W(e,r):e)},t.prototype.downSample=function(t,e,n,i){var r=ex(this);return r._store=this._store.downSample(this._getStoreDimIndex(t),e,n,i),r},t.prototype.lttbDownSample=function(t,e){var n=ex(this);return n._store=this._store.lttbDownSample(this._getStoreDimIndex(t),e),n},t.prototype.getRawDataItem=function(t){return this._store.getRawDataItem(t)},t.prototype.getItemModel=function(t){var e=this.hostModel,n=this.getRawDataItem(t);return new Mc(n,e,e&&e.ecModel)},t.prototype.diff=function(t){var e=this;return new Vm(t?t.getStore().getIndices():[],this.getStore().getIndices(),(function(e){return $m(t,e)}),(function(t){return $m(e,t)}))},t.prototype.getVisual=function(t){var e=this._visual;return e&&e[t]},t.prototype.setVisual=function(t,e){this._visual=this._visual||{},ix(t)?A(this._visual,t):this._visual[t]=e},t.prototype.getItemVisual=function(t,e){var n=this._itemVisuals[t],i=n&&n[e];return null==i?this.getVisual(e):i},t.prototype.hasItemVisual=function(){return this._itemVisuals.length>0},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;s<n.length;s++)n[s]=-1;for(s=0;s<a.count();s++)n[a.get(r.storeDimIndex,s)]=s}}))},Jm=function(t,e,n){return Ao(t._getCategory(e,n),null)},$m=function(t,e){var n=t._idList[e];return null==n&&null!=t._idDimIdx&&(n=Jm(t,t._idDimIdx,e)),null==n&&(n=\"e\\0\\0\"+e),n},Qm=function(t){return Y(t)||(t=null!=t?[t]:[]),t},ex=function(e){var n=new t(e._schema?e._schema:rx(e.dimensions,e._getDimInfo,e),e.hostModel);return tx(n,e),n},tx=function(t,e){E(ax.concat(e.__wrappedMethods||[]),(function(n){e.hasOwnProperty(n)&&(t[n]=e[n])})),t.__wrappedMethods=e.__wrappedMethods,E(sx,(function(n){t[n]=T(e[n])})),t._calculationInfo=A({},e._calculationInfo)},void(nx=function(t,e){var n=t._nameList,i=t._idList,r=t._nameDimIdx,o=t._idDimIdx,a=n[e],s=i[e];if(null==a&&null!=r&&(n[e]=a=Jm(t,r,e)),null==s&&null!=o&&(i[e]=s=Jm(t,o,e)),null==s&&null!=a){var l=t._nameRepeatCount,u=l[a]=(l[a]||0)+1;s=a,u>1&&(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;d<p.length;d++)p[d]=-1;function f(t){var e=p[t];if(e<0){var n=i[t],r=q(n)?n:{name:n},a=new Wm,s=r.name;null!=s&&null!=u.get(s)&&(a.name=a.displayName=s),null!=r.type&&(a.type=r.type),null!=r.displayName&&(a.displayName=r.displayName);var l=o.length;return p[t]=l,a.storeDimIndex=t,o.push(a),a}return o[e]}if(!s)for(d=0;d<a;d++)f(d);c.each((function(t,e){var n=bo(t).slice();if(1===n.length&&!U(n[0])&&n[0]<0)c.set(e,!1);else{var i=c.set(e,[]);E(n,(function(t,n){var r=U(t)?u.get(t):t;null!=r&&r<a&&(i[n]=r,y(f(r),e,n))}))}}));var g=0;function y(t,e,n){null!=Vp.get(e)?t.otherDims[e]=n:(t.coordDim=e,t.coordDimIndex=n,r.set(e,!0))}E(n,(function(t){var e,n,i,r;if(U(t))e=t,r={};else{e=(r=t).name;var o=r.ordinalMeta;r.ordinalMeta=null,(r=A({},r)).ordinalMeta=o,n=r.dimsDef,i=r.otherDims,r.name=r.coordDim=r.coordDimIndex=r.dimsDef=r.otherDims=null}var s=c.get(e);if(!1!==s){if(!(s=bo(s)).length)for(var u=0;u<(n&&n.length||1);u++){for(;g<a&&null!=f(g).coordDim;)g++;g<a&&s.push(g++)}E(s,(function(t,o){var a=f(t);if(l&&null!=r.type&&(a.type=r.type),y(k(a,r),e,o),null==a.name&&n){var s=n[o];!q(s)&&(s={name:s}),a.name=a.displayName=s.name,a.defaultTooltip=s.defaultTooltip}i&&k(a.otherDims,i)}))}}));var v=e.generateCoord,m=e.generateCoordCount,x=null!=m;m=v?m||1:0;var _=v||\"value\";function b(t){null==t.name&&(t.name=t.coordDim)}if(s)E(o,(function(t){b(t)})),o.sort((function(t,e){return t.storeDimIndex-e.storeDimIndex}));else for(var w=0;w<a;w++){var S=f(w);null==S.coordDim&&(S.coordDim=hx(_,r,x),S.coordDimIndex=0,(!v||m<=0)&&(S.isExtraCoord=!0),m--),b(S),null!=S.type||td(t,w)!==Zp&&(!S.isExtraCoord||null==S.otherDims.itemName&&null==S.otherDims.seriesName)||(S.type=\"ordinal\")}return function(t){for(var e=yt(),n=0;n<t.length;n++){var i=t[n],r=i.name,o=e.get(r)||0;o>0&&(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(;e<t.length&&null==t[e];)e++;return t[e]}(t.data||[]);return!Y(Mo(e))}}(i)?function(t,e,n,i){return i===c?n:this.defaultDimValueGetter(t,e,n,i)}:null;return f.hasItemOption=!1,f.initData(o?i:p,null,g),f}var mx=function(){function t(t){this._setting=t||{},this._extent=[1/0,-1/0]}return t.prototype.getSetting=function(t){return this._setting[t]},t.prototype.unionExtent=function(t){var e=this._extent;t[0]<e[0]&&(e[0]=t[0]),t[1]>e[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&&a<n&&(a=r.interval=n),null!=i&&a>i&&(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<a;++r){var s=e[r];n[r]=s,i[s]=r}for(var l=0;r<o;++r){for(;null!=i[l];)l++;n.push(l),i[l]=r}}else this._ordinalNumbersByTick=this._ticksByOrdinalNumber=null},e.prototype._getTickNumber=function(t){var e=this._ticksByOrdinalNumber;return e&&t>=0&&t<e.length?e[t]:t},e.prototype.getRawOrdinalNumber=function(t){var e=this._ordinalNumbersByTick;return e&&t>=0&&t<e.length?e[t]:t},e.prototype.getLabel=function(t){if(!this.isBlank()){var e=this.getRawOrdinalNumber(t.value),n=this._ordinalMeta.categories[e];return null==n?\"\":n+\"\"}},e.prototype.count=function(){return this._extent[1]-this._extent[0]+1},e.prototype.unionExtentFromData=function(t,e){this.unionExtent(t.getApproximateExtent(e))},e.prototype.isInExtentRange=function(t){return t=this._getTickNumber(t),this._extent[0]<=t&&this._extent[1]>=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[0]&&(e[0]=t[0]),t[1]>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]<i[0]&&(t?o.push({value:Px(i[0]-e,r)}):o.push({value:n[0]}));for(var a=i[0];a<=i[1]&&(o.push({value:a}),(a=Px(a+e,r))!==o[o.length-1].value);)if(o.length>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;r<e.length;r++){for(var o=e[r],a=e[r-1],s=0,l=[],u=(o.value-a.value)/t;s<t-1;){var h=Px(a.value+(s+1)*u);h>i[0]&&h<i[1]&&l.push(h),s++}n.push(l)}return n},e.prototype.getLabel=function(t,e){if(null==t)return\"\";var n=e&&e.precision;return null==n?n=qr(t.value)||0:\"auto\"===n&&(n=this._intervalPrecision),pp(Px(t.value,n,!0))},e.prototype.calcNiceTicks=function(t,e,n){t=t||5;var i=this._extent,r=i[1]-i[0];if(isFinite(r)){r<0&&(r=-r,i.reverse());var o=Mx(i,t,e,n);this._intervalPrecision=o.intervalPrecision,this._interval=o.interval,this._niceExtent=o.niceTickExtent}},e.prototype.calcNiceExtent=function(t){var e=this._extent;if(e[0]===e[1])if(0!==e[0]){var n=Math.abs(e[0]);t.fixMax||(e[1]+=n/2),e[0]-=n/2}else e[1]=1;var i=e[1]-e[0];isFinite(i)||(e[0]=0,e[1]=1),this.calcNiceTicks(t.splitNumber,t.minInterval,t.maxInterval);var r=this._interval;t.fixMin||(e[0]=Px(Math.floor(e[0]/r)*r)),t.fixMax||(e[1]=Px(Math.ceil(e[1]/r)*r))},e.prototype.setNiceExtent=function(t,e){this._niceExtent=[t,e]},e.type=\"interval\",e}(mx);mx.registerClass(Ox);var Rx=\"undefined\"!=typeof Float32Array,Nx=Rx?Float32Array:Array;function Ex(t){return Y(t)?Rx?new Float32Array(t):t:new Nx(t)}var zx=\"__ec_stack_\";function Vx(t){return t.get(\"stack\")||zx+t.seriesIndex}function Bx(t){return t.dim+t.index}function Fx(t,e){var n=[];return e.eachSeriesByType(t,(function(t){Xx(t)&&n.push(t)})),n}function Gx(t){var e=function(t){var e={};E(t,(function(t){var n=t.coordinateSystem.getBaseAxis();if(\"time\"===n.type||\"value\"===n.type)for(var i=t.getData(),r=n.dim+\"_\"+n.index,o=i.getDimensionIndex(i.mapDimension(n.dim)),a=i.getStore(),s=0,l=a.count();s<l;++s){var u=a.get(o,s);e[r]?e[r].push(u):e[r]=[u]}}));var n={};for(var i in e)if(e.hasOwnProperty(i)){var r=e[i];if(r){r.sort((function(t,e){return t-e}));for(var o=null,a=1;a<r.length;++a){var s=r[a]-r[a-1];s>0&&(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&&e<i&&(i=Math.min(e,u)),n&&n>i&&(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)<f&&(A=(A<0?-1:1)*f)}else{L=n.dataToPoint([M,S]);if(h)I=n.dataToPoint([M,T])[1];C=L[0]+v,D=I,A=y,k=L[1]-I,Math.abs(k)<f&&(k=(k<=0?-1:1)*f)}d?(l[w]=C,l[w+1]=D,l[w+2]=c?A:k,u&&(u[w]=c?x.x:C,u[w+1]=c?D:x.y,u[w+2]=_),m[i]=i):e.setItemLayout(i,{x:C,y:D,width:A,height:k}),w+=3}d&&e.setLayout({largePoints:l,largeDataIndices:m,largeBackgroundPoints:u,valueAxisHorizontal:c})}}}}}}function Xx(t){return t.coordinateSystem&&\"cartesian2d\"===t.coordinateSystem.type}function Ux(t){return t.pipelineContext&&t.pipelineContext.large}var Zx=function(t){function e(e){var n=t.call(this,e)||this;return n.type=\"time\",n}return n(e,t),e.prototype.getLabel=function(t){var e=this.getSetting(\"useUTC\");return qc(t.value,Hc[function(t){switch(t){case\"year\":case\"month\":return\"day\";case\"millisecond\":return\"millisecond\";default:return\"second\"}}(Zc(this._minLevelUnit))]||Hc.second,e,this.getSetting(\"locale\"))},e.prototype.getFormattedLabel=function(t,e,n){var i=this.getSetting(\"useUTC\");return function(t,e,n,i,r){var o=null;if(U(n))o=n;else if(X(n))o=n(t.value,e,{level:t.level});else{var a=A({},Gc);if(t.level>0)for(var s=0;s<Yc.length;++s)a[Yc[s]]=\"{primary|\"+a[Yc[s]]+\"}\";var l=n?!1===n.inherit?n:k(n,a):a,u=Kc(t.value,r);if(l[u])o=l[u];else if(l.inherit){for(s=Xc.indexOf(u)-1;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]();u<n&&u<=i[1];)s.push({value:u}),h+=t,l[o](h),u=l.getTime();s.push({value:u,notAdd:!0})}function l(t,r,o){var a=[],l=!r.length;if(!function(t,e,n,i){var r=ro(e),o=ro(n),a=function(t){return $c(r,t,i)===$c(o,t,i)},s=function(){return a(\"year\")},l=function(){return s()&&a(\"month\")},u=function(){return l()&&a(\"day\")},h=function(){return u()&&a(\"hour\")},c=function(){return h()&&a(\"minute\")},p=function(){return c()&&a(\"second\")},d=function(){return p()&&a(\"millisecond\")};switch(t){case\"year\":return s();case\"month\":return l();case\"day\":return u();case\"hour\":return h();case\"minute\":return c();case\"second\":return p();case\"millisecond\":return d()}}(Zc(t),i[0],i[1],n)){l&&(r=[{value:t_(new Date(i[0]),t,n)},{value:i[1]}]);for(var u=0;u<r.length-1;u++){var h=r[u].value,c=r[u+1].value;if(h!==c){var p=void 0,d=void 0,f=void 0,g=!1;switch(t){case\"year\":p=Math.max(1,Math.round(e/Bc/365)),d=Jc(n),f=op(n);break;case\"half-year\":case\"quarter\":case\"month\":p=Kx(e),d=Qc(n),f=ap(n);break;case\"week\":case\"half-week\":case\"day\":p=qx(e),d=tp(n),f=sp(n),g=!0;break;case\"half-day\":case\"quarter-day\":case\"hour\":p=$x(e),d=ep(n),f=lp(n);break;case\"minute\":p=Jx(e,!0),d=np(n),f=up(n);break;case\"second\":p=Jx(e,!1),d=ip(n),f=hp(n);break;case\"millisecond\":p=Qx(e),d=rp(n),f=cp(n)}s(p,h,c,d,f,g,a),\"year\"===t&&o.length>1&&0===u&&o.unshift({value:o[0].value-p})}}for(u=0;u<a.length;u++)o.push(a[u]);return a}}for(var u=[],h=[],c=0,p=0,d=0;d<o.length&&a++<r;++d){var f=Zc(o[d]);if(jc(o[d]))if(l(o[d],u[u.length-1]||[],h),f!==(o[d+1]?Zc(o[d+1]):null)){if(h.length){p=c,h.sort((function(t,e){return t.value-e.value}));for(var g=[],y=0;y<h.length;++y){var v=h[y].value;0!==y&&h[y-1].value===v||(g.push(h[y]),v>=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;d<x.length;++d)for(var w=x[d],S=0;S<w.length;++S)_.push({value:w[S].value,level:b-d});_.sort((function(t,e){return t.value-e.value}));var M=[];for(d=0;d<_.length;++d)0!==d&&_[d].value===_[d-1].value||M.push(_[d]);return M}(this._minLevelUnit,this._approxInterval,i,e);return(n=n.concat(r)).push({value:e[1],level:0}),n},e.prototype.calcNiceExtent=function(t){var e=this._extent;if(e[0]===e[1]&&(e[0]-=Bc,e[1]+=Bc),e[1]===-1/0&&e[0]===1/0){var n=new Date;e[1]=+new Date(n.getFullYear(),n.getMonth(),n.getDate()),e[0]=e[1]-Bc}this.calcNiceTicks(t.splitNumber,t.minInterval,t.maxInterval)},e.prototype.calcNiceTicks=function(t,e,n){t=t||10;var i=this._extent,r=i[1]-i[0];this._approxInterval=r/t,null!=e&&this._approxInterval<e&&(this._approxInterval=e),null!=n&&this._approxInterval>n&&(this._approxInterval=n);var o=jx.length,a=Math.min(function(t,e,n,i){for(;n<i;){var r=n+i>>>1;t[r][1]<e?n=r+1:i=r}return n}(jx,this._approxInterval,0,o),o-1);this._interval=jx[a][1],this._minLevelUnit=jx[Math.max(a-1,0)][0]},e.prototype.parse=function(t){return j(t)?t:+ro(t)},e.prototype.contain=function(t){return Dx(this.parse(t),this._extent)},e.prototype.normalize=function(t){return Ax(this.parse(t),this._extent)},e.prototype.scale=function(t){return kx(t,this._extent)},e.type=\"time\",e}(Ox),jx=[[\"second\",Ec],[\"minute\",zc],[\"hour\",Vc],[\"quarter-day\",216e5],[\"half-day\",432e5],[\"day\",10368e4],[\"half-week\",3024e5],[\"week\",6048e5],[\"month\",26784e5],[\"quarter\",8208e6],[\"half-year\",Fc/2],[\"year\",Fc]];function qx(t,e){return(t/=Bc)>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]<n[0]&&(n=[NaN,NaN]),this._dataMin=n[0],this._dataMax=n[1];var i=this._isOrdinal=\"ordinal\"===t.type;this._needCrossZero=\"interval\"===t.type&&e.getNeedCrossZero&&e.getNeedCrossZero();var r=this._modelMinRaw=e.get(\"min\",!0);X(r)?this._modelMinNum=g_(t,r({min:n[0],max:n[1]})):\"dataMin\"!==r&&(this._modelMinNum=g_(t,r));var o=this._modelMaxRaw=e.get(\"max\",!0);if(X(o)?this._modelMaxNum=g_(t,o({min:n[0],max:n[1]})):\"dataMax\"!==o&&(this._modelMaxNum=g_(t,o)),i)this._axisDataLen=e.getCategories().length;else{var a=e.get(\"boundaryGap\"),s=Y(a)?a:[a||0,a||0];\"boolean\"==typeof s[0]||\"boolean\"==typeof s[1]?this._boundaryGapInner=[0,0]:this._boundaryGapInner=[Ir(s[0],1),Ir(s[1],1)]}},t.prototype.calculate=function(){var t=this._isOrdinal,e=this._dataMin,n=this._dataMax,i=this._axisDataLen,r=this._boundaryGapInner,o=t?null:n-e||Math.abs(e),a=\"dataMin\"===this._modelMinRaw?e:this._modelMinNum,s=\"dataMax\"===this._modelMaxRaw?n:this._modelMaxNum,l=null!=a,u=null!=s;null==a&&(a=t?i?0:NaN:e-r[0]*o),null==s&&(s=t?i?i-1:NaN:n+r[1]*o),(null==a||!isFinite(a))&&(a=NaN),(null==s||!isFinite(s))&&(s=NaN);var h=nt(a)||nt(s)||t&&!i;this._needCrossZero&&(a>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;o<t.length;o++){var a=t[o];i+=ds(r[0],r[1],a[0],a[1],e,n),r=a}var s=t[0];return D_(r[0],s[0])&&D_(r[1],s[1])||(i+=ds(r[0],r[1],s[0],s[1],e,n)),0!==i}var k_=[];function L_(t,e){for(var n=0;n<t.length;n++)Wt(t[n],t[n],e)}function P_(t,e,n,i){for(var r=0;r<t.length;r++){var o=t[r];i&&(o=i.project(o)),o&&isFinite(o[0])&&isFinite(o[1])&&(Ht(e,e,o),Yt(n,n,o))}}var O_=function(){function t(t){this.name=t}return t.prototype.setCenter=function(t){this._center=t},t.prototype.getCenter=function(){var t=this._center;return t||(t=this._center=this.calcCenter()),t},t}(),R_=function(t,e){this.type=\"polygon\",this.exterior=t,this.interiors=e},N_=function(t){this.type=\"linestring\",this.points=t},E_=function(t){function e(e,n,i){var r=t.call(this,e)||this;return r.type=\"geoJSON\",r.geometries=n,r._center=i&&[i[0],i[1]],r}return n(e,t),e.prototype.calcCenter=function(){for(var t,e=this.geometries,n=0,i=0;i<e.length;i++){var r=e[i],o=r.exterior,a=o&&o.length;a>n&&(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<r;s++){var l=t[s][0],u=t[s][1],h=o*u-l*a;e+=h,n+=(o+l)*h,i+=(a+u)*h,o=l,a=u}return e?[n/e/3,i/e/3,e]:[t[0][0]||0,t[0][1]||0]}(t.exterior);var s=this.getBoundingRect();return[s.x+s.width/2,s.y+s.height/2]},e.prototype.getBoundingRect=function(t){var e=this._rect;if(e&&!t)return e;var n=[1/0,1/0],i=[-1/0,-1/0];return E(this.geometries,(function(e){\"polygon\"===e.type?P_(e.exterior,n,i,t):E(e.points,(function(e){P_(e,n,i,t)}))})),isFinite(n[0])&&isFinite(n[1])&&isFinite(i[0])&&isFinite(i[1])||(n[0]=n[1]=i[0]=i[1]=0),e=new ze(n[0],n[1],i[0]-n[0],i[1]-n[1]),t||(this._rect=e),e},e.prototype.contain=function(t){var e=this.getBoundingRect(),n=this.geometries;if(!e.contain(t[0],t[1]))return!1;t:for(var i=0,r=n.length;i<r;i++){var o=n[i];if(\"polygon\"===o.type){var a=o.exterior,s=o.interiors;if(A_(a,t[0],t[1])){for(var l=0;l<(s?s.length:0);l++)if(A_(s[l],t[0],t[1]))continue t;return!0}}}return!1},e.prototype.transformTo=function(t,e,n,i){var r=this.getBoundingRect(),o=r.width/r.height;n?i||(i=n/o):n=o*i;for(var a=new ze(t,e,n,i),s=r.calculateTransform(a),l=this.geometries,u=0;u<l.length;u++){var h=l[u];\"polygon\"===h.type?(L_(h.exterior,s),E(h.interiors,(function(t){L_(t,s)}))):E(h.points,(function(t){L_(t,s)}))}(r=this._rect).copy(a),this._center=[r.x+r.width/2,r.y+r.height/2]},e.prototype.cloneShallow=function(t){null==t&&(t=this.name);var n=new e(t,this.geometries,this._center);return n._rect=this._rect,n.transformTo=null,n},e}(O_),z_=function(t){function e(e,n){var i=t.call(this,e)||this;return i.type=\"geoSVG\",i._elOnlyForCalculate=n,i}return n(e,t),e.prototype.calcCenter=function(){for(var t=this._elOnlyForCalculate,e=t.getBoundingRect(),n=[e.x+e.width/2,e.y+e.height/2],i=xe(k_),r=t;r&&!r.isGeoSVGGraphicRoot;)be(i,r.getLocalTransform(),i),r=r.parent;return Ie(i,i),Wt(n,n,i),n},e}(O_);function V_(t,e,n){for(var i=0;i<t.length;i++)t[i]=B_(t[i],e[i],n)}function B_(t,e,n){for(var i=[],r=e[0],o=e[1],a=0;a<t.length;a+=2){var s=t.charCodeAt(a)-64,l=t.charCodeAt(a+1)-64;s=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;n<t.length;n++)if(t[n].key===e)return t[n].value}function J_(t,e,n){return t.push({key:e,value:n}),n}function Q_(t,e,n){var i=x_(t),r=t.scale,o=r.getExtent(),a=t.getLabelModel(),s=[],l=Math.max((e||0)+1,1),u=o[0],h=r.count();0!==u&&l>1&&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:t<e}}(this,n,e.get(\"alignWithLabel\"),t.clamp),n},t.prototype.getMinorTicksCoords=function(){if(\"ordinal\"===this.scale.type)return[];var t=this.model.getModel(\"minorTick\").get(\"splitNumber\");return t>0&&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 m<x?(l[0]=f,l[1]=g,Math.sqrt(m)):(l[0]=y,l[1]=v,Math.sqrt(x))}function ub(t,e,n,i,r,o,a,s){var l=r-t,u=o-e,h=n-t,c=i-e,p=Math.sqrt(h*h+c*c),d=(l*(h/=p)+u*(c/=p))/p;s&&(d=Math.min(Math.max(d,0),1)),d*=p;var f=a[0]=t+d*h,g=a[1]=e+d*c;return Math.sqrt((f-r)*(f-r)+(g-o)*(g-o))}function hb(t,e,n,i,r,o,a){n<0&&(t+=n,n=-n),i<0&&(e+=i,i=-i);var s=t+n,l=e+i,u=a[0]=Math.min(Math.max(r,t),s),h=a[1]=Math.min(Math.max(o,e),l);return Math.sqrt((u-r)*(u-r)+(h-o)*(h-o))}var cb=[];function pb(t,e,n){var i=hb(e.x,e.y,e.width,e.height,t.x,t.y,cb);return n.set(cb[0],cb[1]),i}function db(t,e,n){for(var i,r,o=0,a=0,s=0,l=0,u=1/0,h=e.data,c=t.x,p=t.y,d=0;d<h.length;){var f=h[d++];1===d&&(s=o=h[d],l=a=h[d+1]);var g=u;switch(f){case ob.M:o=s=h[d++],a=l=h[d++];break;case ob.L:g=ub(o,a,h[d],h[d+1],c,p,cb,!0),o=h[d++],a=h[d++];break;case ob.C:g=Sn(o,a,h[d++],h[d++],h[d++],h[d++],h[d],h[d+1],c,p,cb),o=h[d++],a=h[d++];break;case ob.Q:g=An(o,a,h[d++],h[d++],h[d],h[d+1],c,p,cb),o=h[d++],a=h[d++];break;case ob.A:var y=h[d++],v=h[d++],m=h[d++],x=h[d++],_=h[d++],b=h[d++];d+=1;var w=!!(1-h[d++]);i=Math.cos(_)*m+y,r=Math.sin(_)*x+v,d<=1&&(s=i,l=r),g=lb(y,v,x,_,_+b,w,(c-y)*x/m+y,p,cb),o=Math.cos(_+b)*m+y,a=Math.sin(_+b)*x+v;break;case ob.R:g=hb(s=o=h[d++],l=a=h[d++],h[d++],h[d++],c,p,cb);break;case ob.Z:g=ub(o,a,s,l,c,p,cb,!0),o=s,a=l}g<u&&(u=g,n.set(cb[0],cb[1]))}return u}var fb=new De,gb=new De,yb=new De,vb=new De,mb=new De;function xb(t,e){if(t){var n=t.getTextGuideLine(),i=t.getTextContent();if(i&&n){var r=t.textGuideLineConfig||{},o=[[0,0],[0,0],[0,0]],a=r.candidates||ab,s=i.getBoundingRect().clone();s.applyTransform(i.getComputedTransform());var l=1/0,u=r.anchor,h=t.getComputedTransform(),c=h&&Ie([],h),p=e.get(\"length2\")||0;u&&yb.copy(u);for(var d=0;d<a.length;d++){sb(a[d],0,s,fb,vb),De.scaleAndAdd(gb,fb,vb,p),gb.transform(c);var f=t.getBoundingRect(),g=u?u.distance(gb):t instanceof Is?db(gb,t.path,yb):pb(gb,f,yb);g<l&&(l=g,gb.transform(h),yb.transform(h),yb.toArray(o[0]),gb.toArray(o[1]),fb.toArray(o[2]))}wb(o,e.get(\"minTurnAngle\")),n.setShape({points:o})}}}var _b=[],bb=new De;function wb(t,e){if(e<=180&&e>0){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)<r){var o=ub(gb.x,gb.y,yb.x,yb.y,fb.x,fb.y,_b,!1);bb.fromArray(_b),bb.scaleAndAdd(mb,o/Math.tan(Math.PI-e));var a=yb.x!==gb.x?(bb.x-gb.x)/(yb.x-gb.x):(bb.y-gb.y)/(yb.y-gb.y);if(isNaN(a))return;a<0?De.copy(bb,gb):a>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)<Math.cos(n)){var o=ub(gb.x,gb.y,yb.x,yb.y,fb.x,fb.y,_b,!1);bb.fromArray(_b);var a=Math.PI/2,s=a+Math.acos(mb.dot(e))-n;if(s>=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;h<i.length;h++)t.lineTo(i[h][0],i[h][1])}function Tb(t,e,n){var i=t.getTextGuideLine(),r=t.getTextContent();if(r){for(var o=e.normal,a=o.get(\"show\"),s=r.ignore,l=0;l<al.length;l++){var u=al[l],h=e[u],c=\"normal\"===u;if(h){var p=h.get(\"show\");if((c?s:rt(r.states[u]&&r.states[u].ignore,s))||!rt(p,a)){var d=c?i:i&&i.states[u];d&&(d.ignore=!0);continue}i||(i=new Yu,t.setTextGuideLine(i),c||!s&&a||Mb(i,!0,\"normal\",e.normal),t.stateProxy&&(i.stateProxy=t.stateProxy)),Mb(i,!1,u,h)}}if(i){k(i.style,n),i.style.fill=null;var f=o.get(\"showAbove\");(t.textGuideLineConfig=t.textGuideLineConfig||{}).showAbove=f||!1,i.buildPath=Ib}}else i&&t.removeTextGuideLine()}function Cb(t,e){e=e||\"labelLine\";for(var n={normal:t.getModel(e)},i=0;i<ol.length;i++){var r=ol[i];n[r]=t.getModel([r,e])}return n}function Db(t){for(var e=[],n=0;n<t.length;n++){var i=t[n];if(!i.defaultAttr.ignore){var r=i.label,o=r.getComputedTransform(),a=r.getBoundingRect(),s=!o||o[1]<1e-5&&o[2]<1e-5,l=r.style.margin||0,u=a.clone();u.applyTransform(o),u.x-=l/2,u.y-=l/2,u.width+=l,u.height+=l;var h=s?new lh(a,o):null;e.push({label:r,labelLine:i.labelLine,rect:u,localRect:a,obb:h,priority:i.priority,defaultAttr:i.defaultAttr,layoutOption:i.computedLayoutOption,axisAligned:s,transform:o})}}return e}function Ab(t,e,n,i,r,o){var a=t.length;if(!(a<2)){t.sort((function(t,n){return t.rect[e]-n.rect[e]}));for(var s,l=0,u=!1,h=0,c=0;c<a;c++){var p=t[c],d=p.rect;(s=d[e]-l)<0&&(d[e]-=s,p.label[e]-=s,u=!0),h+=Math.max(-s,0),l=d[e]+d[n]}h>0&&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;o<r;o++){var a=t[o];a.rect[e]+=n,a.label[e]+=n}}function b(i,r){for(var o=[],s=0,l=1;l<a;l++){var u=t[l-1].rect,h=Math.max(t[l].rect[e]-u[e]-u[n],0);o.push(h),s+=h}if(s){var c=Math.min(Math.abs(i)/s,r);if(i>0)for(l=0;l<a-1;l++){_(o[l]*c,0,l+1)}else for(l=a-1;l>0;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;i<a-1;i++)if(e>0?_(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<t.length;r++){var o=t[r],a=o.axisAligned,s=o.localRect,l=o.transform,u=o.label,h=o.labelLine;n.copy(o.rect),n.width-=.1,n.height-=.1,n.x+=.05,n.y+=.05;for(var c=o.obb,p=!1,d=0;d<e.length;d++){var f=e[d];if(n.intersect(f.rect)){if(a&&f.axisAligned){p=!0;break}if(f.obb||(f.obb=new lh(f.localRect,f.transform)),c||(c=new lh(s,l)),c.intersect(f.obb)){p=!0;break}}}p?(i(u),h&&i(h)):(u.attr(\"ignore\",o.defaultAttr.ignore),h&&h.attr(\"ignore\",o.defaultAttr.labelGuideIgnore),e.push(o))}}function Pb(t){if(t){for(var e=[],n=0;n<t.length;n++)e.push(t[n].slice());return e}}function Ob(t,e){var n=t.label,i=e&&e.getTextGuideLine();return{dataIndex:t.dataIndex,dataType:t.dataType,seriesIndex:t.seriesModel.seriesIndex,text:t.label.style.text,rect:t.hostRect,labelRect:t.rect,align:n.style.align,verticalAlign:n.style.verticalAlign,labelLinePoints:Pb(i&&i.shape.points)}}var Rb=[\"align\",\"verticalAlign\",\"width\",\"height\",\"fontSize\"],Nb=new gr,Eb=Oo(),zb=Oo();function Vb(t,e,n){for(var i=0;i<n.length;i++){var r=n[i];null!=e[r]&&(t[r]=e[r])}}var Bb=[\"x\",\"y\",\"rotation\"],Fb=function(){function t(){this._labelList=[],this._chartViewList=[]}return t.prototype.clearLabels=function(){this._labelList=[],this._chartViewList=[]},t.prototype._addLabel=function(t,e,n,i,r){var o=i.style,a=i.__hostTarget.textConfig||{},s=i.getComputedTransform(),l=i.getBoundingRect().plain();ze.applyTransform(l,l,s),s?Nb.setLocalTransform(s):(Nb.x=Nb.y=Nb.rotation=Nb.originX=Nb.originY=0,Nb.scaleX=Nb.scaleY=1),Nb.rotation=hs(Nb.rotation);var u,h=i.__hostTarget;if(h){u=h.getBoundingRect().plain();var c=h.getComputedTransform();ze.applyTransform(u,u,c)}var p=u&&h.getTextGuideLine();this._labelList.push({label:i,labelLine:p,seriesModel:n,dataIndex:t,dataType:e,layoutOption:r,computedLayoutOption:null,rect:l,hostRect:u,priority:u?u.width*u.height:0,defaultAttr:{ignore:i.ignore,labelGuideIgnore:p&&p.ignore,x:Nb.x,y:Nb.y,scaleX:Nb.scaleX,scaleY:Nb.scaleY,rotation:Nb.rotation,style:{x:o.x,y:o.y,align:o.align,verticalAlign:o.verticalAlign,width:o.width,height:o.height,fontSize:o.fontSize},cursor:i.cursor,attachedPos:a.position,attachedRot:a.rotation}})},t.prototype.addLabelsOfSeries=function(t){var e=this;this._chartViewList.push(t);var n=t.__model,i=n.get(\"labelLayout\");(X(i)||G(i).length)&&t.group.traverse((function(t){if(t.ignore)return!0;var r=t.getTextContent(),o=Qs(t);r&&!r.disableLabelLayout&&e._addLabel(o.dataIndex,o.dataType,n,r,i)}))},t.prototype.updateLayoutConfig=function(t){var e=t.getWidth(),n=t.getHeight();function i(t,e){return function(){xb(t,e)}}for(var r=0;r<this._labelList.length;r++){var o=this._labelList[r],a=o.label,s=a.__hostTarget,l=o.defaultAttr,u=void 0;u=(u=X(o.layoutOption)?o.layoutOption(Ob(o,s)):o.layoutOption)||{},o.computedLayoutOption=u;var h=Math.PI/180;s&&s.setTextConfig({local:!1,position:null!=u.x||null!=u.y?null:l.attachedPos,rotation:null!=u.rotate?u.rotate*h:l.attachedRot,offset:[u.dx||0,u.dy||0]});var c=!1;if(null!=u.x?(a.x=Ur(u.x,e),a.setStyle(\"x\",0),c=!0):(a.x=l.x,a.setStyle(\"x\",l.style.x)),null!=u.y?(a.y=Ur(u.y,n),a.setStyle(\"y\",0),c=!0):(a.y=l.y,a.setStyle(\"y\",l.style.y)),u.labelLinePoints){var p=s.getTextGuideLine();p&&(p.setShape({points:u.labelLinePoints}),c=!1)}Eb(a).needsUpdateLabelLine=c,a.rotation=null!=u.rotate?u.rotate*h:l.rotation,a.scaleX=l.scaleX,a.scaleY=l.scaleY;for(var d=0;d<Rb.length;d++){var f=Rb[d];a.setStyle(f,null!=u[f]?u[f]:l.style[f])}if(u.draggable){if(a.draggable=!0,a.cursor=\"move\",s){var g=o.seriesModel;if(null!=o.dataIndex)g=o.seriesModel.getData(o.dataType).getItemModel(o.dataIndex);a.on(\"drag\",i(s,g.getModel(\"labelLine\")))}}else a.off(\"drag\"),a.cursor=l.cursor}},t.prototype.layout=function(t){var e,n=t.getWidth(),i=t.getHeight(),r=Db(this._labelList),o=B(r,(function(t){return\"shiftX\"===t.layoutOption.moveOverlap})),a=B(r,(function(t){return\"shiftY\"===t.layoutOption.moveOverlap}));Ab(o,\"x\",\"width\",0,n,e),kb(a,0,i),Lb(B(r,(function(t){return t.layoutOption.hideOverlap})))},t.prototype.processLabelsOverall=function(){var t=this;E(this._chartViewList,(function(e){var n=e.__model,i=e.ignoreLabelLineUpdate,r=n.isAnimationEnabled();e.group.traverse((function(e){if(e.ignore&&!e.forceLabelAnimation)return!0;var o=!i,a=e.getTextContent();!o&&a&&(o=Eb(a).needsUpdateLabelLine),o&&t._updateLabelLine(e,n),r&&t._animateLabels(e,n)}))}))},t.prototype._updateLabelLine=function(t,e){var n=t.getTextContent(),i=Qs(t),r=i.dataIndex;if(n&&null!=r){var o=e.getData(i.dataType),a=o.getItemModel(r),s={},l=o.getItemVisual(r,\"style\");if(l){var u=o.getVisual(\"drawType\");s.stroke=l[u]}var h=a.getModel(\"labelLine\");Tb(t,Cb(a),s),xb(t,h)}},t.prototype._animateLabels=function(t,e){var n=t.getTextContent(),i=t.getTextGuideLine();if(n&&(t.forceLabelAnimation||!n.ignore&&!n.invisible&&!t.disableLabelAnimation&&!yh(t))){var r=(d=Eb(n)).oldLayout,o=Qs(t),a=o.dataIndex,s={x:n.x,y:n.y,rotation:n.rotation},l=e.getData(o.dataType);if(r){n.attr(r);var u=t.prevStates;u&&(P(u,\"select\")>=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<arguments.length;c++){var p=arguments[c];if(isNaN(p))return void(this._invalid=!0);u.push(Math.round(p*h)/h)}this._d.push(t+u.join(\" \")),this._start=\"Z\"===t},t.prototype.generateStr=function(){this._str=this._invalid?\"\":this._d.join(\"\"),this._d=[]},t.prototype.getStr=function(){return this._str},t}(),jb=\"none\",qb=Math.round;var Kb=[\"lineCap\",\"miterLimit\",\"lineJoin\"],$b=z(Kb,(function(t){return\"stroke-\"+t.toLowerCase()}));function Jb(t,e,n,i){var r=null==e.opacity?1:e.opacity;if(n instanceof ks)t(\"opacity\",r);else{if(function(t){var e=t.fill;return null!=e&&e!==jb}(e)){var o=li(e.fill);t(\"fill\",o.color);var a=null!=e.fillOpacity?e.fillOpacity*o.opacity*r:o.opacity*r;(i||a<1)&&t(\"fill-opacity\",a)}else t(\"fill\",jb);if(function(t){var e=t.stroke;return null!=e&&e!==jb}(e)){var s=li(e.stroke);t(\"stroke\",s.color);var l=e.strokeNoScale?n.getLineScale():1,u=l?(e.lineWidth||0)/l:0,h=null!=e.strokeOpacity?e.strokeOpacity*s.opacity*r:s.opacity*r,c=e.strokeFirst;if((i||1!==u)&&t(\"stroke-width\",u),(i||c)&&t(\"paint-order\",c?\"stroke\":\"fill\"),(i||h<1)&&t(\"stroke-opacity\",h),e.lineDash){var p=qy(n),d=p[0],f=p[1];d&&(f=qb(f||0),t(\"stroke-dasharray\",d.join(\",\")),(f||i)&&t(\"stroke-dashoffset\",f))}else i&&t(\"stroke-dasharray\",jb);for(var g=0;g<Kb.length;g++){var y=Kb[g];if(i||e[y]!==ws[y]){var v=e[y]||ws[y];v&&t($b[g],v)}}}else i&&t(\"stroke\",jb)}}var Qb=\"http://www.w3.org/2000/svg\",tw=\"http://www.w3.org/1999/xlink\";function ew(t){return document.createElementNS(Qb,t)}function nw(t,e,n,i,r){return{tag:t,attrs:n||{},children:i,text:r,key:e}}function iw(t,e){var n=(e=e||{}).newline?\"\\n\":\"\";return function t(e){var i=e.children,r=e.tag,o=e.attrs,a=e.text;return function(t,e){var n=[];if(e)for(var i in e){var r=e[i],o=i;!1!==r&&(!0!==r&&null!=r&&(o+='=\"'+r+'\"'),n.push(o))}return\"<\"+t+\" \"+n.join(\" \")+\">\"}(r,o)+(\"style\"!==r?re(a):a||\"\")+(i?\"\"+n+z(i,(function(e){return t(e)})).join(n)+n:\"\")+(\"</\"+r+\">\")}(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;u<o;u++){var h=r[u],c=[h.getMaxTime()/1e3+\"s\"],p=pw(h.getClip().easing),d=h.getDelay();p?c.push(p):c.push(\"linear\"),d&&c.push(d/1e3+\"s\"),h.getLoop()&&c.push(\"infinite\");var f=c.join(\" \");l[f]=l[f]||[f,[]],l[f][1].push(h)}function g(r){var o,a=r[1],s=a.length,l={},u={},h={},c=\"animation-timing-function\";function p(t,e,n){for(var i=t.getTracks(),r=t.getMaxTime(),o=0;o<i.length;o++){var a=i[o];if(a.needsAnimate()){var s=a.keyframes,l=a.propName;if(n&&(l=n(l)),l)for(var u=0;u<s.length;u++){var h=s[u],p=Math.round(h.time/r*100)+\"%\",d=pw(h.easing),f=h.rawValue;(U(f)||j(f))&&(e[p]=e[p]||{},e[p][l]=h.rawValue,d&&(e[p][c]=d))}}}}for(var d=0;d<s;d++){(S=(w=a[d]).targetName)?\"shape\"===S&&p(w,u):!i&&p(w,l)}for(var f in l){var g={};vr(g,t),A(g,l[f]);var y=bi(g),v=l[f][c];h[f]=y?{transform:y}:{},uw(h[f],g),v&&(h[f][c]=v)}var m=!0;for(var f in u){h[f]=h[f]||{};var x=!o;v=u[f][c];x&&(o=new os);var _=o.len();o.reset(),h[f].d=lw(t,u[f],o);var b=o.len();if(!x&&_!==b){m=!1;break}v&&(h[f][c]=v)}if(!m)for(var f in h)delete h[f].d;if(!i)for(d=0;d<s;d++){var w,S;\"style\"===(S=(w=a[d]).targetName)&&p(w,h,(function(t){return hw[t]}))}var M,I=G(h),T=!0;for(d=1;d<I.length;d++){var C=I[d-1],D=I[d];if(h[C][sw]!==h[D][sw]){T=!1;break}M=h[C][sw]}if(T&&M){for(var f in h)h[f][sw]&&delete h[f][sw];e[sw]=M}if(B(I,(function(t){return G(h[t]).length>0})).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;o<i.length;o++)r.push(fw(i[o][0]*n)/n),r.push(fw(i[o][1]*n)/n);e.points=r.join(\" \")}function bw(t){return!t.smooth}var ww,Sw,Mw={circle:[(ww=[\"cx\",\"cy\",\"r\"],Sw=z(ww,(function(t){return\"string\"==typeof t?[t,t]:t})),function(t,e,n){for(var i=0;i<Sw.length;i++){var r=Sw[i],o=t[r[0]];null!=o&&(e[r[1]]=fw(o*n)/n)}})],polyline:[_w,bw],polygon:[_w,bw]};function Iw(t,e){var n=t.style,i=t.shape,r=Mw[t.type],o={},a=e.animation,s=\"path\",l=t.style.strokePercent,u=e.compress&&_i(t)||4;if(!r||e.willUpdate||r[1]&&!r[1](i)||a&&function(t){for(var e=t.animators,n=0;n<e.length;n++)if(\"shape\"===e[n].targetName)return!0;return!1}(t)||l<1){var h=!t.path||t.shapeChanged();t.path||t.createPathProxy();var c=t.path;h&&(c.beginPath(),t.buildPath(c,t.shape),t.pathUpdated());var p=c.getVersion(),d=t,f=d.__svgPathBuilder;d.__svgPathVersion===p&&f&&l===d.__svgPathStrokePercent||(f||(f=d.__svgPathBuilder=new Zb),f.reset(u),c.rebuildPath(f,l),f.generateStr(),d.__svgPathVersion=p,d.__svgPathStrokePercent=l),o.d=f.getStr()}else{s=t.type;var g=Math.pow(10,u);r[0](i,o,g)}return xw(o,t.transform),vw(o,n,t,e),e.animation&&dw(t,o,e),nw(s,t.id+\"\",o)}function Tw(t,e){return t instanceof Is?Iw(t,e):t instanceof ks?function(t,e){var n=t.style,i=n.image;if(i&&!U(i)&&(gw(i)?i=i.src:yw(i)&&(i=i.toDataURL())),i){var r=n.x||0,o=n.y||0,a={href:i,width:n.width,height:n.height};return r&&(a.x=r),o&&(a.y=o),xw(a,t.transform),vw(a,n,t,e),e.animation&&dw(t,a,e),nw(\"image\",t.id+\"\",a)}}(t,e):t instanceof Cs?function(t,e){var n=t.style,i=n.text;if(null!=i&&(i+=\"\"),i&&!isNaN(n.x)&&!isNaN(n.y)){var r=n.font||a,s=n.x||0,l=function(t,e,n){return\"top\"===n?t+=e/2:\"bottom\"===n&&(t-=e/2),t}(n.y||0,Mr(r),n.textBaseline),u={\"dominant-baseline\":\"central\",\"text-anchor\":di[n.textAlign]||n.textAlign};if(Us(n)){var h=\"\",c=n.fontStyle,p=Ys(n.fontSize);if(!parseFloat(p))return;var d=n.fontFamily||o,f=n.fontWeight;h+=\"font-size:\"+p+\";font-family:\"+d+\";\",c&&\"normal\"!==c&&(h+=\"font-style:\"+c+\";\"),f&&\"normal\"!==f&&(h+=\"font-weight:\"+f+\";\"),u.style=h}else u.style=\"font: \"+r;return i.match(/\\s/)&&(u[\"xml:space\"]=\"preserve\"),s&&(u.x=s),l&&(u.y=l),xw(u,t.transform),vw(u,n,t,e),e.animation&&dw(t,u,e),nw(\"text\",t.id+\"\",u,void 0,i)}}(t,e):void 0}function Cw(t,e,n,i){var r,o=t[n],a={gradientUnits:o.global?\"userSpaceOnUse\":\"objectBoundingBox\"};if(yi(o))r=\"linearGradient\",a.x1=o.x,a.y1=o.y,a.x2=o.x2,a.y2=o.y2;else{if(!vi(o))return void 0;r=\"radialGradient\",a.cx=rt(o.x,.5),a.cy=rt(o.y,.5),a.r=rt(o.r,.5)}for(var s=o.colorStops,l=[],u=0,h=s.length;u<h;++u){var c=100*pi(s[u].offset)+\"%\",p=li(s[u].color),d=p.color,f=p.opacity,g={offset:c};g[\"stop-color\"]=d,f<1&&(g[\"stop-opacity\"]=f),l.push(nw(\"stop\",u+\"\",g))}var y=iw(nw(r,\"\",a,l)),v=i.gradientCache,m=v[y];m||(m=i.zrId+\"-g\"+i.gradientIdx++,v[y]=m,a.id=m,i.defs[m]=nw(r,m,a,l)),e[n]=xi(m)}function Dw(t,e,n,i){var r,o=t.style[n],a=t.getBoundingRect(),s={},l=o.repeat,u=\"no-repeat\"===l,h=\"repeat-x\"===l,c=\"repeat-y\"===l;if(fi(o)){var p=o.imageWidth,d=o.imageHeight,f=void 0,g=o.image;if(U(g)?f=g:gw(g)?f=g.src:yw(g)&&(f=g.toDataURL()),\"undefined\"==typeof Image){var y=\"Image width/height must been given explictly in svg-ssr renderer.\";lt(p,y),lt(d,y)}else if(null==p||null==d){var v=function(t,e){if(t){var n=t.elm,i=p||e.width,r=d||e.height;\"pattern\"===t.tag&&(h?(r=1,i/=a.width):c&&(i=1,r/=a.height)),t.attrs.width=i,t.attrs.height=r,n&&(n.setAttribute(\"width\",i),n.setAttribute(\"height\",r))}},m=ia(f,null,t,(function(t){u||v(w,t),v(r,t)}));m&&m.width&&m.height&&(p=p||m.width,d=d||m.height)}r=nw(\"image\",\"img\",{href:f,width:p,height:d}),s.width=p,s.height=d}else o.svgElement&&(r=T(o.svgElement),s.width=o.svgWidth,s.height=o.svgHeight);if(r){var x,_;u?x=_=1:h?(_=1,x=s.width/a.width):c?(x=1,_=s.height/a.height):s.patternUnits=\"userSpaceOnUse\",null==x||isNaN(x)||(s.width=x),null==_||isNaN(_)||(s.height=_);var b=bi(o);b&&(s.patternTransform=b);var w=nw(\"pattern\",\"\",s,[r]),S=iw(w),M=i.patternCache,I=M[S];I||(I=i.zrId+\"-p\"+i.patternIdx++,M[S]=I,s.id=I,w=i.defs[I]=nw(\"pattern\",I,s,[r])),e[n]=xi(I)}}function Aw(t,e,n){var i=n.clipPathCache,r=n.defs,o=i[t.id];if(!o){var a={id:o=n.zrId+\"-c\"+n.clipPathIdx++};i[t.id]=o,r[o]=nw(\"clipPath\",o,a,[Iw(t,n)])}e[\"clip-path\"]=xi(o)}function kw(t){return document.createTextNode(t)}function Lw(t,e,n){t.insertBefore(e,n)}function Pw(t,e){t.removeChild(e)}function Ow(t,e){t.appendChild(e)}function Rw(t){return t.parentNode}function Nw(t){return t.nextSibling}function Ew(t,e){t.textContent=e}var zw=nw(\"\",\"\");function Vw(t){return void 0===t}function Bw(t){return void 0!==t}function Fw(t,e,n){for(var i={},r=e;r<=n;++r){var o=t[r].key;void 0!==o&&(i[o]=r)}return i}function Gw(t,e){var n=t.key===e.key;return t.tag===e.tag&&n}function Ww(t){var e,n=t.children,i=t.tag;if(Bw(i)){var r=t.elm=ew(i);if(Xw(zw,t),Y(n))for(e=0;e<n.length;++e){var o=n[e];null!=o&&Ow(r,Ww(o))}else Bw(t.text)&&!q(t.text)&&Ow(r,kw(t.text))}else t.elm=kw(t.text);return t.elm}function Hw(t,e,n,i,r){for(;i<=r;++i){var o=n[i];null!=o&&Lw(t,Ww(o),e)}}function Yw(t,e,n,i){for(;n<=i;++n){var r=e[n];if(null!=r)if(Bw(r.tag))Pw(Rw(r.elm),r.elm);else Pw(t,r.elm)}}function Xw(t,e){var n,i=e.elm,r=t&&t.attrs||{},o=e.attrs||{};if(r!==o){for(n in o){var a=o[n];r[n]!==a&&(!0===a?i.setAttribute(n,\"\"):!1===a?i.removeAttribute(n):120!==n.charCodeAt(0)?i.setAttribute(n,a):\"xmlns:xlink\"===n||\"xmlns\"===n?i.setAttributeNS(\"http://www.w3.org/2000/xmlns/\",n,a):58===n.charCodeAt(3)?i.setAttributeNS(\"http://www.w3.org/XML/1998/namespace\",n,a):58===n.charCodeAt(5)?i.setAttributeNS(tw,n,a):i.setAttribute(n,a))}for(n in r)n in o||i.removeAttribute(n)}}function Uw(t,e){var n=e.elm=t.elm,i=t.children,r=e.children;t!==e&&(Xw(t,e),Vw(e.text)?Bw(i)&&Bw(r)?i!==r&&function(t,e,n){for(var i,r,o,a=0,s=0,l=e.length-1,u=e[0],h=e[l],c=n.length-1,p=n[0],d=n[c];a<=l&&s<=c;)null==u?u=e[++a]:null==h?h=e[--l]:null==p?p=n[++s]:null==d?d=n[--c]:Gw(u,p)?(Uw(u,p),u=e[++a],p=n[++s]):Gw(h,d)?(Uw(h,d),h=e[--l],d=n[--c]):Gw(u,d)?(Uw(u,d),Lw(t,u.elm,Nw(h.elm)),u=e[++a],d=n[--c]):Gw(h,p)?(Uw(h,p),Lw(t,h.elm,u.elm),h=e[--l],p=n[++s]):(Vw(i)&&(i=Fw(e,a,l)),Vw(r=i[p.key])||(o=e[r]).tag!==p.tag?Lw(t,Ww(p),u.elm):(Uw(o,p),e[r]=void 0,Lw(t,o.elm,u.elm)),p=n[++s]);(a<=l||s<=c)&&(a>l?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?[\"<![CDATA[\",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<o;u++){var h=t[u];if(!h.invisible){var c=h.__clipPaths,p=c&&c.length||0,d=r&&r.length||0,f=void 0;for(f=Math.max(p-1,d-1);f>=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<p;y++){var v={};Aw(c[y],v,e);var m=nw(\"g\",\"clip-g-\"+l++,v,[]);(i?i.children:n).push(m),a[s++]=m,i=m}r=c;var x=Tw(h,e);x&&(i?i.children:n).push(x)}}},t.prototype.resize=function(t,e){var n=this._opts,i=this.root,r=this._viewport;if(null!=t&&(n.width=t),null!=e&&(n.height=e),i&&r&&(r.style.display=\"none\",t=jy(i,0,n),e=jy(i,1,n),r.style.display=\"\"),this._width!==t||this._height!==e){if(this._width=t,this._height=e,r){var o=r.style;o.width=t+\"px\",o.height=e+\"px\"}if(gi(this._backgroundColor))this.refresh();else{var a=this._svgDom;a&&(a.setAttribute(\"width\",t),a.setAttribute(\"height\",e));var s=this._bgVNode&&this._bgVNode.elm;s&&(s.setAttribute(\"width\",t),s.setAttribute(\"height\",e))}}},t.prototype.getWidth=function(){return this._width},t.prototype.getHeight=function(){return this._height},t.prototype.dispose=function(){this.root&&(this.root.innerHTML=\"\"),this._svgDom=this._viewport=this.storage=this._oldVNode=this._bgVNode=this._mainVNode=null},t.prototype.clear=function(){this._svgDom&&(this._svgDom.innerHTML=null),this._oldVNode=null},t.prototype.toDataURL=function(t){var e=this.renderToString(),n=\"data:image/svg+xml;\";return t?(e=wi(e))&&n+\"base64,\"+e:n+\"charset=UTF-8,\"+encodeURIComponent(e)},t}();function qw(t){return function(){0}}function Kw(t,e,n){var i=h.createCanvas(),r=e.getWidth(),o=e.getHeight(),a=i.style;return a&&(a.position=\"absolute\",a.left=\"0\",a.top=\"0\",a.width=r+\"px\",a.height=o+\"px\",i.setAttribute(\"data-zr-dom-id\",t)),i.width=r*n,i.height=o*n,i}var $w=function(t){function e(e,n,i){var r,o=t.call(this)||this;o.motionBlur=!1,o.lastFrameAlpha=.7,o.dpr=1,o.virtual=!1,o.config={},o.incremental=!1,o.zlevel=0,o.maxRepaintRectCount=5,o.__dirty=!0,o.__firstTimePaint=!0,o.__used=!1,o.__drawIndex=0,o.__startIndex=0,o.__endIndex=0,o.__prevStartIndex=null,o.__prevEndIndex=null,i=i||or,\"string\"==typeof e?r=Kw(e,n,i):q(e)&&(e=(r=e).id),o.id=e,o.dom=r;var a=r.style;return a&&(xt(r),r.onselectstart=function(){return!1},a.padding=\"0\",a.margin=\"0\",a.borderWidth=\"0\"),o.painter=n,o.dpr=i,o}return n(e,t),e.prototype.getElementCount=function(){return this.__endIndex-this.__startIndex},e.prototype.afterBrush=function(){this.__prevStartIndex=this.__startIndex,this.__prevEndIndex=this.__endIndex},e.prototype.initContext=function(){this.ctx=this.dom.getContext(\"2d\"),this.ctx.dpr=this.dpr},e.prototype.setUnpainted=function(){this.__firstTimePaint=!0},e.prototype.createBackBuffer=function(){var t=this.dpr;this.domBack=Kw(\"back-\"+this.id,this.painter,t),this.ctxBack=this.domBack.getContext(\"2d\"),1!==t&&this.ctxBack.scale(t,t)},e.prototype.createRepaintRects=function(t,e,n,i){if(this.__firstTimePaint)return this.__firstTimePaint=!1,null;var r,o=[],a=this.maxRepaintRectCount,s=!1,l=new ze(0,0,0,0);function u(t){if(t.isFinite()&&!t.isZero())if(0===o.length){(e=new ze(0,0,0,0)).copy(t),o.push(e)}else{for(var e,n=!1,i=1/0,r=0,u=0;u<o.length;++u){var h=o[u];if(h.intersect(t)){var c=new ze(0,0,0,0);c.copy(h),c.union(t),o[u]=c,n=!0;break}if(s){l.copy(t),l.union(h);var p=t.width*t.height,d=h.width*h.height,f=l.width*l.height-p-d;f<i&&(i=f,r=u)}}if(s&&(o[r].union(t),n=!0),!n)(e=new ze(0,0,0,0)).copy(t),o.push(e);s||(s=o.length>=a)}}for(var h=this.__startIndex;h<this.__endIndex;++h){if(d=t[h]){var c=d.shouldBePainted(n,i,!0,!0);(f=d.__isRendered&&(1&d.__dirty||!c)?d.getPrevPaintRect():null)&&u(f);var p=c&&(1&d.__dirty||!d.__isRendered)?d.getPaintRect():null;p&&u(p)}}for(h=this.__prevStartIndex;h<this.__prevEndIndex;++h){var d,f;c=(d=e[h]).shouldBePainted(n,i,!0,!0);if(d&&(!c||!d.__zr)&&d.__isRendered)(f=d.getPrevPaintRect())&&u(f)}do{r=!1;for(h=0;h<o.length;)if(o[h].isZero())o.splice(h,1);else{for(var g=h+1;g<o.length;)o[h].intersect(o[g])?(r=!0,o[h].union(o[g]),o.splice(g,1)):g++;h++}}while(r);return this._paintRects=o,o},e.prototype.debugGetPaintRects=function(){return(this._paintRects||[]).slice()},e.prototype.resize=function(t,e){var n=this.dpr,i=this.dom,r=i.style,o=this.domBack;r&&(r.width=t+\"px\",r.height=e+\"px\"),i.width=t*n,i.height=e*n,o&&(o.width=t*n,o.height=e*n,1!==n&&this.ctxBack.scale(n,n))},e.prototype.clear=function(t,e,n){var i=this.dom,r=this.ctx,o=i.width,a=i.height;e=e||this.clearColor;var s=this.motionBlur&&!t,l=this.lastFrameAlpha,u=this.dpr,h=this;s&&(this.domBack||this.createBackBuffer(),this.ctxBack.globalCompositeOperation=\"copy\",this.ctxBack.drawImage(i,0,0,o/u,a/u));var c=this.domBack;function p(t,n,i,o){if(r.clearRect(t,n,i,o),e&&\"transparent\"!==e){var a=void 0;if(Q(e))a=(e.global||e.__width===i&&e.__height===o)&&e.__canvasGradient||Uy(r,e,{x:0,y:0,width:i,height:o}),e.__canvasGradient=a,e.__width=i,e.__height=o;else tt(e)&&(e.scaleX=e.scaleX||u,e.scaleY=e.scaleY||u,a=nv(r,e,{dirty:function(){h.setUnpainted(),h.__painter.refresh()}}));r.save(),r.fillStyle=a||e,r.fillRect(t,n,i,o),r.restore()}s&&(r.save(),r.globalAlpha=l,r.drawImage(c,t,n,i,o),r.restore())}!n||s?p(0,0,o,a):n.length&&E(n,(function(t){p(t.x*u,t.y*u,t.width*u,t.height*u)}))},e}(jt),Jw=1e5,Qw=314159,tS=.01;var eS=function(){function t(t,e,n,i){this.type=\"canvas\",this._zlevelList=[],this._prevDisplayList=[],this._layers={},this._layerConfig={},this._needsManuallyCompositing=!1,this.type=\"canvas\";var r=!t.nodeName||\"CANVAS\"===t.nodeName.toUpperCase();this._opts=n=A({},n||{}),this.dpr=n.devicePixelRatio||or,this._singleCanvas=r,this.root=t,t.style&&(xt(t),t.innerHTML=\"\"),this.storage=e;var o=this._zlevelList;this._prevDisplayList=[];var a=this._layers;if(r){var s=t,l=s.width,u=s.height;null!=n.width&&(l=n.width),null!=n.height&&(u=n.height),this.dpr=n.devicePixelRatio||1,s.width=l*this.dpr,s.height=u*this.dpr,this._width=l,this._height=u;var h=new $w(s,this,this.dpr);h.__builtin__=!0,h.initContext(),a[314159]=h,h.zlevel=Qw,o.push(Qw),this._domRoot=t}else{this._width=jy(t,0,n),this._height=jy(t,1,n);var c=this._domRoot=function(t,e){var n=document.createElement(\"div\");return n.style.cssText=[\"position:relative\",\"width:\"+t+\"px\",\"height:\"+e+\"px\",\"padding:0\",\"margin:0\",\"border-width:0\"].join(\";\")+\";\",n}(this._width,this._height);t.appendChild(c)}}return t.prototype.getType=function(){return\"canvas\"},t.prototype.isSingleCanvas=function(){return this._singleCanvas},t.prototype.getViewportRoot=function(){return this._domRoot},t.prototype.getViewportRootOffset=function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},t.prototype.refresh=function(t){var e=this.storage.getDisplayList(!0),n=this._prevDisplayList,i=this._zlevelList;this._redrawId=Math.random(),this._paintList(e,n,t,this._redrawId);for(var r=0;r<i.length;r++){var o=i[r],a=this._layers[o];if(!a.__builtin__&&a.refresh){var s=0===r?this._backgroundColor:null;a.refresh(s)}}return this._opts.useDirtyRect&&(this._prevDisplayList=e.slice()),this},t.prototype.refreshHover=function(){this._paintHoverList(this.storage.getDisplayList(!1))},t.prototype._paintHoverList=function(t){var e=t.length,n=this._hoverlayer;if(n&&n.clear(),e){for(var i,r={inHover:!0,viewWidth:this._width,viewHeight:this._height},o=0;o<e;o++){var a=t[o];a.__inHover&&(n||(n=this._hoverlayer=this.getLayer(Jw)),i||(i=n.ctx).save(),cv(i,a,r,o===e-1))}i&&i.restore()}},t.prototype.getHoverLayer=function(){return this.getLayer(Jw)},t.prototype.paintOne=function(t,e){hv(t,e)},t.prototype._paintList=function(t,e,n,i){if(this._redrawId===i){n=n||!1,this._updateLayerStatus(t);var r=this._doPaintList(t,e,n),o=r.finished,a=r.needsRefreshHover;if(this._needsManuallyCompositing&&this._compositeManually(),a&&this._paintHoverList(t),o)this.eachLayer((function(t){t.afterBrush&&t.afterBrush()}));else{var s=this;on((function(){s._paintList(t,e,n,i)}))}}},t.prototype._compositeManually=function(){var t=this.getLayer(Qw).ctx,e=this._domRoot.width,n=this._domRoot.height;t.clearRect(0,0,e,n),this.eachBuiltinLayer((function(i){i.virtual&&t.drawImage(i.dom,0,0,e,n)}))},t.prototype._doPaintList=function(t,e,n){for(var i=this,o=[],a=this._opts.useDirtyRect,s=0;s<this._zlevelList.length;s++){var l=this._zlevelList[s],u=this._layers[l];u.__builtin__&&u!==this._hoverlayer&&(u.__dirty||n)&&o.push(u)}for(var h=!0,c=!1,p=function(r){var s,l=o[r],u=l.ctx,p=a&&l.createRepaintRects(t,e,d._width,d._height),f=n?l.__startIndex:l.__drawIndex,g=!n&&l.incremental&&Date.now,y=g&&Date.now(),v=l.zlevel===d._zlevelList[0]?d._backgroundColor:null;if(l.__startIndex===l.__endIndex)l.clear(!1,v,p);else if(f===l.__startIndex){var m=t[f];m.incremental&&m.notClear&&!n||l.clear(!1,v,p)}-1===f&&(console.error(\"For some unknown reason. drawIndex is -1\"),f=l.__startIndex);var x=function(e){var n={inHover:!1,allClipped:!1,prevEl:null,viewWidth:i._width,viewHeight:i._height};for(s=f;s<l.__endIndex;s++){var r=t[s];if(r.__inHover&&(c=!0),i._doPaintEl(r,l,a,e,n,s===l.__endIndex-1),g)if(Date.now()-y>15)break}n.prevElClipPaths&&u.restore()};if(p)if(0===p.length)s=l.__endIndex;else for(var _=d.dpr,b=0;b<p.length;++b){var w=p[b];u.save(),u.beginPath(),u.rect(w.x*_,w.y*_,w.width*_,w.height*_),u.clip(),x(w),u.restore()}else u.save(),x(),u.restore();l.__drawIndex=s,l.__drawIndex<l.__endIndex&&(h=!1)},d=this,f=0;f<o.length;f++)p(f);return r.wxa&&E(this._layers,(function(t){t&&t.ctx&&t.ctx.draw&&t.ctx.draw()})),{finished:h,needsRefreshHover:c}},t.prototype._doPaintEl=function(t,e,n,i,r,o){var a=e.ctx;if(n){var s=t.getPaintRect();(!i||s&&s.intersect(i))&&(cv(a,t,r,o),t.setPrevPaintRect(s))}else cv(a,t,r,o)},t.prototype.getLayer=function(t,e){this._singleCanvas&&!this._needsManuallyCompositing&&(t=Qw);var n=this._layers[t];return n||((n=new $w(\"zr_\"+t,this,this.dpr)).zlevel=t,n.__builtin__=!0,this._layerConfig[t]?C(n,this._layerConfig[t],!0):this._layerConfig[t-tS]&&C(n,this._layerConfig[t-tS],!0),e&&(n.virtual=e),this.insertLayer(t,n),n.initContext()),n},t.prototype.insertLayer=function(t,e){var n=this._layers,i=this._zlevelList,r=i.length,o=this._domRoot,a=null,s=-1;if(!n[t]&&function(t){return!!t&&(!!t.__builtin__||\"function\"==typeof t.resize&&\"function\"==typeof t.refresh)}(e)){if(r>0&&t>i[0]){for(s=0;s<r-1&&!(i[s]<t&&i[s+1]>t);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;i<n.length;i++){var r=n[i];t.call(e,this._layers[r],r)}},t.prototype.eachBuiltinLayer=function(t,e){for(var n=this._zlevelList,i=0;i<n.length;i++){var r=n[i],o=this._layers[r];o.__builtin__&&t.call(e,o,r)}},t.prototype.eachOtherLayer=function(t,e){for(var n=this._zlevelList,i=0;i<n.length;i++){var r=n[i],o=this._layers[r];o.__builtin__||t.call(e,o,r)}},t.prototype.getLayers=function(){return this._layers},t.prototype._updateLayerStatus=function(t){function e(t){o&&(o.__endIndex!==t&&(o.__dirty=!0),o.__endIndex=t)}if(this.eachBuiltinLayer((function(t,e){t.__dirty=t.__used=!1})),this._singleCanvas)for(var n=1;n<t.length;n++){if((s=t[n]).zlevel!==t[n-1].zlevel||s.incremental){this._needsManuallyCompositing=!0;break}}var i,r,o=null,a=0;for(r=0;r<t.length;r++){var s,l=(s=t[r]).zlevel,u=void 0;i!==l&&(i=l,a=0),s.incremental?((u=this.getLayer(l+.001,this._needsManuallyCompositing)).incremental=!0,a=1):u=this.getLayer(l+(a>0?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<this._zlevelList.length;i++){var r=this._zlevelList[i];if(r===t||r===t+tS)C(this._layers[r],n[t],!0)}}},t.prototype.delLayer=function(t){var e=this._layers,n=this._zlevelList,i=e[t];i&&(i.dom.parentNode.removeChild(i.dom),delete e[t],n.splice(P(n,t),1))},t.prototype.resize=function(t,e){if(this._domRoot.style){var n=this._domRoot;n.style.display=\"none\";var i=this._opts,r=this.root;if(null!=t&&(i.width=t),null!=e&&(i.height=e),t=jy(r,0,i),e=jy(r,1,i),n.style.display=\"\",this._width!==t||e!==this._height){for(var o in n.style.width=t+\"px\",n.style.height=e+\"px\",this._layers)this._layers.hasOwnProperty(o)&&this._layers[o].resize(t,e);this.refresh(!0)}this._width=t,this._height=e}else{if(null==t||null==e)return;this._width=t,this._height=e,this.getLayer(Qw).resize(t,e)}return this},t.prototype.clearLayer=function(t){var e=this._layers[t];e&&e.clear()},t.prototype.dispose=function(){this.root.innerHTML=\"\",this.root=this.storage=this._domRoot=this._layers=null},t.prototype.getRenderedCanvas=function(t){if(t=t||{},this._singleCanvas&&!this._compositeManually)return this._layers[314159].dom;var e=new $w(\"image\",this,t.pixelRatio||this.dpr);e.initContext(),e.clear(!1,t.backgroundColor||this._backgroundColor);var n=e.ctx;if(t.pixelRatio<=this.dpr){this.refresh();var i=e.dom.width,r=e.dom.height;this.eachLayer((function(t){t.__builtin__?n.drawImage(t.dom,0,0,i,r):t.renderToCanvas&&(n.save(),t.renderToCanvas(n),n.restore())}))}else for(var o={inHover:!1,viewWidth:this._width,viewHeight:this._height},a=this.storage.getDisplayList(!0),s=0,l=a.length;s<l;s++){var u=a[s];cv(n,u,o,s===l-1)}return e.dom},t.prototype.getWidth=function(){return this._width},t.prototype.getHeight=function(){return this._height},t}();var nS=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){return vx(null,this,{useEncodeDefaulter:!0})},e.prototype.getLegendIcon=function(t){var e=new zr,n=Wy(\"line\",0,t.itemHeight/2,t.itemWidth,0,t.lineStyle.stroke,!1);e.add(n),n.setStyle(t.lineStyle);var i=this.getData().getVisual(\"symbol\"),r=this.getData().getVisual(\"symbolRotate\"),o=\"none\"===i?\"circle\":i,a=.8*t.itemHeight,s=Wy(o,(t.itemWidth-a)/2,(t.itemHeight-a)/2,a,a,t.itemStyle.fill);e.add(s),s.setStyle(t.itemStyle);var l=\"inherit\"===t.iconRotate?r:t.iconRotate||0;return s.rotation=l*Math.PI/180,s.setOrigin([t.itemWidth/2,t.itemHeight/2]),o.indexOf(\"empty\")>-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<n.length;a++)o.push(gf(t,e,n[a]));return o.join(\" \")}}function rS(t,e){var n=t.mapDimensionsAll(\"defaultedLabel\");if(!Y(e))return e+\"\";for(var i=[],r=0;r<n.length;r++){var o=t.getDimensionIndex(n[r]);o>=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;r<t.end;r++){var o=e.getItemLayout(r);if(sS(e,o,r,n)){var a=new this._SymbolCtor(e,r,this._seriesScope);a.traverse(i),a.setPosition(o),this.group.add(a),e.setItemGraphicEl(r,a),this._progressiveEls.push(a)}}},t.prototype.eachRendered=function(t){qh(this._progressiveEls||this.group,t)},t.prototype.remove=function(t){var e=this.group,n=this._data;n&&t?n.eachItemGraphicEl((function(t){t.fadeOut((function(){e.remove(t)}),n.hostModel)})):e.removeAll()},t}();function cS(t,e,n){var i=t.getBaseAxis(),r=t.getOtherAxis(i),o=function(t,e){var n=0,i=t.scale.getExtent();\"start\"===e?n=i[0]:\"end\"===e?n=i[1]:j(e)&&!isNaN(e)?n=e:i[0]>0?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<i;y++){var v=e[2*g],m=e[2*g+1];if(g>=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;)y++,g+=o,w=e[2*(b+=o)],S=e[2*b+1],x=(v=e[2*g])-u,_=(m=e[2*g+1])-h;var M=y+1;if(l)for(;gS(w,S)&&M<i;)M++,w=e[2*(b+=o)],S=e[2*b+1];var I=.5,T=0,C=0,D=void 0,A=void 0;if(M>=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<r&&gS(n[2*i],n[2*i+1]);i++);}for(;i<r;)i+=yS(t,n,i,r,r,1,e.smooth,e.smoothMonotone,e.connectNulls)+1},e.prototype.getPointOn=function(t,e){this.path||(this.createPathProxy(),this.buildPath(this.path,this.shape));for(var n,i,r=this.path.data,o=os.CMD,a=\"x\"===e,s=[],l=0;l<r.length;){var u=void 0,h=void 0,c=void 0,p=void 0,d=void 0,f=void 0,g=void 0;switch(r[l++]){case o.M:n=r[l++],i=r[l++];break;case o.L:if(u=r[l++],h=r[l++],(g=a?(t-n)/(u-n):(t-i)/(h-i))<=1&&g>=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<v;m++){var x=s[m];if(x<=1&&x>=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<o&&gS(n[2*r],n[2*r+1]);r++);}for(;r<o;){var s=yS(t,n,r,o,o,1,e.smooth,a,e.connectNulls);yS(t,i,r+s-1,s,o,-1,e.stackedOnSmooth,a,e.connectNulls),r+=s+1,t.closePath()}},e}(Is);function bS(t,e,n,i,r){var o=t.getArea(),a=o.x,s=o.y,l=o.width,u=o.height,h=n.get([\"lineStyle\",\"width\"])||2;a-=h/2,s-=h/2,l+=h,u+=h,a=Math.floor(a),l=Math.round(l);var c=new zs({shape:{x:a,y:s,width:l,height:u}});if(e){var p=t.getBaseAxis(),d=p.isHorizontal(),f=p.inverse;d?(f&&(c.shape.x+=l),c.shape.width=0):(f||(c.shape.y+=u),c.shape.height=0);var g=X(r)?function(t){r(t,c)}:null;gh(c,{shape:{width:l,height:u,x:a,y:s}},n,null,i,g)}return c}function wS(t,e,n){var i=t.getArea(),r=Zr(i.r0,1),o=Zr(i.r,1),a=new zu({shape:{cx:Zr(t.cx,1),cy:Zr(t.cy,1),r0:r,r:o,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}});e&&(\"angle\"===t.getBaseAxis().dim?a.shape.endAngle=i.startAngle:a.shape.r=r,gh(a,{shape:{endAngle:i.endAngle,r:o}},n));return a}function SS(t,e,n,i,r){return t?\"polar\"===t.type?wS(t,e,n):\"cartesian2d\"===t.type?bS(t,e,n,i,r):null:null}function MS(t,e){return t.type===e}function IS(t,e){if(t.length===e.length){for(var n=0;n<t.length;n++)if(t[n]!==e[n])return;return!0}}function TS(t){for(var e=1/0,n=1/0,i=-1/0,r=-1/0,o=0;o<t.length;){var a=t[o++],s=t[o++];isNaN(a)||(e=Math.min(a,e),i=Math.max(a,i)),isNaN(s)||(n=Math.min(s,n),r=Math.max(s,r))}return[[e,n],[i,r]]}function CS(t,e){var n=TS(t),i=n[0],r=n[1],o=TS(e),a=o[0],s=o[1];return Math.max(Math.abs(i[0]-a[0]),Math.abs(i[1]-a[1]),Math.abs(r[0]-s[0]),Math.abs(r[1]-s[1]))}function DS(t){return j(t)?t:t?.5:0}function AS(t,e,n,i){var r=e.getBaseAxis(),o=\"x\"===r.dim||\"radius\"===r.dim?0:1,a=[],s=0,l=[],u=[],h=[],c=[];if(i){for(s=0;s<t.length;s+=2)isNaN(t[s])||isNaN(t[s+1])||c.push(t[s],t[s+1]);t=c}for(s=0;s<t.length-2;s+=2)switch(h[0]=t[s+2],h[1]=t[s+3],u[0]=t[s],u[1]=t[s+1],a.push(u[0],u[1]),n){case\"end\":l[o]=h[o],l[1-o]=u[1-o],a.push(l[0],l[1]);break;case\"middle\":var p=(u[o]+h[o])/2,d=[];l[o]=d[o]=p,l[1-o]=u[1-o],d[1-o]=h[1-o],a.push(l[0],l[1]),a.push(d[0],d[1]);break;default:l[o]=u[o],l[1-o]=h[1-o],a.push(l[0],l[1])}return a.push(t[s++],t[s++]),a}function kS(t,e,n){var i=t.getVisual(\"visualMeta\");if(i&&i.length&&t.count()&&\"cartesian2d\"===e.type){for(var r,o,a=i.length-1;a>=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;s<o;s++){var l=t[s],u=l.coord;if(u<0)n=l;else{if(u>e){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;a<r;a+=o)if(1.5*oS.getSymbolSize(e,a)[t.isHorizontal()?1:0]>i)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;e<ol.length;e++)if(t.get([ol[e],\"endLabel\",\"show\"]))return!0;return!1}function RS(t,e,n,i){if(MS(e,\"cartesian2d\")){var r=i.getModel(\"endLabel\"),o=r.get(\"valueAnimation\"),a=i.getData(),s={lastFrameIndex:0},l=OS(i)?function(n,i){t._endLabelOnDuring(n,i,a,s,o,r,e)}:null,u=e.getBaseAxis().isHorizontal(),h=bS(e,n,i,(function(){var e=t._endLabel;e&&n&&null!=s.originalX&&e.attr({x:s.originalX,y:s.originalY})}),l);if(!i.get(\"clip\",!0)){var c=h.shape,p=Math.max(c.width,c.height);u?(c.y-=p,c.height+=2*p):(c.x-=p,c.width+=2*p)}return l&&l(1,h),h}return wS(e,n,i)}var NS=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(){var t=new zr,e=new hS;this.group.add(e.group),this._symbolDraw=e,this._lineGroup=t},e.prototype.render=function(t,e,n){var i=this,r=t.coordinateSystem,o=this.group,a=t.getData(),s=t.getModel(\"lineStyle\"),l=t.getModel(\"areaStyle\"),u=a.getLayout(\"points\")||[],h=\"polar\"===r.type,c=this._coordSys,p=this._symbolDraw,d=this._polyline,f=this._polygon,g=this._lineGroup,y=!e.ssr&&t.isAnimationEnabled(),v=!l.isEmpty(),m=l.get(\"origin\"),x=cS(r,a,m),_=v&&function(t,e,n){if(!n.valueDim)return[];for(var i=e.count(),r=Ex(2*i),o=0;o<i;o++){var a=pS(n,t,e,o);r[2*o]=a[0],r[2*o+1]=a[1]}return r}(r,a,x),b=t.get(\"showSymbol\"),w=t.get(\"connectNulls\"),S=b&&!h&&LS(t,a,r),M=this._data;M&&M.eachItemGraphicEl((function(t,e){t.__temp&&(o.remove(t),M.setItemGraphicEl(e,null))})),b||p.remove(),o.add(g);var I,T=!h&&t.get(\"step\");r&&r.getArea&&t.get(\"clip\",!0)&&(null!=(I=r.getArea()).width?(I.x-=.1,I.y-=.1,I.width+=.2,I.height+=.2):I.r0&&(I.r0-=.5,I.r+=.5)),this._clipShapeForSymbol=I;var C=kS(a,r,n)||a.getVisual(\"style\")[a.getVisual(\"drawType\")];if(d&&c.type===r.type&&T===this._step){v&&!f?f=this._newPolygon(u,_):f&&!v&&(g.remove(f),f=this._polygon=null),h||this._initOrUpdateEndLabel(t,r,_p(C));var D=g.getClipPath();if(D)gh(D,{shape:RS(this,r,!1,t).shape},t);else g.setClipPath(RS(this,r,!0,t));b&&p.updateData(a,{isIgnore:S,clipShape:I,disableAnimation:!0,getSymbolPoint:function(t){return[u[2*t],u[2*t+1]]}}),IS(this._stackedOnPoints,_)&&IS(this._points,u)||(y?this._doUpdateAnimation(a,_,r,n,T,m,w):(T&&(u=AS(u,r,T,w),_&&(_=AS(_,r,T,w))),d.setShape({points:u}),f&&f.setShape({points:u,stackedOnPoints:_})))}else b&&p.updateData(a,{isIgnore:S,clipShape:I,disableAnimation:!0,getSymbolPoint:function(t){return[u[2*t],u[2*t+1]]}}),y&&this._initSymbolLabelAnimation(a,r,I),T&&(u=AS(u,r,T,w),_&&(_=AS(_,r,T,w))),d=this._newPolyline(u),v?f=this._newPolygon(u,_):f&&(g.remove(f),f=this._polygon=null),h||this._initOrUpdateEndLabel(t,r,_p(C)),g.setClipPath(RS(this,r,!0,t));var A=t.getModel(\"emphasis\"),L=A.get(\"focus\"),P=A.get(\"blurScope\"),O=A.get(\"disabled\");(d.useStyle(k(s.getLineStyle(),{fill:\"none\",stroke:C,lineJoin:\"bevel\"})),jl(d,t,\"lineStyle\"),d.style.lineWidth>0&&\"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<o;u++)if(r=t[2*u+a],!isNaN(r)&&!isNaN(t[2*u+1-a]))if(0!==u){if(i<=e&&r>=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;x<l.length;x++){var _=l[x],b=!0,w=void 0,S=void 0;switch(_.cmd){case\"=\":w=2*_.idx,S=2*_.idx1;var M=v[w],I=v[w+1],T=m[S],C=m[S+1];(isNaN(M)||isNaN(I))&&(M=T,I=C),u.push(M,I),h.push(T,C),c.push(n[w],n[w+1]),p.push(i[S],i[S+1]),g.push(e.getRawIndex(_.idx1));break;case\"+\":var D=_.idx,A=y.dataDimsForPoint,k=r.dataToPoint([e.get(A[0],D),e.get(A[1],D)]);S=2*D,u.push(k[0],k[1]),h.push(m[S],m[S+1]);var L=pS(y,r,e,D);c.push(L[0],L[1]),p.push(i[S],i[S+1]),g.push(e.getRawIndex(D));break;case\"-\":b=!1}b&&(d.push(_),f.push(f.length))}f.sort((function(t,e){return g[t]-g[e]}));var P=u.length,O=Ex(P),R=Ex(P),N=Ex(P),E=Ex(P),z=[];for(x=0;x<f.length;x++){var V=f[x],B=2*x,F=2*V;O[B]=u[F],O[B+1]=u[F+1],R[B]=h[F],R[B+1]=h[F+1],N[B]=c[F],N[B+1]=c[F+1],E[B]=p[F],E[B+1]=p[F+1],z[x]=d[V]}return{current:O,next:R,stackedOnCurrent:N,stackedOnNext:E,status:z}}(this._data,t,this._stackedOnPoints,e,this._coordSys,0,this._valueOrigin),c=h.current,p=h.stackedOnCurrent,d=h.next,f=h.stackedOnNext;if(r&&(c=AS(h.current,n,r,a),p=AS(h.stackedOnCurrent,n,r,a),d=AS(h.next,n,r,a),f=AS(h.stackedOnNext,n,r,a)),CS(c,d)>3e3||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;m<v.length;m++){if(\"=\"===v[m].cmd){var x=t.getItemGraphicEl(v[m].idx1);x&&y.push({el:x,ptIdx:m})}}s.animators&&s.animators.length&&s.animators[0].during((function(){l&&l.dirtyShape();for(var t=s.shape.__points,e=0;e<y.length;e++){var n=y[e].el,i=2*y[e].ptIdx;n.x=t[i],n.y=t[i+1],n.markRedraw()}}))},e.prototype.remove=function(t){var e=this.group,n=this._data;this._lineGroup.removeAll(),this._symbolDraw.remove(!0),n&&n.eachItemGraphicEl((function(t,i){t.__temp&&(e.remove(t),n.setItemGraphicEl(i,null))})),this._polyline=this._polygon=this._coordSys=this._points=this._stackedOnPoints=this._endLabel=this._data=null},e.type=\"line\",e}(kg);function ES(t,e){return{seriesType:t,plan:Cg(),reset:function(t){var n=t.getData(),i=t.coordinateSystem,r=t.pipelineContext,o=e||r.large;if(i){var a=z(i.dimensions,(function(t){return n.mapDimension(t)})).slice(0,2),s=a.length,l=n.getCalculationInfo(\"stackResultDimension\");gx(n,a[0])&&(a[0]=l),gx(n,a[1])&&(a[1]=l);var u=n.getStore(),h=n.getDimensionIndex(a[0]),c=n.getDimensionIndex(a[1]);return s&&{progress:function(t,e){for(var n=t.end-t.start,r=o&&Ex(n*s),a=[],l=[],p=t.start,d=0;p<t.end;p++){var f=void 0;if(1===s){var g=u.get(h,p);f=i.dataToPoint(g,null,l)}else a[0]=u.get(h,p),a[1]=u.get(c,p),f=i.dataToPoint(a,null,l);o?(r[d++]=f[0],r[d++]=f[1]):e.setItemLayout(p,f.slice())}o&&e.setLayout(\"points\",r)}}}}}}var zS={average:function(t){for(var e=0,n=0,i=0;i<t.length;i++)isNaN(t[i])||(e+=t[i],n++);return 0===n?NaN:e/n},sum:function(t){for(var e=0,n=0;n<t.length;n++)e+=t[n]||0;return e},max:function(t){for(var e=-1/0,n=0;n<t.length;n++)t[n]>e&&(e=t[n]);return isFinite(e)?e:NaN},min:function(t){for(var e=1/0,n=0;n<t.length;n++)t[n]<e&&(e=t[n]);return isFinite(e)?e:NaN},nearest:function(t){return t[0]}},VS=function(t){return Math.round(t.length/2)};function BS(t){return{seriesType:t,reset:function(t,e,n){var i=t.getData(),r=t.get(\"sampling\"),o=t.coordinateSystem,a=i.count();if(a>10&&\"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;c<i.length;c++){var p=i[c].coord,d=c===i.length-1?i[c-1].tickValue+h:i[c].tickValue;if(d===a){u=p;break}if(d<a)l=p;else if(null!=l&&d>a){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-l<c:l-u<c;p||(l=u-(h?c:-c));var d=Math.cos(l),f=Math.sin(l),g=Math.cos(u),y=Math.sin(u);p?(t.moveTo(d*r+n,f*r+i),t.arc(d*s+n,f*s+i,a,-Math.PI+l,l,!h)):t.moveTo(d*o+n,f*o+i),t.arc(n,i,o,l,u,!h),t.arc(g*s+n,y*s+i,a,u-2*Math.PI,u-Math.PI,!h),0!==r&&t.arc(n,i,r,u,l,h)},e}(Is);function YS(t,e,n){return e*Math.sin(t)*(n?-1:1)}function XS(t,e,n){return e*Math.cos(t)*(n?1:-1)}function US(t,e,n){var i=t.get(\"borderRadius\");if(null==i)return n?{cornerRadius:0}:null;Y(i)||(i=[i,i,i,i]);var r=Math.abs(e.r||0-e.r0||0);return{cornerRadius:z(i,(function(t){return Ir(t,r)}))}}var ZS=Math.max,jS=Math.min;var qS=function(t){function e(){var n=t.call(this)||this;return n.type=e.type,n._isFirstFrame=!0,n}return n(e,t),e.prototype.render=function(t,e,n,i){this._model=t,this._removeOnRenderedListener(n),this._updateDrawMode(t);var r=t.get(\"coordinateSystem\");(\"cartesian2d\"===r||\"polar\"===r)&&(this._progressiveEls=null,this._isLargeDraw?this._renderLarge(t,e,n):this._renderNormal(t,e,n,i))},e.prototype.incrementalPrepareRender=function(t){this._clear(),this._updateDrawMode(t),this._updateLargeClip(t)},e.prototype.incrementalRender=function(t,e){this._progressiveEls=[],this._incrementalRenderLarge(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,e,n,i){var r,o=this.group,a=t.getData(),s=this._data,l=t.coordinateSystem,u=l.getBaseAxis();\"cartesian2d\"===l.type?r=u.isHorizontal():\"polar\"===l.type&&(r=\"angle\"===u.dim);var h=t.isAnimationEnabled()?t:null,c=function(t,e){var n=t.get(\"realtimeSort\",!0),i=e.getBaseAxis();0;if(n&&\"category\"===i.type&&\"cartesian2d\"===e.type)return{baseAxis:i,otherAxis:e.getOtherAxis(i)}}(t,l);c&&this._enableRealtimeSort(c,a,n);var p=t.get(\"clip\",!0)||c,d=function(t,e){var n=t.getArea&&t.getArea();if(MS(t,\"cartesian2d\")){var i=t.getBaseAxis();if(\"category\"!==i.type||!i.onBand){var r=e.getLayout(\"bandWidth\");i.isHorizontal()?(n.x-=r,n.width+=2*r):(n.y-=r,n.height+=2*r)}}return n}(l,a);o.removeClipPath();var f=t.get(\"roundCap\",!0),g=t.get(\"showBackground\",!0),y=t.getModel(\"backgroundStyle\"),v=y.get(\"borderRadius\")||0,m=[],x=this._backgroundEls,_=i&&i.isInitSort,b=i&&\"changeAxisOrder\"===i.type;function w(t){var e=iM[l.type](a,t),n=function(t,e,n){var i=\"polar\"===t.type?zu:zs;return new i({shape:hM(e,n,t),silent:!0,z2:0})}(l,r,e);return n.useStyle(y.getItemStyle()),\"cartesian2d\"===l.type?n.setShape(\"r\",v):n.setShape(\"cornerRadius\",v),m[t]=n,n}a.diff(s).add((function(e){var n=a.getItemModel(e),i=iM[l.type](a,e,n);if(g&&w(e),a.hasValue(e)&&nM[l.type](i)){var s=!1;p&&(s=KS[l.type](d,i));var y=$S[l.type](t,a,e,i,r,h,u.model,!1,f);c&&(y.forceLabelAnimation=!0),oM(y,a,e,n,i,t,r,\"polar\"===l.type),_?y.attr({shape:i}):c?JS(c,h,y,i,e,r,!1,!1):gh(y,{shape:i},t,e),a.setItemGraphicEl(e,y),o.add(y),y.ignore=s}})).update((function(e,n){var i=a.getItemModel(e),S=iM[l.type](a,e,i);if(g){var M=void 0;0===x.length?M=w(n):((M=x[n]).useStyle(y.getItemStyle()),\"cartesian2d\"===l.type?M.setShape(\"r\",v):M.setShape(\"cornerRadius\",v),m[e]=M);var I=iM[l.type](a,e);fh(M,{shape:hM(r,I,l)},h,e)}var T=s.getItemGraphicEl(n);if(a.hasValue(e)&&nM[l.type](S)){var C=!1;if(p&&(C=KS[l.type](d,S))&&o.remove(T),T?_h(T):T=$S[l.type](t,a,e,S,r,h,u.model,!!T,f),c&&(T.forceLabelAnimation=!0),b){var D=T.getTextContent();if(D){var A=uc(D);null!=A.prevValue&&(A.prevValue=A.value)}}else oM(T,a,e,i,S,t,r,\"polar\"===l.type);_?T.attr({shape:S}):c?JS(c,h,T,S,e,r,!0,b):fh(T,{shape:S},t,e,null),a.setItemGraphicEl(e,T),T.ignore=C,o.add(T)}else o.remove(T)})).remove((function(e){var n=s.getItemGraphicEl(e);n&&xh(n,t,e)})).execute();var S=this._backgroundGroup||(this._backgroundGroup=new zr);S.removeAll();for(var M=0;M<m.length;++M)S.add(m[M]);o.add(S),this._backgroundEls=m,this._data=a},e.prototype._renderLarge=function(t,e,n){this._clear(),lM(t,this.group),this._updateLargeClip(t)},e.prototype._incrementalRenderLarge=function(t,e){this._removeBackground(),lM(e,this.group,this._progressiveEls,!0)},e.prototype._updateLargeClip=function(t){var e=t.get(\"clip\",!0)&&SS(t.coordinateSystem,!1,t),n=this.group;e?n.setClipPath(e):n.removeClipPath()},e.prototype._enableRealtimeSort=function(t,e,n){var i=this;if(e.count()){var r=t.baseAxis;if(this._isFirstFrame)this._dispatchInitSort(e,t,n),this._isFirstFrame=!1;else{var o=function(t){var n=e.getItemGraphicEl(t),i=n&&n.shape;return i&&Math.abs(r.isHorizontal()?i.height:i.width)||0};this._onRendered=function(){i._updateSortWithinSameData(e,o,r,n)},n.getZr().on(\"rendered\",this._onRendered)}}},e.prototype._dataSort=function(t,e,n){var i=[];return t.each(t.mapDimension(e.dim),(function(t,e){var r=n(e);r=null==r?NaN:r,i.push({dataIndex:e,mappedValue:r,ordinalNumber:t})})),i.sort((function(t,e){return e.mappedValue-t.mappedValue})),{ordinalNumbers:z(i,(function(t){return t.ordinalNumber}))}},e.prototype._isOrderChangedWithinSameData=function(t,e,n){for(var i=n.scale,r=t.mapDimension(n.dim),o=Number.MAX_VALUE,a=0,s=i.getOrdinalMeta().categories.length;a<s;++a){var l=t.rawIndexOf(r,i.getRawOrdinalNumber(a)),u=l<0?Number.MIN_VALUE:e(t.indexOfRawIndex(l));if(u>o)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=s<a,c=u<l;return e.x=h&&a>r?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;n<e.length;n++)if(!isFinite(t[e[n]]))return!0;return!1}var tM=[\"x\",\"y\",\"width\",\"height\"],eM=[\"cx\",\"cy\",\"r\",\"startAngle\",\"endAngle\"],nM={cartesian2d:function(t){return!QS(t,tM)},polar:function(t){return!QS(t,eM)}},iM={cartesian2d:function(t,e,n){var i=t.getItemLayout(e),r=n?function(t,e){var n=t.get([\"itemStyle\",\"borderColor\"]);if(!n||\"none\"===n)return 0;var i=t.get([\"itemStyle\",\"borderWidth\"])||0,r=isNaN(e.width)?Number.MAX_VALUE:Math.abs(e.width),o=isNaN(e.height)?Number.MAX_VALUE:Math.abs(e.height);return Math.min(i,r,o)}(n,i):0,o=i.width>0?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<n.length;l+=3)a[i]=s,a[r]=n[l+2],o[i]=n[l+i],o[r]=n[l+r],t.rect(o[0],o[1],a[0],a[1])},e}(Is);function lM(t,e,n,i){var r=t.getData(),o=r.getLayout(\"valueAxisHorizontal\")?1:0,a=r.getLayout(\"largeDataIndices\"),s=r.getLayout(\"size\"),l=t.getModel(\"backgroundStyle\"),u=r.getLayout(\"largeBackgroundPoints\");if(u){var h=new sM({shape:{points:u},incremental:!!i,silent:!0,z2:0});h.baseDimIdx=o,h.largeDataIndices=a,h.barWidth=s,h.useStyle(l.getItemStyle()),e.add(h),n&&n.push(h)}var c=new sM({shape:{points:r.getLayout(\"largePoints\")},incremental:!!i,ignoreCoarsePointer:!0,z2:1});c.baseDimIdx=o,c.largeDataIndices=a,c.barWidth=s,e.add(c),c.useStyle(r.getVisual(\"style\")),Qs(c).seriesIndex=t.seriesIndex,t.get(\"silent\")||(c.on(\"mousedown\",uM),c.on(\"mousemove\",uM)),n&&n.push(c)}var uM=Bg((function(t){var e=function(t,e,n){for(var i=t.baseDimIdx,r=1-i,o=t.shape.points,a=t.largeDataIndices,s=[],l=[],u=t.barWidth,h=0,c=o.length/3;h<c;h++){var p=3*h;if(l[i]=u,l[r]=o[p+2],s[i]=o[p+i],s[r]=o[p+r],l[r]<0&&(s[r]+=l[r],l[r]=-l[r]),e>=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)<c?(i=c,x-=c):_+=t;var r=b+w*i;e.setItemLayout(n,{angle:i,startAngle:b,endAngle:r,clockwise:g,cx:a,cy:s,r0:u,r:y?Xr(t,m,[u,l]):l}),b=r}})),x<cM&&p)if(x<=.001){var S=cM/p;e.each(i,(function(t,n){if(!isNaN(t)){var i=e.getItemLayout(n);i.angle=S,i.startAngle=h+w*n*S,i.endAngle=h+w*(n+1)*S}}))}else f=x/_,b=h,e.each(i,(function(t,n){if(!isNaN(t)){var i=e.getItemLayout(n),r=i.angle===c?c:t*f;i.startAngle=b,i.endAngle=b+w*r,b+=w*r}}))}))}function yM(t){return{seriesType:t,reset:function(t,e){var n=e.findComponents({mainType:\"legend\"});if(n&&n.length){var i=t.getData();i.filterSelf((function(t){for(var e=i.getName(t),r=0;r<n.length;r++)if(!n[r].isSelected(e))return!1;return!0}))}}}}var vM=Math.PI/180;function mM(t,e,n,i,r,o,a,s,l,u){if(!(t.length<2)){for(var h=t.length,c=0;c<h;c++)if(\"outer\"===t[c].position&&\"labelLine\"===t[c].labelAlignTo){var p=t[c].label.x-u;t[c].linePoints[1][0]+=p,t[c].label.x=u}kb(t,l,l+a)&&function(t){for(var o={list:[],maxY:0},a={list:[],maxY:0},s=0;s<t.length;s++)if(\"none\"===t[s].labelAlignTo){var l=t[s],u=l.label.y>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)<p?Math.sqrt(h*h/(1-c*c/p/p)):p;u.rB=f,u.maxY=h}u.list.push(l)}d(o),d(a)}(t)}function d(t){for(var o=t.rB,a=o*o,s=0;s<t.list.length;s++){var l=t.list[s],u=Math.abs(l.label.y-n),h=i+l.len,c=h*h,p=Math.sqrt((1-Math.abs(u*u/a))*c),d=e+(p+l.len2)*r,f=d-l.label.x;xM(l,l.targetTextWidth-f*r,!0),l.label.x=d}}}function xM(t,e,n){if(void 0===n&&(n=!1),null==t.labelStyleWidth){var i=t.label,r=i.style,o=t.rect,a=r.backgroundColor,s=r.padding,l=s?s[1]+s[3]:0,u=r.overflow,h=o.width+(a?0:l);if(e<h||n){var c=o.height;if(u&&u.match(\"break\")){i.setStyle(\"backgroundColor\",null),i.setStyle(\"width\",e-l);var p=i.getBoundingRect();i.setStyle(\"width\",Math.ceil(p.width)),i.setStyle(\"backgroundColor\",a)}else{var d=e-l,f=e<h?d:n?d>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)<a)return E(p.states,d),p.ignore=!0,void(f&&(E(f.states,d),f.ignore=!0));if(function(t){if(!t.ignore)return!0;for(var e in t.states)if(!1===t.states[e].ignore)return!0;return!1}(p)){var I,T,C,D,A=(c.startAngle+c.endAngle)/2,k=Math.cos(A),L=Math.sin(A);e=c.cx,n=c.cy;var P=\"inside\"===v||\"inner\"===v;if(\"center\"===v)I=c.cx,T=c.cy,D=\"center\";else{var O=(P?(c.r+c.r0)/2*k:c.r*k)+e,R=(P?(c.r+c.r0)/2*L:c.r*L)+n;if(I=O+3*k,T=R+3*L,!P){var N=O+k*(S+l-c.r),z=R+L*(S+l-c.r),V=N+(k<0?-1:1)*M;I=\"edge\"===x?k<0?h+_:h+u-_:V+(k<0?-m:m),T=z,C=[[O,R],[N,z],[V,z]]}D=P?\"center\":\"edge\"===x?k>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;p<t.length;p++){var d=t[p].label;_M(t[p])||(d.x<e?(h=Math.min(h,d.x),l.push(t[p])):(c=Math.max(c,d.x),u.push(t[p])))}for(p=0;p<t.length;p++)if(!_M(y=t[p])&&y.linePoints){if(null!=y.labelStyleWidth)continue;d=y.label;var f=y.linePoints,g=void 0;g=\"edge\"===y.labelAlignTo?d.x<e?f[2][0]-y.labelDistance-a-y.edgeDistance:a+r-y.edgeDistance-f[2][0]-y.labelDistance:\"labelLine\"===y.labelAlignTo?d.x<e?h-a-y.bleedMargin:a+r-c-y.bleedMargin:d.x<e?d.x-a-y.bleedMargin:a+r-d.x-y.bleedMargin,y.targetTextWidth=g,xM(y,g)}for(mM(u,e,n,i,1,0,o,0,s,c),mM(l,e,n,i,-1,0,o,0,s,h),p=0;p<t.length;p++){var y;if(!_M(y=t[p])&&y.linePoints){d=y.label,f=y.linePoints;var v=\"edge\"===y.labelAlignTo,m=d.style.padding,x=m?m[1]+m[3]:0,_=d.style.backgroundColor?0:x,b=y.rect.width+_,w=f[1][0]-f[2][0];v?d.x<e?f[2][0]=a+y.edgeDistance+b+y.labelDistance:f[2][0]=a+r-y.edgeDistance-b-y.labelDistance:(d.x<e?f[2][0]=d.x+y.labelDistance:f[2][0]=d.x-y.labelDistance,f[1][0]=f[2][0]+w),f[1][1]=f[2][1]=d.y}}}(r,e,n,l,u,p,h,c);for(var f=0;f<r.length;f++){var g=r[f],y=g.label,v=g.labelLine,m=isNaN(y.x)||isNaN(y.y);if(y){y.setStyle({align:g.textAlign}),m&&(E(y.states,d),y.ignore=!0);var x=y.states.select;x&&(x.x+=y.x,x.y+=y.y)}if(v){var _=g.linePoints;m||!_?(E(v.states,d),v.ignore=!0):(wb(_,g.minTurnAngle),Sb(_,g.surfaceNormal,g.maxSurfaceAngle),v.setShape({points:_}),y.__hostTarget.textGuideLineConfig={anchor:new De(_[0][0],_[0][1])})}}}var wM=function(t){function e(e,n,i){var r=t.call(this)||this;r.z2=2;var o=new Fs;return r.setTextContent(o),r.updateData(e,n,i,!0),r}return n(e,t),e.prototype.updateData=function(t,e,n,i){var r=this,o=t.hostModel,a=t.getItemModel(e),s=a.getModel(\"emphasis\"),l=t.getItemLayout(e),u=A(US(a.getModel(\"itemStyle\"),l,!0),l);if(isNaN(u.startAngle))r.setShape(u);else{if(i){r.setShape(u);var h=o.getShallow(\"animationType\");o.ecModel.ssr?(gh(r,{scaleX:0,scaleY:0},o,{dataIndex:e,isFrom:!0}),r.originX=u.cx,r.originY=u.cy):\"scale\"===h?(r.shape.r=l.r0,gh(r,{shape:{r:l.r}},o,e)):null!=n?(r.setShape({startAngle:n,endAngle:n}),gh(r,{shape:{startAngle:l.startAngle,endAngle:l.endAngle}},o,e)):(r.shape.endAngle=l.startAngle,fh(r,{shape:{endAngle:l.endAngle}},o,e))}else _h(r),fh(r,{shape:u},o,e);r.useStyle(t.getItemVisual(e,\"style\")),jl(r,a);var c=(l.startAngle+l.endAngle)/2,p=o.get(\"selectedOffset\"),d=Math.cos(c)*p,f=Math.sin(c)*p,g=a.getShallow(\"cursor\");g&&r.attr(\"cursor\",g),this._updateLabel(o,t,e),r.ensureState(\"emphasis\").shape=A({r:l.r+(s.get(\"scale\")&&s.get(\"scaleSize\")||0)},US(s.getModel(\"itemStyle\"),l)),A(r.ensureState(\"select\"),{x:d,y:f,shape:US(a.getModel([\"select\",\"itemStyle\"]),l)}),A(r.ensureState(\"blur\"),{shape:US(a.getModel([\"blur\",\"itemStyle\"]),l)});var y=r.getTextGuideLine(),v=r.getTextContent();y&&A(y.ensureState(\"select\"),{x:d,y:f}),A(v.ensureState(\"select\"),{x:d,y:f}),Yl(this,s.get(\"focus\"),s.get(\"blurScope\"),s.get(\"disabled\"))}},e.prototype._updateLabel=function(t,e,n){var i=this,r=e.getItemModel(n),o=r.getModel(\"labelLine\"),a=e.getItemVisual(n,\"style\"),s=a&&a.fill,l=a&&a.opacity;tc(i,ec(r),{labelFetcher:e.hostModel,labelDataIndex:n,inheritColor:s,defaultOpacity:l,defaultText:t.getFormattedLabel(n,\"normal\")||e.getName(n)});var u=i.getTextContent();i.setTextConfig({position:null,rotation:null}),u.attr({z2:10});var h=t.get([\"label\",\"position\"]);if(\"outside\"!==h&&\"outer\"!==h)i.removeTextGuideLine();else{var c=this.getTextGuideLine();c||(c=new Yu,this.setTextGuideLine(c)),Tb(this,Cb(r),{stroke:s,opacity:ot(o.get([\"lineStyle\",\"opacity\"]),l,1)})}},e}(zu),SM=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.ignoreLabelLineUpdate=!0,e}return n(e,t),e.prototype.render=function(t,e,n,i){var r,o=t.getData(),a=this._data,s=this.group;if(!a&&o.count()>0){for(var l=o.getItemLayout(0),u=1;isNaN(l&&l.startAngle)&&u<o.count();++u)l=o.getItemLayout(u);l&&(r=l.startAngle)}if(this._emptyCircleSector&&s.remove(this._emptyCircleSector),0===o.count()&&t.get(\"showEmptyCircle\")){var h=new zu({shape:fM(t,n)});h.useStyle(t.getModel(\"emptyCircleStyle\").getItemStyle()),this._emptyCircleSector=h,s.add(h)}o.diff(a).add((function(t){var e=new wM(o,t,r);o.setItemGraphicEl(t,e),s.add(e)})).update((function(t,e){var n=a.getItemGraphicEl(e);n.updateData(o,t,r),n.off(\"click\"),s.add(n),o.setItemGraphicEl(t,n)})).remove((function(e){xh(a.getItemGraphicEl(e),t,e)})).execute(),bM(t),\"expansion\"!==t.get(\"animationTypeUpdate\")&&(this._data=o)},e.prototype.dispose=function(){},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=\"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<i.length;){var h=i[n++],c=i[n++];isNaN(h)||isNaN(c)||(u&&!u.contain(h,c)||(a.x=h-r[0]/2,a.y=c-r[1]/2,a.width=r[0],a.height=r[1],o.buildPath(t,a,!0)))}this.incremental&&(this._off=n,this.notClear=!0)}},e.prototype.afterBrush=function(){var t,e=this.shape,n=e.points,i=e.size,r=this._ctx,o=this.softClipShape;if(r){for(t=this._off;t<n.length;){var a=n[t++],s=n[t++];isNaN(a)||isNaN(s)||(o&&!o.contain(a,s)||r.fillRect(a-i[0]/2,s-i[1]/2,i[0],i[1]))}this.incremental&&(this._off=t,this.notClear=!0)}},e.prototype.findDataIndex=function(t,e){for(var n=this.shape,i=n.points,r=n.size,o=Math.max(r[0],4),a=Math.max(r[1],4),s=i.length/2-1;s>=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<n.length;){var c=n[h++],p=n[h++];a=Math.min(c,a),l=Math.max(c,l),s=Math.min(p,s),u=Math.max(p,u)}t=this._rect=new ze(a-r/2,s-o/2,l-a+r,u-s+o)}return t},e}(Is),LM=function(){function t(){this.group=new zr}return t.prototype.updateData=function(t,e){this._clear();var n=this._create();n.setShape({points:t.getLayout(\"points\")}),this._setCommon(n,t,e)},t.prototype.updateLayout=function(t){var e=t.getLayout(\"points\");this.group.eachChild((function(t){if(null!=t.startIndex){var n=2*(t.endIndex-t.startIndex),i=4*t.startIndex*2;e=new Float32Array(e.buffer,i,n)}t.setShape(\"points\",e),t.reset()}))},t.prototype.incrementalPrepareUpdate=function(t){this._clear()},t.prototype.incrementalUpdate=function(t,e,n){var i=this._newAdded[0],r=e.getLayout(\"points\"),o=i&&i.shape.points;if(o&&o.length<2e4){var a=o.length,s=new Float32Array(a+r.length);s.set(o),s.set(r,a),i.endIndex=t.end,i.setShape({points:s})}else{this._newAdded=[];var l=this._create();l.startIndex=t.start,l.endIndex=t.end,l.incremental=!0,l.setShape({points:r}),this._setCommon(l,e,n)}},t.prototype.eachRendered=function(t){this._newAdded[0]&&t(this._newAdded[0])},t.prototype._create=function(){var t=new kM({cursor:\"default\"});return t.ignoreCoarsePointer=!0,this.group.add(t),this._newAdded.push(t),t},t.prototype._setCommon=function(t,e,n){var i=e.hostModel;n=n||{};var r=e.getVisual(\"symbolSize\");t.setShape(\"size\",r instanceof Array?r:[r,r]),t.softClipShape=n.clipShape||null,t.symbolProxy=Wy(e.getVisual(\"symbol\"),0,0,0,0),t.setColor=t.symbolProxy.setColor;var o=t.shape.size[0]<4;t.useStyle(i.getModel(\"itemStyle\").getItemStyle(o?[\"color\",\"shadowBlur\",\"shadowColor\"]:[\"color\"]));var a=e.getVisual(\"style\"),s=a&&a.fill;s&&t.setColor(s);var l=Qs(t);l.seriesIndex=i.seriesIndex,t.on(\"mousemove\",(function(e){l.dataIndex=null;var n=t.hoverDataIdx;n>=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;y<u[1]&&isFinite(y)&&isFinite(u[1]);)f=Ix(f),y=u[0]+f*a;else if(c)for(g=u[1]-f*a;g>u[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;h<r;h+=u){var c=l(i?i[h]:{value:o[0]+h},h),p=b_(s.getTextRect(c),s.get(\"rotate\")||0);a?a.union(p):a=p}return a}}(t);if(e){var n=t.isHorizontal()?\"height\":\"width\",i=t.model.get([\"axisLabel\",\"margin\"]);o[n]-=e[n]+i,\"top\"===t.position?o.y+=e.height+i:\"left\"===t.position&&(o.x+=e.width+i)}}})),s()),E(this._coordsList,(function(t){t.calcAffineTransform()}))},t.prototype.getAxis=function(t,e){var n=this._axesMap[t];if(null!=n)return n[e||0]},t.prototype.getAxes=function(){return this._axesList.slice()},t.prototype.getCartesian=function(t,e){if(null!=t&&null!=e){var n=\"x\"+t+\"y\"+e;return this._coordsMap[n]}q(t)&&(e=t.yAxisIndex,t=t.xAxisIndex);for(var i=0,r=this._coordsList;i<r.length;i++)if(r[i].getAxis(\"x\").index===t||r[i].getAxis(\"y\").index===e)return r[i]},t.prototype.getCartesians=function(){return this._coordsList.slice()},t.prototype.convertToPixel=function(t,e,n){var i=this._findConvertTarget(e);return i.cartesian?i.cartesian.dataToPoint(n):i.axis?i.axis.toGlobalCoord(i.axis.dataToCoord(n)):null},t.prototype.convertFromPixel=function(t,e,n){var i=this._findConvertTarget(e);return i.cartesian?i.cartesian.pointToData(n):i.axis?i.axis.coordToData(i.axis.toLocalCoord(n)):null},t.prototype._findConvertTarget=function(t){var e,n,i=t.seriesModel,r=t.xAxisModel||i&&i.getReferringComponents(\"xAxis\",zo).models[0],o=t.yAxisModel||i&&i.getReferringComponents(\"yAxis\",zo).models[0],a=t.gridModel,s=this._coordsList;if(i)P(s,e=i.coordinateSystem)<0&&(e=null);else if(r&&o)e=this.getCartesian(r.componentIndex,o.componentIndex);else if(r)n=this.getAxis(\"x\",r.componentIndex);else if(o)n=this.getAxis(\"y\",o.componentIndex);else if(a){a.coordinateSystem===this&&(e=this._coordsList[0])}return{cartesian:e,axis:n}},t.prototype.containPoint=function(t){var e=this._coordsList[0];if(e)return e.containPoint(t)},t.prototype._initCartesian=function(t,e,n){var i=this,r=this,o={left:!1,right:!1,top:!1,bottom:!1},a={x:{},y:{}},s={x:0,y:0};if(e.eachComponent(\"xAxis\",l(\"x\"),this),e.eachComponent(\"yAxis\",l(\"y\"),this),!s.x||!s.y)return this._axesMap={},void(this._axesList=[]);function l(e){return function(n,i){if(QM(n,t)){var l=n.get(\"position\");\"x\"===e?\"top\"!==l&&\"bottom\"!==l&&(l=o.bottom?\"top\":\"bottom\"):\"left\"!==l&&\"right\"!==l&&(l=o.left?\"right\":\"left\"),o[l]=!0;var u=new UM(e,m_(n),[0,0],n.get(\"type\"),l),h=\"category\"===u.type;u.onBand=h&&n.get(\"boundaryGap\"),u.inverse=n.get(\"inverse\"),n.axis=u,u.model=n,u.grid=r,u.index=i,r._axesList.push(u),a[e][i]=u,s[e]++}}}this._axesMap=a,E(a.x,(function(e,n){E(a.y,(function(r,o){var a=\"x\"+n+\"y\"+o,s=new XM(a);s.master=i,s.model=t,i._coordsMap[a]=s,i._coordsList.push(s),s.addAxis(e),s.addAxis(r)}))}))},t.prototype._updateScale=function(t,e){function n(t,e){E(M_(t,e.dim),(function(n){e.scale.unionExtentFromData(t,n)}))}E(this._axesList,(function(t){if(t.scale.setExtent(1/0,-1/0),\"category\"===t.type){var e=t.model.get(\"categorySortInfo\");t.scale.setSortInfo(e)}})),t.eachSeries((function(t){if(jM(t)){var i=qM(t),r=i.xAxisModel,o=i.yAxisModel;if(!QM(r,e)||!QM(o,e))return;var a=this.getCartesian(r.componentIndex,o.componentIndex),s=t.getData(),l=a.getAxis(\"x\"),u=a.getAxis(\"y\");n(s,l),n(s,u)}}),this)},t.prototype.getTooltipAxes=function(t){var e=[],n=[];return E(this.getCartesians(),(function(i){var r=null!=t&&\"auto\"!==t?i.getAxis(t):i.getBaseAxis(),o=i.getOtherAxis(r);P(e,r)<0&&e.push(r),P(n,o)<0&&n.push(o)})),{baseAxes:e,otherAxes:n}},t.create=function(e,n){var i=[];return e.eachComponent(\"grid\",(function(r,o){var a=new t(r,e,n);a.name=\"grid_\"+o,a.resize(r,n,!0),r.coordinateSystem=a,i.push(a)})),e.eachSeries((function(t){if(jM(t)){var e=qM(t),n=e.xAxisModel,i=e.yAxisModel,r=n.getCoordSysModel();0;var o=r.coordinateSystem;t.coordinateSystem=o.getCartesian(n.componentIndex,i.componentIndex)}})),i},t.dimensions=HM,t}();function QM(t,e){return t.getCoordSysModel()===e}function tI(t,e,n,i){n.getAxesOnZeroOf=function(){return r?[r]:[]};var r,o=t[e],a=n.model,s=a.get([\"axisLine\",\"onZero\"]),l=a.get([\"axisLine\",\"onZeroAxisIndex\"]);if(s){if(null!=l)eI(o[l])&&(r=o[l]);else for(var u in o)if(o.hasOwnProperty(u)&&eI(o[u])&&!i[h(o[u])]){r=o[u];break}r&&(i[h(r)]=!0)}function h(t){return t.dim+\"_\"+t.index}}function eI(t){return t&&\"category\"!==t.type&&\"time\"!==t.type&&function(t){var e=t.scale.getExtent(),n=e[0],i=e[1];return!(n>0&&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&&o<nI?n>0?\"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;h<u.length;h++)t.add(u[h]);return u}(n,i,e,t),o=function(t,e,n,i){var r=n.axis,o=it(i.axisLabelShow,n.get([\"axisLabel\",\"show\"]));if(!o||r.scale.isBlank())return;var a=n.getModel(\"axisLabel\"),s=a.get(\"margin\"),l=r.getViewLabels(),u=(it(i.labelRotate,a.get(\"rotate\"))||0)*nI/180,h=iI.innerTextLayout(i.rotation,u,i.labelDirection),c=n.getCategories&&n.getCategories(!0),p=[],d=iI.isLabelSilent(n),f=n.get(\"triggerEvent\");return E(l,(function(o,l){var u=\"ordinal\"===r.scale.type?r.scale.getRawOrdinalNumber(o.tickValue):o.tickValue,g=o.formattedLabel,y=o.rawLabel,v=a;if(c&&c[u]){var m=c[u];q(m)&&m.textStyle&&(v=new Mc(m.textStyle,a,n.ecModel))}var x=v.getTextColor()||n.get([\"axisLine\",\"lineStyle\",\"color\"]),_=r.dataToCoord(u),b=new Fs({x:_,y:i.labelOffset+i.labelDirection*s,rotation:h.rotation,silent:d,z2:10+(o.level||0),style:nc(v,{text:g,align:v.getShallow(\"align\",!0)||h.textAlign,verticalAlign:v.getShallow(\"verticalAlign\",!0)||v.getShallow(\"baseline\",!0)||h.textVerticalAlign,fill:X(x)?x(\"category\"===r.type?y:\"value\"===r.type?u+\"\":u,l):x})});if(b.anid=\"label_\"+u,f){var w=iI.makeAxisEventDataBase(n);w.targetType=\"axisLabel\",w.value=y,w.tickIndex=l,\"category\"===r.type&&(w.dataIndex=u),Qs(b).eventData=w}e.add(b),b.updateTransform(),p.push(b),t.add(b),b.decomposeTransform()})),p}(n,i,e,t);(function(t,e,n){if(S_(t.axis))return;var i=t.get([\"axisLabel\",\"showMinLabel\"]),r=t.get([\"axisLabel\",\"showMaxLabel\"]);e=e||[],n=n||[];var o=e[0],a=e[1],s=e[e.length-1],l=e[e.length-2],u=n[0],h=n[1],c=n[n.length-1],p=n[n.length-2];!1===i?(oI(o),oI(u)):aI(o,a)&&(i?(oI(a),oI(h)):(oI(o),oI(u)));!1===r?(oI(s),oI(c)):aI(l,s)&&(r?(oI(l),oI(p)):(oI(s),oI(c)))}(e,o,r),function(t,e,n,i){var r=n.axis,o=n.getModel(\"minorTick\");if(!o.get(\"show\")||r.scale.isBlank())return;var a=r.getMinorTicksCoords();if(!a.length)return;for(var s=o.getModel(\"lineStyle\"),l=i*o.get(\"length\"),u=k(s.getLineStyle(),k(n.getModel(\"axisTick\").getLineStyle(),{stroke:n.get([\"axisLine\",\"lineStyle\",\"color\"])})),h=0;h<a.length;h++)for(var c=lI(a[h],e.transform,l,u,\"minorticks_\"+h),p=0;p<c.length;p++)t.add(c[p])}(n,i,e,t.tickDirection),e.get([\"axisLabel\",\"hideOverlap\"]))&&Lb(Db(z(o,(function(t){return{label:t,priority:t.z2,defaultAttr:{ignore:t.ignore}}}))))},axisName:function(t,e,n,i){var r=it(t.axisName,e.get(\"name\"));if(r){var o,a,s=e.get(\"nameLocation\"),l=t.nameDirection,u=e.getModel(\"nameTextStyle\"),h=e.get(\"nameGap\")||0,c=e.axis.getExtent(),p=c[0]>c[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<t.length;l++){var u=t[l].coord;a[0]=u,a[1]=0,s[0]=u,s[1]=n,e&&(Wt(a,a,e),Wt(s,s,e));var h=new Zu({shape:{x1:a[0],y1:a[1],x2:s[0],y2:s[1]},style:i,z2:2,autoBatch:!0,silent:!0});Rh(h.shape,h.style.lineWidth),h.anid=r+\"_\"+t[l].tickValue,o.push(h)}return o}function uI(t,e){var n={axesInfo:{},seriesInvolved:!1,coordSysAxesInfo:{},coordSysMap:{}};return function(t,e,n){var i=e.getComponent(\"tooltip\"),r=e.getComponent(\"axisPointer\"),o=r.get(\"link\",!0)||[],a=[];E(n.getCoordinateSystems(),(function(n){if(n.axisPointerEnabled){var s=fI(n.model),l=t.coordSysAxesInfo[s]={};t.coordSysMap[s]=n;var u=n.model.getModel(\"tooltip\",i);if(E(n.getAxes(),H(d,!1,null)),n.getTooltipAxes&&i&&u.get(\"show\")){var h=\"axis\"===u.get(\"trigger\"),c=\"cross\"===u.get([\"axisPointer\",\"type\"]),p=n.getTooltipAxes(u.get([\"axisPointer\",\"axis\"]));(h||c)&&E(p.baseAxes,H(d,!c||\"cross\",h)),c&&E(p.otherAxes,H(d,\"cross\",!1))}}function d(i,s,h){var c=h.model.getModel(\"axisPointer\",r),p=c.get(\"show\");if(p&&(\"auto\"!==p||i||dI(c))){null==s&&(s=c.get(\"triggerTooltip\")),c=i?function(t,e,n,i,r,o){var a=e.getModel(\"axisPointer\"),s={};E([\"type\",\"snap\",\"lineStyle\",\"shadowStyle\",\"label\",\"animation\",\"animationDurationUpdate\",\"animationEasingUpdate\",\"z\"],(function(t){s[t]=T(a.get(t))})),s.snap=\"category\"!==t.type&&!!o,\"cross\"===a.get(\"type\")&&(s.type=\"line\");var l=s.label||(s.label={});if(null==l.show&&(l.show=!1),\"cross\"===r){var u=a.get([\"label\",\"show\"]);if(l.show=null==u||u,!o){var h=s.lineStyle=a.get(\"crossStyle\");h&&k(l,h.textStyle)}}return t.model.getModel(\"axisPointer\",new Mc(s,n,i))}(h,u,r,e,i,s):c;var d=c.get(\"snap\"),f=c.get(\"triggerEmphasis\"),g=fI(h.model),y=s||d||\"category\"===h.type,v=t.axesInfo[g]={key:g,axis:h,coordSys:n,axisPointerModel:c,triggerTooltip:s,triggerEmphasis:f,involveSeries:y,snap:d,useHandle:dI(c),seriesModels:[],linkGroup:null};l[g]=v,t.seriesInvolved=t.seriesInvolved||y;var m=function(t,e){for(var n=e.model,i=e.dim,r=0;r<t.length;r++){var o=t[r]||{};if(hI(o[i+\"AxisId\"],n.id)||hI(o[i+\"AxisIndex\"],n.componentIndex)||hI(o[i+\"AxisName\"],n.name))return r}}(o,h);if(null!=m){var x=a[m]||(a[m]={axesInfo:{}});x.axesInfo[g]=v,x.mapper=o[m].mapper,v.linkGroup=x}}}}))}(n,t,e),n.seriesInvolved&&function(t,e){e.eachSeries((function(e){var n=e.coordinateSystem,i=e.get([\"tooltip\",\"trigger\"],!0),r=e.get([\"tooltip\",\"show\"],!0);n&&\"none\"!==i&&!1!==i&&\"item\"!==i&&!1!==r&&!1!==e.get([\"axisPointer\",\"show\"],!0)&&E(t.coordSysAxesInfo[fI(n.model)],(function(t){var i=t.axis;n.getAxis(i.dim)===i&&(t.seriesModels.push(e),null==t.seriesDataCount&&(t.seriesDataCount=0),t.seriesDataCount+=e.getData().count())}))}))}(n,t),n}function hI(t,e){return\"all\"===t||Y(t)&&P(t,e)>=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]),a<l[0]&&(a=l[0]),r.value=a,s&&(r.status=e.axis.scale.isBlank()?\"hide\":\"show\")}}function pI(t){var e=(t.ecModel.getComponent(\"axisPointer\")||{}).coordSysAxesInfo;return e&&e.axesInfo[fI(t)]}function dI(t){return!!t.get([\"handle\",\"show\"])}function fI(t){return t.type+\"||\"+t.id}var gI={},yI=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(e,n,i,r){this.axisPointerClass&&cI(e),t.prototype.render.apply(this,arguments),this._doUpdateAxisPointerClass(e,i,!0)},e.prototype.updateAxisPointer=function(t,e,n,i){this._doUpdateAxisPointerClass(t,n,!1)},e.prototype.remove=function(t,e){var n=this._axisPointer;n&&n.remove(e)},e.prototype.dispose=function(e,n){this._disposeAxisPointer(n),t.prototype.dispose.apply(this,arguments)},e.prototype._doUpdateAxisPointerClass=function(t,n,i){var r=e.getAxisPointerClass(this.axisPointerClass);if(r){var o=function(t){var e=pI(t);return e&&e.axisPointerModel}(t);o?(this._axisPointer||(this._axisPointer=new r)).render(t,o,n,i):this._disposeAxisPointer(n)}},e.prototype._disposeAxisPointer=function(t){this._axisPointer&&this._axisPointer.dispose(t),this._axisPointer=null},e.registerAxisPointerClass=function(t,e){gI[t]=e},e.getAxisPointerClass=function(t){return t&&gI[t]},e.type=\"axis\",e}(Tg),vI=Oo();function mI(t,e,n,i){var r=n.axis;if(!r.scale.isBlank()){var o=n.getModel(\"splitArea\"),a=o.getModel(\"areaStyle\"),s=a.get(\"color\"),l=i.coordinateSystem.getRect(),u=r.getTicksCoords({tickModel:o,clamp:!0});if(u.length){var h=s.length,c=vI(t).splitAreaColors,p=yt(),d=0;if(c)for(var f=0;f<u.length;f++){var g=c.get(u[f].tickValue);if(null!=g){d=(g+(h-1)*f)%h;break}}var y=r.toGlobalCoord(u[0].coord),v=a.getAreaStyle();s=Y(s)?s:[s];for(f=1;f<u.length;f++){var m=r.toGlobalCoord(u[f].coord),x=void 0,_=void 0,b=void 0,w=void 0;r.isHorizontal()?(x=y,_=l.y,b=m-x,w=l.height,y=x+b):(x=l.x,_=y,b=l.width,y=_+(w=m-_));var S=u[f-1].tickValue;null!=S&&p.set(S,d),e.add(new zs({anid:null!=S?\"area_\"+S:null,shape:{x:x,y:_,width:b,height:w},style:k({fill:s[d]},v),autoBatch:!0,silent:!0})),d=(d+1)%h}vI(t).splitAreaColors=p}}}function xI(t){vI(t).splitAreaColors=null}var _I=[\"axisLine\",\"axisTickLabel\",\"axisName\"],bI=[\"splitArea\",\"splitLine\",\"minorSplitLine\"],wI=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass=\"CartesianAxisPointer\",n}return n(e,t),e.prototype.render=function(e,n,i,r){this.group.removeAll();var o=this._axisGroup;if(this._axisGroup=new zr,this.group.add(this._axisGroup),e.get(\"show\")){var a=e.getCoordSysModel(),s=ZM(a,e),l=new iI(e,A({handleAutoShown:function(t){for(var n=a.coordinateSystem.getCartesians(),i=0;i<n.length;i++)if(Sx(n[i].getOtherAxis(e.axis).scale))return!0;return!1}},s));E(_I,l.add,l),this._axisGroup.add(l.getGroup()),E(bI,(function(t){e.get([t,\"show\"])&&SI[t](this,this._axisGroup,e,a)}),this),r&&\"changeAxisOrder\"===r.type&&r.isInitSort||Fh(o,this._axisGroup,e),t.prototype.render.call(this,e,n,i,r)}},e.prototype.remove=function(){xI(this)},e.type=\"cartesianAxis\",e}(yI),SI={splitLine:function(t,e,n,i){var r=n.axis;if(!r.scale.isBlank()){var o=n.getModel(\"splitLine\"),a=o.getModel(\"lineStyle\"),s=a.get(\"color\");s=Y(s)?s:[s];for(var l=i.coordinateSystem.getRect(),u=r.isHorizontal(),h=0,c=r.getTicksCoords({tickModel:o}),p=[],d=[],f=a.getLineStyle(),g=0;g<c.length;g++){var y=r.toGlobalCoord(c[g].coord);u?(p[0]=y,p[1]=l.y,d[0]=y,d[1]=l.y+l.height):(p[0]=l.x,p[1]=y,d[0]=l.x+l.width,d[1]=y);var v=h++%s.length,m=c[g].tickValue,x=new Zu({anid:null!=m?\"line_\"+c[g].tickValue:null,autoBatch:!0,shape:{x1:p[0],y1:p[1],x2:d[0],y2:d[1]},style:k({stroke:s[v]},f),silent:!0});Rh(x.shape,f.lineWidth),e.add(x)}}},minorSplitLine:function(t,e,n,i){var r=n.axis,o=n.getModel(\"minorSplitLine\").getModel(\"lineStyle\"),a=i.coordinateSystem.getRect(),s=r.isHorizontal(),l=r.getMinorTicksCoords();if(l.length)for(var u=[],h=[],c=o.getLineStyle(),p=0;p<l.length;p++)for(var d=0;d<l[p].length;d++){var f=r.toGlobalCoord(l[p][d].coord);s?(u[0]=f,u[1]=a.y,h[0]=f,h[1]=a.y+a.height):(u[0]=a.x,u[1]=f,h[0]=a.x+a.width,h[1]=f);var g=new Zu({anid:\"minor_line_\"+l[p][d].tickValue,autoBatch:!0,shape:{x1:u[0],y1:u[1],x2:h[0],y2:h[1]},style:c,silent:!0});Rh(g.shape,c.lineWidth),e.add(g)}},splitArea:function(t,e,n,i){mI(t,e,n,i)}},MI=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=\"xAxis\",e}(wI),II=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=MI.type,e}return n(e,t),e.type=\"yAxis\",e}(wI),TI=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=\"grid\",e}return n(e,t),e.prototype.render=function(t,e){this.group.removeAll(),t.get(\"show\")&&this.group.add(new zs({shape:t.coordinateSystem.getRect(),style:k({fill:t.get(\"backgroundColor\")},t.getItemStyle()),silent:!0,z2:-1}))},e.type=\"grid\",e}(Tg),CI={offset:0};function DI(t){t.registerComponentView(TI),t.registerComponentModel(OM),t.registerCoordinateSystem(\"cartesian2d\",JM),FM(t,\"x\",RM,CI),FM(t,\"y\",RM,CI),t.registerComponentView(MI),t.registerComponentView(II),t.registerPreprocessor((function(t){t.xAxis&&t.yAxis&&!t.grid&&(t.grid={})}))}function AI(t){t.eachSeriesByType(\"radar\",(function(t){var e=t.getData(),n=[],i=t.coordinateSystem;if(i){var r=i.getIndicatorAxes();E(r,(function(t,o){e.each(e.mapDimension(r[o].dim),(function(t,e){n[e]=n[e]||[];var r=i.dataToPoint(t,o);n[e][o]=kI(r)?r:LI(i)}))})),e.each((function(t){var r=F(n[t],(function(t){return kI(t)}))||LI(i);n[t].push(r.slice()),e.setItemLayout(t,n[t])}))}}))}function kI(t){return!isNaN(t[0])&&!isNaN(t[1])}function LI(t){return[t.cx,t.cy]}function PI(t){var e=t.polar;if(e){Y(e)||(e=[e]);var n=[];E(e,(function(e,i){e.indicator?(e.type&&!e.shape&&(e.shape=e.type),t.radar=t.radar||[],Y(t.radar)||(t.radar=[t.radar]),t.radar.push(e)):n.push(e)})),t.polar=n}E(t.series,(function(t){t&&\"radar\"===t.type&&t.polarIndex&&(t.radarIndex=t.polarIndex)}))}var OI=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.coordinateSystem,r=this.group,o=t.getData(),a=this._data;function s(t,e){var n=t.getItemVisual(e,\"symbol\")||\"circle\";if(\"none\"!==n){var i=Hy(t.getItemVisual(e,\"symbolSize\")),r=Wy(n,-1,-1,2,2),o=t.getItemVisual(e,\"symbolRotate\")||0;return r.attr({style:{strokeNoScale:!0},z2:100,scaleX:i[0]/2,scaleY:i[1]/2,rotation:o*Math.PI/180||0}),r}}function l(e,n,i,r,o,a){i.removeAll();for(var l=0;l<n.length-1;l++){var u=s(r,o);u&&(u.__dimIdx=l,e[l]?(u.setPosition(e[l]),Kh[a?\"initProps\":\"updateProps\"](u,{x:n[l][0],y:n[l][1]},t,o)):u.setPosition(n[l]),i.add(u))}}function u(t){return z(t,(function(t){return[i.cx,i.cy]}))}o.diff(a).add((function(e){var n=o.getItemLayout(e);if(n){var i=new Wu,r=new Yu,a={shape:{points:n}};i.shape.points=u(n),r.shape.points=u(n),gh(i,a,t,e),gh(r,a,t,e);var s=new zr,h=new zr;s.add(r),s.add(i),s.add(h),l(r.shape.points,n,h,o,e,!0),o.setItemGraphicEl(e,s)}})).update((function(e,n){var i=a.getItemGraphicEl(n),r=i.childAt(0),s=i.childAt(1),u=i.childAt(2),h={shape:{points:o.getItemLayout(e)}};h.shape.points&&(l(r.shape.points,h.shape.points,u,o,e,!1),_h(s),_h(r),fh(r,h,t),fh(s,h,t),o.setItemGraphicEl(e,i))})).remove((function(t){r.remove(a.getItemGraphicEl(t))})).execute(),o.eachItemGraphicEl((function(t,e){var n=o.getItemModel(e),i=t.childAt(0),a=t.childAt(1),s=t.childAt(2),l=o.getItemVisual(e,\"style\"),u=l.fill;r.add(t),i.useStyle(k(n.getModel(\"lineStyle\").getLineStyle(),{fill:\"none\",stroke:u})),jl(i,n,\"lineStyle\"),jl(a,n,\"areaStyle\");var h=n.getModel(\"areaStyle\"),c=h.isEmpty()&&h.parentModel.isEmpty();a.ignore=c,E([\"emphasis\",\"select\",\"blur\"],(function(t){var e=n.getModel([t,\"areaStyle\"]),i=e.isEmpty()&&e.parentModel.isEmpty();a.ensureState(t).ignore=i&&c})),a.useStyle(k(h.getAreaStyle(),{fill:u,opacity:.7,decal:l.decal}));var p=n.getModel(\"emphasis\"),d=p.getModel(\"itemStyle\").getItemStyle();s.eachChild((function(t){if(t instanceof ks){var i=t.style;t.useStyle(A({image:i.image,x:i.x,y:i.y,width:i.width,height:i.height},l))}else t.useStyle(l),t.setColor(u),t.style.strokeNoScale=!0;t.ensureState(\"emphasis\").style=T(d);var r=o.getStore().get(o.getDimensionIndex(t.__dimIdx),e);(null==r||isNaN(r))&&(r=\"\"),tc(t,ec(n),{labelFetcher:o.hostModel,labelDataIndex:e,labelDimIndex:t.__dimIdx,defaultText:r,inheritColor:u,defaultOpacity:l.opacity})})),Yl(t,p.get(\"focus\"),p.get(\"blurScope\"),p.get(\"disabled\"))})),this._data=o},e.prototype.remove=function(){this.group.removeAll(),this._data=null},e.type=\"radar\",e}(kg),RI=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),this.legendVisualProvider=new IM(W(this.getData,this),W(this.getRawData,this))},e.prototype.getInitialData=function(t,e){return MM(this,{generateCoord:\"indicator_\",generateCoordCount:1/0})},e.prototype.formatTooltip=function(t,e,n){var i=this.getData(),r=this.coordinateSystem.getIndicatorAxes(),o=this.getData().getName(t),a=\"\"===o?this.name:o,s=cg(this,t);return ng(\"section\",{header:a,sortBlocks:!0,blocks:z(r,(function(e){var n=i.get(i.mapDimension(e.dim),t);return ng(\"nameValue\",{markerType:\"subItem\",markerColor:s,name:e.name,value:n,sortParam:n})}))})},e.prototype.getTooltipPosition=function(t){if(null!=t)for(var e=this.getData(),n=this.coordinateSystem,i=e.getValues(z(n.dimensions,(function(t){return e.mapDimension(t)})),t),r=0,o=i.length;r<o;r++)if(!isNaN(i[r])){var a=n.getIndicatorAxes();return n.coordToPoint(a[r].dataToCoord(i[r]),r)}},e.type=\"series.radar\",e.dependencies=[\"radar\"],e.defaultOption={z:2,colorBy:\"data\",coordinateSystem:\"radar\",legendHoverLink:!0,radarIndex:0,lineStyle:{width:2,type:\"solid\",join:\"round\"},label:{position:\"top\"},symbolSize:8},e}(mg),NI=VM.value;function EI(t,e){return k({show:e},t)}var zI=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(){var t=this.get(\"boundaryGap\"),e=this.get(\"splitNumber\"),n=this.get(\"scale\"),i=this.get(\"axisLine\"),r=this.get(\"axisTick\"),o=this.get(\"axisLabel\"),a=this.get(\"axisName\"),s=this.get([\"axisName\",\"show\"]),l=this.get([\"axisName\",\"formatter\"]),u=this.get(\"axisNameGap\"),h=this.get(\"triggerEvent\"),c=z(this.get(\"indicator\")||[],(function(c){null!=c.max&&c.max>0&&!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;x<y.length;x++){if(l)f[C(f,p,x)].push(new _u({shape:{cx:v,cy:m,r:y[x].coord}}));if(u&&x<y.length-1)g[C(g,d,x)].push(new Bu({shape:{cx:v,cy:m,r0:y[x].coord,r:y[x+1].coord}}))}else{var _,b=z(n,(function(t,n){var i=t.getTicksCoords();return _=null==_?i.length-1:Math.min(i.length-1,_),z(i,(function(t){return e.coordToPoint(t.coord,n)}))})),w=[];for(x=0;x<=_;x++){for(var S=[],M=0;M<n.length;M++)S.push(b[M][x]);if(S[0]&&S.push(S[0].slice()),l)f[C(f,p,x)].push(new Yu({shape:{points:S}}));if(u&&w)g[C(g,d,x-1)].push(new Wu({shape:{points:S.concat(w)}}));w=S.slice().reverse()}}var I=a.getLineStyle(),T=s.getAreaStyle();E(g,(function(t,e){this.group.add(Ph(t,{style:k({stroke:\"none\",fill:d[e%d.length]},T),silent:!0}))}),this),E(f,(function(t,e){this.group.add(Ph(t,{style:k({fill:\"none\",stroke:p[e%p.length]},I),silent:!0}))}),this)}function C(t,e,n){var i=n%e.length;return t[i]=t[i]||[],i}},e.type=\"radar\",e}(Tg),FI=function(t){function e(e,n,i){var r=t.call(this,e,n,i)||this;return r.type=\"value\",r.angle=0,r.name=\"\",r}return n(e,t),e}(nb),GI=function(){function t(t,e,n){this.dimensions=[],this._model=t,this._indicatorAxes=z(t.getIndicatorModels(),(function(t,e){var n=\"indicator_\"+e,i=new FI(n,new Ox);return i.name=t.get(\"name\"),i.model=t,t.axis=i,this.dimensions.push(n),i}),this),this.resize(t,n)}return t.prototype.getIndicatorAxes=function(){return this._indicatorAxes},t.prototype.dataToPoint=function(t,e){var n=this._indicatorAxes[e];return this.coordToPoint(n.dataToCoord(t),e)},t.prototype.coordToPoint=function(t,e){var n=this._indicatorAxes[e].angle;return[this.cx+t*Math.cos(n),this.cy-t*Math.sin(n)]},t.prototype.pointToData=function(t){var e=t[0]-this.cx,n=t[1]-this.cy,i=Math.sqrt(e*e+n*n);e/=i,n/=i;for(var r,o=Math.atan2(-n,e),a=1/0,s=-1,l=0;l<this._indicatorAxes.length;l++){var u=this._indicatorAxes[l],h=Math.abs(o-u.angle);h<a&&(r=u,s=l,a=h)}return[s,+(r&&r.coordToData(i))]},t.prototype.resize=function(t,e){var n=t.get(\"center\"),i=e.getWidth(),r=e.getHeight(),o=Math.min(i,r)/2;this.cx=Ur(n[0],i),this.cy=Ur(n[1],r),this.startAngle=t.get(\"startAngle\")*Math.PI/180;var a=t.get(\"radius\");(U(a)||j(a))&&(a=[0,a]),this.r0=Ur(a[0],o),this.r=Ur(a[1],o),E(this._indicatorAxes,(function(t,e){t.setExtent(this.r0,this.r);var n=this.startAngle+e*Math.PI*2/this._indicatorAxes.length;n=Math.atan2(Math.sin(n),Math.cos(n)),t.angle=n}),this)},t.prototype.update=function(t,e){var n=this._indicatorAxes,i=this._model;E(n,(function(t){t.scale.setExtent(1/0,-1/0)})),t.eachSeriesByType(\"radar\",(function(e,r){if(\"radar\"===e.get(\"coordinateSystem\")&&t.getComponent(\"radar\",e.get(\"radarIndex\"))===i){var o=e.getData();E(n,(function(t){t.scale.unionExtentFromData(o,o.mapDimension(t.dim))}))}}),this);var r=i.get(\"splitNumber\"),o=new Ox;o.setExtent(0,r),o.setInterval(1),E(n,(function(t,e){$M(t.scale,t.model,o)}))},t.prototype.convertToPixel=function(t,e,n){return console.warn(\"Not implemented.\"),null},t.prototype.convertFromPixel=function(t,e,n){return console.warn(\"Not implemented.\"),null},t.prototype.containPoint=function(t){return console.warn(\"Not implemented.\"),!1},t.create=function(e,n){var i=[];return e.eachComponent(\"radar\",(function(r){var o=new t(r,e,n);i.push(o),r.coordinateSystem=o})),e.eachSeriesByType(\"radar\",(function(t){\"radar\"===t.get(\"coordinateSystem\")&&(t.coordinateSystem=i[t.get(\"radarIndex\")||0])})),i},t.dimensions=[],t}();function WI(t){t.registerCoordinateSystem(\"radar\",GI),t.registerComponentModel(zI),t.registerComponentView(BI),t.registerVisual({seriesType:\"radar\",reset:function(t){var e=t.getData();e.each((function(t){e.setItemVisual(t,\"legendIcon\",\"roundRect\")})),e.setVisual(\"legendIcon\",\"roundRect\")}})}var HI=\"\\0_ec_interaction_mutex\";function YI(t,e){return!!XI(t)[e]}function XI(t){return t[HI]||(t[HI]={})}Mm({type:\"takeGlobalCursor\",event:\"globalCursorTaken\",update:\"update\"},bt);var UI=function(t){function e(e){var n=t.call(this)||this;n._zr=e;var i=W(n._mousedownHandler,n),r=W(n._mousemoveHandler,n),o=W(n._mouseupHandler,n),a=W(n._mousewheelHandler,n),s=W(n._pinchHandler,n);return n.enable=function(t,n){this.disable(),this._opt=k(T(n)||{},{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),null==t&&(t=!0),!0!==t&&\"move\"!==t&&\"pan\"!==t||(e.on(\"mousedown\",i),e.on(\"mousemove\",r),e.on(\"mouseup\",o)),!0!==t&&\"scale\"!==t&&\"zoom\"!==t||(e.on(\"mousewheel\",a),e.on(\"pinch\",s))},n.disable=function(){e.off(\"mousedown\",i),e.off(\"mousemove\",r),e.off(\"mouseup\",o),e.off(\"mousewheel\",a),e.off(\"pinch\",s)},n}return n(e,t),e.prototype.isDragging=function(){return this._dragging},e.prototype.isPinching=function(){return this._pinching},e.prototype.setPointerChecker=function(t){this.pointerChecker=t},e.prototype.dispose=function(){this.disable()},e.prototype._mousedownHandler=function(t){if(!fe(t)){for(var e=t.target;e;){if(e.draggable)return;e=e.__hostTarget||e.parent}var n=t.offsetX,i=t.offsetY;this.pointerChecker&&this.pointerChecker(t,n,i)&&(this._x=n,this._y=i,this._dragging=!0)}},e.prototype._mousemoveHandler=function(t){if(this._dragging&&qI(\"moveOnMouseMove\",t,this._opt)&&\"pinch\"!==t.gestureEvent&&!YI(this._zr,\"globalPan\")){var e=t.offsetX,n=t.offsetY,i=this._x,r=this._y,o=e-i,a=n-r;this._x=e,this._y=n,this._opt.preventDefaultMouseMove&&de(t.event),jI(this,\"pan\",\"moveOnMouseMove\",t,{dx:o,dy:a,oldX:i,oldY:r,newX:e,newY:n,isAvailableBehavior:null})}},e.prototype._mouseupHandler=function(t){fe(t)||(this._dragging=!1)},e.prototype._mousewheelHandler=function(t){var e=qI(\"zoomOnMouseWheel\",t,this._opt),n=qI(\"moveOnMouseWheel\",t,this._opt),i=t.wheelDelta,r=Math.abs(i),o=t.offsetX,a=t.offsetY;if(0!==i&&(e||n)){if(e){var s=r>3?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<e.length;n++){var i=e[n];i[0].style[i[1]]=t[i[2]]}}(this._defs,this._defsUsePending),this._defsUsePending=[],o){var c=yT(o);c.length>=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;i<e.length;i+=2){var r=parseFloat(e[i]),o=parseFloat(e[i+1]);n.push([r,o])}return n}function pT(t,e,n,i,r){var o=e,a=o.__inheritedStyle=o.__inheritedStyle||{},s={};1===t.nodeType&&(function(t,e){var n=t.getAttribute(\"transform\");if(n){n=n.replace(/,/g,\" \");var i=[],r=null;n.replace(vT,(function(t,e,n){return i.push(e,n),\"\"}));for(var o=i.length-1;o>0;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;i<iT.length;i++){var r=iT[i];null!=(o=t.getAttribute(r))&&(e[nT[r]]=o)}for(i=0;i<oT.length;i++){var o;r=oT[i];null!=(o=t.getAttribute(r))&&(n[rT[r]]=o)}}(t,a,s)),o.style=o.style||{},null!=a.fill&&(o.style.fill=fT(o,\"fill\",a.fill,n)),null!=a.stroke&&(o.style.stroke=fT(o,\"stroke\",a.stroke,n)),E([\"lineWidth\",\"opacity\",\"fillOpacity\",\"strokeOpacity\",\"miterLimit\",\"fontSize\"],(function(t){null!=a[t]&&(o.style[t]=parseFloat(a[t]))})),E([\"lineDashOffset\",\"lineCap\",\"lineJoin\",\"fontWeight\",\"fontFamily\",\"fontStyle\",\"textAlign\"],(function(t){null!=a[t]&&(o.style[t]=a[t])})),r&&(o.__selfStyle=s),a.lineDash&&(o.style.lineDash=z(yT(a.lineDash),(function(t){return parseFloat(t)}))),\"hidden\"!==a.visibility&&\"collapse\"!==a.visibility||(o.invisible=!0),\"none\"===a.display&&(o.ignore=!0)}var dT=/^url\\(\\s*#(.*?)\\)/;function fT(t,e,n,i){var r=n&&n.match(dT);if(!r)return\"none\"===n&&(n=null),n;var o=ut(r[1]);i.push([t,e,o])}var gT=/-?([0-9]*\\.)?[0-9]+([eE]-?[0-9]+)?/g;function yT(t){return t.match(gT)||[]}var vT=/(translate|scale|rotate|skewX|skewY|matrix)\\(([\\-\\s0-9\\.eE,]*)\\)/g,mT=Math.PI/180;var xT=/([^\\s:;]+)\\s*:\\s*([^:;]+)/g;function _T(t,e,n){var i,r=t.getAttribute(\"style\");if(r)for(xT.lastIndex=0;null!=(i=xT.exec(r));){var o=i[1],a=_t(nT,o)?nT[o]:null;a&&(e[a]=i[2]);var s=_t(rT,o)?rT[o]:null;s&&(n[s]=i[2])}}function bT(t,e){var n=e.width/t.width,i=e.height/t.height,r=Math.min(n,i);return{scale:r,x:-(t.x+t.width/2)*r+(e.x+e.width/2),y:-(t.y+t.height/2)*r+(e.y+e.height/2)}}var wT=yt([\"rect\",\"circle\",\"line\",\"ellipse\",\"polygon\",\"polyline\",\"path\",\"text\",\"tspan\",\"g\"]),ST=function(){function t(t,e){this.type=\"geoSVG\",this._usedGraphicMap=yt(),this._freedGraphics=[],this._mapName=t,this._parsedXML=eT(e)}return t.prototype.load=function(){var t=this._firstGraphic;if(!t){t=this._firstGraphic=this._buildGraphic(this._parsedXML),this._freedGraphics.push(t),this._boundingRect=this._firstGraphic.boundingRect.clone();var e=function(t){var e=[],n=yt();return E(t,(function(t){if(null==t.namedFrom){var i=new z_(t.name,t.el);e.push(i),n.set(t.name,i)}})),{regions:e,regionsMap:n}}(t.named),n=e.regions,i=e.regionsMap;this._regions=n,this._regionsMap=i}return{boundingRect:this._boundingRect,regions:this._regions,regionsMap:this._regionsMap}},t.prototype._buildGraphic=function(t){var e,n,i,r;try{lt(null!=(n=(e=t&&(i=t,r={ignoreViewBox:!0,ignoreRootClip:!0},(new aT).parse(i,r))||{}).root))}catch(t){throw new Error(\"Invalid svg format\\n\"+t.message)}var o=new zr;o.add(n),o.isGeoSVGGraphicRoot=!0;var a=e.width,s=e.height,l=e.viewBoxRect,u=this._boundingRect;if(!u){var h=void 0,c=void 0,p=void 0,d=void 0;if(null!=a?(h=0,p=a):l&&(h=l.x,p=l.width),null!=s?(c=0,d=s):l&&(c=l.y,d=l.height),null==h||null==c){var f=n.getBoundingRect();null==h&&(h=f.x,p=f.width),null==c&&(c=f.y,d=f.height)}u=this._boundingRect=new ze(h,c,p,d)}if(l){var g=bT(l,u);n.scaleX=n.scaleY=g.scale,n.x=g.x,n.y=g.y}o.setClipPath(new zs({shape:u.plain()}));var y=[];return E(e.named,(function(t){var e;null!=wT.get(t.svgNodeTagLower)&&(y.push(t),(e=t.el).silent=!1,e.isGroup&&e.traverse((function(t){t.silent=!1})))})),{root:o,boundingRect:u,named:y}},t.prototype.useGraphic=function(t){var e=this._usedGraphicMap,n=e.get(t);return n||(n=this._freedGraphics.pop()||this._buildGraphic(this._parsedXML),e.set(t,n),n)},t.prototype.freeGraphic=function(t){var e=this._usedGraphicMap,n=e.get(t);n&&(e.removeKey(t),this._freedGraphics.push(n))},t}();for(var MT=[126,25],IT=\"南海诸岛\",TT=[[[0,3.5],[7,11.2],[15,11.9],[30,7],[42,.7],[52,.7],[56,7.7],[59,.7],[64,.7],[64,0],[5,0],[0,3.5]],[[13,16.1],[19,14.7],[16,21.7],[11,23.1],[13,16.1]],[[12,32.2],[14,38.5],[15,38.5],[13,32.2],[12,32.2]],[[16,47.6],[12,53.2],[13,53.2],[18,47.6],[16,47.6]],[[6,64.4],[8,70],[9,70],[8,64.4],[6,64.4]],[[23,82.6],[29,79.8],[30,79.8],[25,82.6],[23,82.6]],[[37,70.7],[43,62.3],[44,62.3],[39,70.7],[37,70.7]],[[48,51.1],[51,45.5],[53,45.5],[50,51.1],[48,51.1]],[[51,35],[51,28.7],[53,28.7],[53,35],[51,35]],[[52,22.4],[55,17.5],[56,17.5],[53,22.4],[52,22.4]],[[58,12.6],[62,7],[63,7],[60,12.6],[58,12.6]],[[0,3.5],[0,93.1],[64,93.1],[64,0],[63,0],[63,92.4],[1,92.4],[1,3.5],[0,3.5]]],CT=0;CT<TT.length;CT++)for(var DT=0;DT<TT[CT].length;DT++)TT[CT][DT][0]/=10.5,TT[CT][DT][1]/=-14,TT[CT][DT][0]+=MT[0],TT[CT][DT][1]+=MT[1];var AT={\"南海诸岛\":[32,80],\"广东\":[0,-10],\"香港\":[10,5],\"澳门\":[-10,10],\"天津\":[5,5]};var kT=[[[123.45165252685547,25.73527164402261],[123.49731445312499,25.73527164402261],[123.49731445312499,25.750734064600884],[123.45165252685547,25.750734064600884],[123.45165252685547,25.73527164402261]]];var LT=function(){function t(t,e,n){var i;this.type=\"geoJSON\",this._parsedMap=yt(),this._mapName=t,this._specialAreas=n,this._geoJSON=U(i=e)?\"undefined\"!=typeof JSON&&JSON.parse?JSON.parse(i):new Function(\"return (\"+i+\");\")():i}return t.prototype.load=function(t,e){e=e||\"name\";var n=this._parsedMap.get(e);if(!n){var i=this._parseToRegions(e);n=this._parsedMap.set(e,{regions:i,boundingRect:PT(i)})}var r=yt(),o=[];return E(n.regions,(function(e){var n=e.name;t&&_t(t,n)&&(e=e.cloneShallow(n=t[n])),o.push(e),r.set(n,e)})),{regions:o,boundingRect:n.boundingRect||new ze(0,0,0,0),regionsMap:r}},t.prototype._parseToRegions=function(t){var e,n=this._mapName,i=this._geoJSON;try{e=i?F_(i,t):[]}catch(t){throw new Error(\"Invalid geoJson format\\n\"+t.message)}return function(t,e){if(\"china\"===t){for(var n=0;n<e.length;n++)if(e[n].name===IT)return;e.push(new E_(IT,z(TT,(function(t){return{type:\"polygon\",exterior:t}})),MT))}}(n,e),E(e,(function(t){var e=t.name;!function(t,e){if(\"china\"===t){var n=AT[e.name];if(n){var i=e.getCenter();i[0]+=n[0]/10.5,i[1]+=-n[1]/14,e.setCenter(i)}}}(n,t),function(t,e){\"china\"===t&&\"台湾\"===e.name&&e.geometries.push({type:\"polygon\",exterior:kT[0]})}(n,t);var i=this._specialAreas&&this._specialAreas[e];i&&t.transformTo(i.left,i.top,i.width,i.height)}),this),e},t.prototype.getMapForUser=function(){return{geoJson:this._geoJSON,geoJSON:this._geoJSON,specialAreas:this._specialAreas}},t}();function PT(t){for(var e,n=0;n<t.length;n++){var i=t[n].getBoundingRect();(e=e||i.clone()).union(i)}return e}var OT=yt(),RT=function(t,e,n){if(e.svg){var i=new ST(t,e.svg);OT.set(t,i)}else{var r=e.geoJson||e.geoJSON;r&&!e.features?n=e.specialAreas:r=e;i=new LT(t,r,n);OT.set(t,i)}},NT=function(t){return OT.get(t)},ET=function(t){var e=OT.get(t);return e&&\"geoJSON\"===e.type&&e.getMapForUser()},zT=function(t,e,n){var i=OT.get(t);if(i)return i.load(e,n)},VT=[\"rect\",\"circle\",\"line\",\"ellipse\",\"polygon\",\"polyline\",\"path\"],BT=yt(VT),FT=yt(VT.concat([\"g\"])),GT=yt(VT.concat([\"g\"])),WT=Oo();function HT(t){var e=t.getItemStyle(),n=t.get(\"areaColor\");return null!=n&&(e.fill=n),e}function YT(t){var e=t.style;e&&(e.stroke=e.stroke||e.fill,e.fill=null)}var XT=function(){function t(t){var e=new zr;this.uid=Tc(\"ec_map_draw\"),this._controller=new UI(t.getZr()),this._controllerHost={target:e},this.group=e,e.add(this._regionsGroup=new zr),e.add(this._svgGroup=new zr)}return t.prototype.draw=function(t,e,n,i,r){var o=\"geo\"===t.mainType,a=t.getData&&t.getData();o&&e.eachComponent({mainType:\"series\",subType:\"map\"},(function(e){a||e.getHostGeoModel()!==t||(a=e.getData())}));var s=t.coordinateSystem,l=this._regionsGroup,u=this.group,h=s.getTransformInfo(),c=h.raw,p=h.roam;!l.childAt(0)||r?(u.x=p.x,u.y=p.y,u.scaleX=p.scaleX,u.scaleY=p.scaleY,u.dirty()):fh(u,p,t);var d=a&&a.getVisual(\"visualMeta\")&&a.getVisual(\"visualMeta\").length>0,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<t.length;++i){var r=u(t[i],n);r&&e.push(r)}return e}function c(t){return{shape:{points:h(t)}}}i.removeAll(),E(t.geo.regions,(function(r){var h=r.name,p=e.get(h),d=n.get(h)||{},f=d.dataIdx,g=d.regionModel;p||(p=e.set(h,new zr),i.add(p),f=a?a.indexOfName(h):null,g=t.isGeo?o.getRegionModel(h):a?a.getItemModel(f):null,n.set(h,{dataIdx:f,regionModel:g}));var y=[],v=[];E(r.geometries,(function(t){if(\"polygon\"===t.type){var e=[t.exterior].concat(t.interiors||[]);l&&(e=$T(e,l)),E(e,(function(t){y.push(new Wu(c(t)))}))}else{var n=t.points;l&&(n=$T(n,l,!0)),E(n,(function(t){v.push(new Yu(c(t)))}))}}));var m=u(r.getCenter(),s&&s.project);function x(e,n){if(e.length){var i=new th({culling:!0,segmentIgnoreThreshold:1,shape:{paths:e}});p.add(i),UT(t,i,f,g),ZT(t,i,h,g,o,f,m),n&&(YT(i),E(i.states,YT))}}x(y),x(v,!0)})),e.each((function(e,i){var r=n.get(i),a=r.dataIdx,s=r.regionModel;jT(t,e,i,s,o,a),qT(t,e,i,s,o),KT(t,e,i,s,o)}),this)},t.prototype._buildSVG=function(t){var e=t.geo.map,n=t.transformInfoRaw;this._svgGroup.x=n.x,this._svgGroup.y=n.y,this._svgGroup.scaleX=n.scaleX,this._svgGroup.scaleY=n.scaleY,this._svgResourceChanged(e)&&(this._freeSVG(),this._useSVG(e));var i=this._svgDispatcherMap=yt(),r=!1;E(this._svgGraphicRecord.named,(function(e){var n=e.name,o=t.mapOrGeoModel,a=t.data,s=e.svgNodeTagLower,l=e.el,u=a?a.indexOfName(n):null,h=o.getRegionModel(n);(null!=BT.get(s)&&l instanceof Sa&&UT(t,l,u,h),l instanceof Sa&&(l.culling=!0),l.z2EmphasisLift=0,e.namedFrom)||(null!=GT.get(s)&&ZT(t,l,n,h,o,u,null),jT(t,l,n,h,o,u),qT(t,l,n,h,o),null!=FT.get(s)&&(\"self\"===KT(t,l,n,h,o)&&(r=!0),(i.get(n)||i.set(n,[])).push(l)))}),this),this._enableBlurEntireSVG(r,t)},t.prototype._enableBlurEntireSVG=function(t,e){if(t&&e.isGeo){var n=e.mapOrGeoModel.getModel([\"blur\",\"itemStyle\"]).getItemStyle().opacity;this._svgGraphicRecord.root.traverse((function(t){if(!t.isGroup){Cl(t);var e=t.ensureState(\"blur\").style||{};null==e.opacity&&null!=n&&(e.opacity=n),t.ensureState(\"emphasis\")}}))}},t.prototype.remove=function(){this._regionsGroup.removeAll(),this._regionsGroupByName=null,this._svgGroup.removeAll(),this._freeSVG(),this._controller.dispose(),this._controllerHost=null},t.prototype.findHighDownDispatchers=function(t,e){if(null==t)return[];var n=e.coordinateSystem;if(\"geoJSON\"===n.resourceType){var i=this._regionsGroupByName;if(i){var r=i.get(t);return r?[r]:[]}}else if(\"geoSVG\"===n.resourceType)return this._svgDispatcherMap&&this._svgDispatcherMap.get(t)||[]},t.prototype._svgResourceChanged=function(t){return this._svgMapName!==t},t.prototype._useSVG=function(t){var e=NT(t);if(e&&\"geoSVG\"===e.type){var n=e.useGraphic(this.uid);this._svgGroup.add(n.root),this._svgGraphicRecord=n,this._svgMapName=t}},t.prototype._freeSVG=function(){var t=this._svgMapName;if(null!=t){var e=NT(t);e&&\"geoSVG\"===e.type&&e.freeGraphic(this.uid),this._svgGraphicRecord=null,this._svgDispatcherMap=null,this._svgGroup.removeAll(),this._svgMapName=null}},t.prototype._updateController=function(t,e,n){var i=t.coordinateSystem,r=this._controller,o=this._controllerHost;o.zoomLimit=t.get(\"scaleLimit\"),o.zoom=i.getZoom(),r.enable(t.get(\"roam\")||!1);var a=t.mainType;function s(){var e={type:\"geoRoam\",componentType:a};return e[a+\"Id\"]=t.id,e}r.off(\"pan\").on(\"pan\",(function(t){this._mouseDownFlag=!1,KI(o,t.dx,t.dy),n.dispatchAction(A(s(),{dx:t.dx,dy:t.dy,animation:{duration:0}}))}),this),r.off(\"zoom\").on(\"zoom\",(function(t){this._mouseDownFlag=!1,$I(o,t.scale,t.originX,t.originY),n.dispatchAction(A(s(),{zoom:t.scale,originX:t.originX,originY:t.originY,animation:{duration:0}}))}),this),r.setPointerChecker((function(e,r,o){return i.containPoint([r,o])&&!tT(e,n,t)}))},t.prototype.resetForLabelLayout=function(){this.group.traverse((function(t){var e=t.getTextContent();e&&(e.ignore=WT(e).ignore)}))},t.prototype._updateMapSelectHandler=function(t,e,n,i){var r=this;e.off(\"mousedown\"),e.off(\"click\"),t.get(\"selectedMode\")&&(e.on(\"mousedown\",(function(){r._mouseDownFlag=!0})),e.on(\"click\",(function(t){r._mouseDownFlag&&(r._mouseDownFlag=!1)})))},t}();function UT(t,e,n,i){var r=i.getModel(\"itemStyle\"),o=i.getModel([\"emphasis\",\"itemStyle\"]),a=i.getModel([\"blur\",\"itemStyle\"]),s=i.getModel([\"select\",\"itemStyle\"]),l=HT(r),u=HT(o),h=HT(s),c=HT(a),p=t.data;if(p){var d=p.getItemVisual(n,\"style\"),f=p.getItemVisual(n,\"decal\");t.isVisualEncodedByVisualMap&&d.fill&&(l.fill=d.fill),f&&(l.decal=gv(f,t.api))}e.setStyle(l),e.style.strokeNoScale=!0,e.ensureState(\"emphasis\").style=u,e.ensureState(\"select\").style=h,e.ensureState(\"blur\").style=c,Cl(e)}function ZT(t,e,n,i,r,o,a){var s=t.data,l=t.isGeo,u=s&&isNaN(s.get(s.mapDimension(\"value\"),o)),h=s&&s.getItemLayout(o);if(l||u||h&&h.showLabel){var c=l?n:o,p=void 0;(!s||o>=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<t.length;e++)s.point(t[e][0],t[e][1]);s.lineEnd()})),!n&&s.polygonEnd(),r}var JT=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){if(!i||\"mapToggleSelect\"!==i.type||i.from!==this.uid){var r=this.group;if(r.removeAll(),!t.getHostGeoModel()){if(this._mapDraw&&i&&\"geoRoam\"===i.type&&this._mapDraw.resetForLabelLayout(),i&&\"geoRoam\"===i.type&&\"series\"===i.componentType&&i.seriesId===t.id)(o=this._mapDraw)&&r.add(o.group);else if(t.needsDrawMap){var o=this._mapDraw||new XT(n);r.add(o.group),o.draw(t,e,n,this,i),this._mapDraw=o}else this._mapDraw&&this._mapDraw.remove(),this._mapDraw=null;t.get(\"showLegendSymbol\")&&e.getComponent(\"legend\")&&this._renderSymbols(t,e,n)}}},e.prototype.remove=function(){this._mapDraw&&this._mapDraw.remove(),this._mapDraw=null,this.group.removeAll()},e.prototype.dispose=function(){this._mapDraw&&this._mapDraw.remove(),this._mapDraw=null},e.prototype._renderSymbols=function(t,e,n){var i=t.originalData,r=this.group;i.each(i.mapDimension(\"value\"),(function(e,n){if(!isNaN(e)){var o=i.getItemLayout(n);if(o&&o.point){var a=o.point,s=o.offset,l=new _u({style:{fill:t.getData().getVisual(\"style\").fill},shape:{cx:a[0]+9*s,cy:a[1],r:3},silent:!0,z2:8+(s?0:11)});if(!s){var u=t.mainSeries.getData(),h=i.getName(n),c=u.indexOfName(h),p=i.getItemModel(n),d=p.getModel(\"label\"),f=u.getItemGraphicEl(c);tc(l,ec(p),{labelFetcher:{getFormattedLabel:function(e,n){return t.getFormattedLabel(c,n)}},defaultText:h}),l.disableLabelAnimation=!0,d.get(\"position\")||l.setTextConfig({position:\"bottom\"}),f.onHoverStateChange=function(t){Il(l,t)}}r.add(l)}}}))},e.type=\"map\",e}(kg),QT=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.needsDrawMap=!1,n.seriesGroup=[],n.getTooltipPosition=function(t){if(null!=t){var e=this.getData().getName(t),n=this.coordinateSystem,i=n.getRegion(e);return i&&n.dataToPoint(i.getCenter())}},n}return n(e,t),e.prototype.getInitialData=function(t){for(var e=MM(this,{coordDimensions:[\"value\"],encodeDefaulter:H(Jp,this)}),n=yt(),i=[],r=0,o=e.count();r<o;r++){var a=e.getName(r);n.set(a,!0)}return E(zT(this.getMapType(),this.option.nameMap,this.option.nameProperty).regions,(function(t){var e=t.name;n.get(e)||i.push(e)})),e.appendValues([],i),e},e.prototype.getHostGeoModel=function(){var t=this.option.geoIndex;return null!=t?this.ecModel.getComponent(\"geo\",t):null},e.prototype.getMapType=function(){return(this.getHostGeoModel()||this).option.map},e.prototype.getRawValue=function(t){var e=this.getData();return e.get(e.mapDimension(\"value\"),t)},e.prototype.getRegionModel=function(t){var e=this.getData();return e.getItemModel(e.indexOfName(t))},e.prototype.formatTooltip=function(t,e,n){for(var i=this.getData(),r=this.getRawValue(t),o=i.getName(t),a=this.seriesGroup,s=[],l=0;l<a.length;l++){var u=a[l].originalData.indexOfName(o),h=i.mapDimension(\"value\");isNaN(a[l].originalData.get(h,u))||s.push(a[l].name)}return ng(\"section\",{header:s.join(\", \"),noHeader:!s.length,blocks:[ng(\"nameValue\",{name:o,value:r})]})},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.getLegendIcon=function(t){var e=t.icon||\"roundRect\",n=Wy(e,0,0,t.itemWidth,t.itemHeight,t.itemStyle.fill);return n.setStyle(t.itemStyle),n.style.stroke=\"none\",e.indexOf(\"empty\")>-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;h<u;h++)s=Math.min(s,r[o][h]),l=Math.max(l,r[o][h]),a+=r[o][h];return 0===u?NaN:\"min\"===i?s:\"max\"===i?l:\"average\"===i?a/u:a}))),a=0;a<t.length;a++)t[a].originalData=t[a].getData();for(a=0;a<t.length;a++)t[a].seriesGroup=t,t[a].needsDrawMap=0===a&&!t[a].getHostGeoModel(),t[a].setData(o.cloneShallow()),t[a].mainSeries=t[0]}))}function eC(t){var e={};t.eachSeriesByType(\"map\",(function(n){var i=n.getMapType();if(!n.getHostGeoModel()&&!e[i]){var r={};E(n.seriesGroup,(function(e){var n=e.coordinateSystem,i=e.originalData;e.get(\"showLegendSymbol\")&&t.getComponent(\"legend\")&&i.each(i.mapDimension(\"value\"),(function(t,e){var o=i.getName(e),a=n.getRegion(o);if(a&&!isNaN(t)){var s=r[o]||0,l=n.dataToPoint(a.getCenter());r[o]=s+1,i.setItemLayout(e,{point:l,offset:s})}}))}));var o=n.getData();o.each((function(t){var e=o.getName(t),n=o.getItemLayout(t)||{};n.showLabel=!r[e],o.setItemLayout(t,n)})),e[i]=!0}}))}var nC=Wt,iC=function(t){function e(e){var n=t.call(this)||this;return n.type=\"view\",n.dimensions=[\"x\",\"y\"],n._roamTransformable=new gr,n._rawTransformable=new gr,n.name=e,n}return n(e,t),e.prototype.setBoundingRect=function(t,e,n,i){return this._rect=new ze(t,e,n,i),this._rect},e.prototype.getBoundingRect=function(){return this._rect},e.prototype.setViewRect=function(t,e,n,i){this._transformTo(t,e,n,i),this._viewRect=new ze(t,e,n,i)},e.prototype._transformTo=function(t,e,n,i){var r=this.getBoundingRect(),o=this._rawTransformable;o.transform=r.calculateTransform(new ze(t,e,n,i));var a=o.parent;o.parent=null,o.decomposeTransform(),o.parent=a,this._updateTransform()},e.prototype.setCenter=function(t,e){t&&(this._center=[Ur(t[0],e.getWidth()),Ur(t[1],e.getHeight())],this._updateCenterAndZoom())},e.prototype.setZoom=function(t){t=t||1;var e=this.zoomLimit;e&&(null!=e.max&&(t=Math.min(e.max,t)),null!=e.min&&(t=Math.max(e.min,t))),this._zoom=t,this._updateCenterAndZoom()},e.prototype.getDefaultCenter=function(){var t=this.getBoundingRect();return[t.x+t.width/2,t.y+t.height/2]},e.prototype.getCenter=function(){return this._center||this.getDefaultCenter()},e.prototype.getZoom=function(){return this._zoom||1},e.prototype.getRoamTransform=function(){return this._roamTransformable.getLocalTransform()},e.prototype._updateCenterAndZoom=function(){var t=this._rawTransformable.getLocalTransform(),e=this._roamTransformable,n=this.getDefaultCenter(),i=this.getCenter(),r=this.getZoom();i=Wt([],i,t),n=Wt([],n,t),e.originX=i[0],e.originY=i[1],e.x=n[0]-i[0],e.y=n[1]-i[1],e.scaleX=e.scaleY=r,this._updateTransform()},e.prototype._updateTransform=function(){var t=this._roamTransformable,e=this._rawTransformable;e.parent=t,t.updateTransform(),e.updateTransform(),_e(this.transform||(this.transform=[]),e.transform||[1,0,0,1,0,0]),this._rawTransform=e.getLocalTransform(),this.invTransform=this.invTransform||[],Ie(this.invTransform,this.transform),this.decomposeTransform()},e.prototype.getTransformInfo=function(){var t=this._rawTransformable,e=this._roamTransformable,n=new gr;return n.transform=e.transform,n.decomposeTransform(),{roam:{x:n.x,y:n.y,scaleX:n.scaleX,scaleY:n.scaleY},raw:{x:t.x,y:t.y,scaleX:t.scaleX,scaleY:t.scaleY}}},e.prototype.getViewRect=function(){return this._viewRect},e.prototype.getViewRectAfterRoam=function(){var t=this.getBoundingRect().clone();return t.applyTransform(this.transform),t},e.prototype.dataToPoint=function(t,e,n){var i=e?this._rawTransform:this.transform;return n=n||[],i?nC(n,t,i):It(n,t)},e.prototype.pointToData=function(t){var e=this.invTransform;return e?nC([],t,e):[t[0],t[1]]},e.prototype.convertToPixel=function(t,e,n){var i=rC(e);return i===this?i.dataToPoint(n):null},e.prototype.convertFromPixel=function(t,e,n){var i=rC(e);return i===this?i.pointToData(n):null},e.prototype.containPoint=function(t){return this.getViewRectAfterRoam().contain(t[0],t[1])},e.dimensions=[\"x\",\"y\"],e}(gr);function rC(t){var e=t.seriesModel;return e?e.coordinateSystem:null}var oC={geoJSON:{aspectScale:.75,invertLongitute:!0},geoSVG:{aspectScale:1,invertLongitute:!1}},aC=[\"lng\",\"lat\"],sC=function(t){function e(e,n,i){var r=t.call(this,e)||this;r.dimensions=aC,r.type=\"geo\",r._nameCoordMap=yt(),r.map=n;var o,a=i.projection,s=zT(n,i.nameMap,i.nameProperty),l=NT(n),u=(r.resourceType=l?l.type:null,r.regions=s.regions),h=oC[l.type];if(r._regionsMap=s.regionsMap,r.regions=s.regions,r.projection=a,a)for(var c=0;c<u.length;c++){var p=u[c].getBoundingRect(a);(o=o||p.clone()).union(p)}else o=s.boundingRect;return r.setBoundingRect(o.x,o.y,o.width,o.height),r.aspectScale=a?1:rt(i.aspectScale,h.aspectScale),r._invertLongitute=!a&&h.invertLongitute,r}return n(e,t),e.prototype._transformTo=function(t,e,n,i){var r=this.getBoundingRect(),o=this._invertLongitute;r=r.clone(),o&&(r.y=-r.y-r.height);var a=this._rawTransformable;a.transform=r.calculateTransform(new ze(t,e,n,i));var s=a.parent;a.parent=null,a.decomposeTransform(),a.parent=s,o&&(a.scaleY=-a.scaleY),this._updateTransform()},e.prototype.getRegion=function(t){return this._regionsMap.get(t)},e.prototype.getRegionByCoord=function(t){for(var e=this.regions,n=0;n<e.length;n++){var i=e[n];if(\"geoJSON\"===i.type&&i.contain(t))return e[n]}},e.prototype.addGeoCoord=function(t,e){this._nameCoordMap.set(t,e)},e.prototype.getGeoCoord=function(t){var e=this._regionsMap.get(t);return this._nameCoordMap.get(t)||e&&e.getCenter()},e.prototype.dataToPoint=function(t,e,n){if(U(t)&&(t=this.getGeoCoord(t)),t){var i=this.projection;return i&&(t=i.project(t)),t&&this.projectedToPoint(t,e,n)}},e.prototype.pointToData=function(t){var e=this.projection;return e&&(t=e.unproject(t)),t&&this.pointToProjected(t)},e.prototype.pointToProjected=function(e){return t.prototype.pointToData.call(this,e)},e.prototype.projectedToPoint=function(e,n,i){return t.prototype.dataToPoint.call(this,e,n,i)},e.prototype.convertToPixel=function(t,e,n){var i=lC(e);return i===this?i.dataToPoint(n):null},e.prototype.convertFromPixel=function(t,e,n){var i=lC(e);return i===this?i.pointToData(n):null},e}(iC);function lC(t){var e=t.geoModel,n=t.seriesModel;return e?e.coordinateSystem:n?n.coordinateSystem||(n.getReferringComponents(\"geo\",zo).models[0]||{}).coordinateSystem:null}function uC(t,e){var n=t.get(\"boundingCoords\");if(null!=n){var i=n[0],r=n[1];if(isFinite(i[0])&&isFinite(i[1])&&isFinite(r[0])&&isFinite(r[1])){var o=this.projection;if(o){var a=i[0],s=i[1],l=r[0],u=r[1];i=[1/0,1/0],r=[-1/0,-1/0];var h=function(t,e,n,a){for(var s=n-t,l=a-e,u=0;u<=100;u++){var h=u/100,c=o.project([t+s*h,e+l*h]);Ht(i,i,c),Yt(r,r,c)}};h(a,s,l,s),h(l,s,l,u),h(l,u,a,u),h(a,u,l,s)}this.setBoundingRect(i[0],i[1],r[0]-i[0],r[1]-i[1])}else 0}var c,p,d,f=this.getBoundingRect(),g=t.get(\"layoutCenter\"),y=t.get(\"layoutSize\"),v=e.getWidth(),m=e.getHeight(),x=f.width/f.height*this.aspectScale,_=!1;if(g&&y&&(c=[Ur(g[0],v),Ur(g[1],m)],p=Ur(y,Math.min(v,m)),isNaN(c[0])||isNaN(c[1])||isNaN(p)||(_=!0)),_)d={},x>1?(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<r.length;a++)o.set(r[a].name,r[a]);return E(zT(e,n,i).regions,(function(t){var e=t.name;!o.get(e)&&r.push({name:e})})),r},t}(),cC=new hC,pC=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,n){var i=NT(t.map);if(i&&\"geoJSON\"===i.type){var r=t.itemStyle=t.itemStyle||{};\"color\"in r||(r.color=\"#eee\")}this.mergeDefaultAndTheme(t,n),wo(t,\"label\",[\"show\"])},e.prototype.optionUpdated=function(){var t=this,e=this.option;e.regions=cC.getFilledRegions(e.regions,e.map,e.nameMap,e.nameProperty);var n={};this._optionModelMap=V(e.regions||[],(function(e,i){var r=i.name;return r&&(e.set(r,new Mc(i,t,t.ecModel)),i.selected&&(n[r]=!0)),e}),yt()),e.selectedMap||(e.selectedMap=n)},e.prototype.getRegionModel=function(t){return this._optionModelMap.get(t)||new Mc(null,this,this.ecModel)},e.prototype.getFormattedLabel=function(t,e){var n=this.getRegionModel(t),i=\"normal\"===e?n.get([\"label\",\"formatter\"]):n.get([\"emphasis\",\"label\",\"formatter\"]),r={name:t};return X(i)?(r.status=e,i(r)):U(i)?i.replace(\"{a}\",null!=t?t:\"\"):void 0},e.prototype.setZoom=function(t){this.option.zoom=t},e.prototype.setCenter=function(t){this.option.center=t},e.prototype.select=function(t){var e=this.option,n=e.selectedMode;n&&(\"multiple\"!==n&&(e.selectedMap=null),(e.selectedMap||(e.selectedMap={}))[t]=!0)},e.prototype.unSelect=function(t){var e=this.option.selectedMap;e&&(e[t]=!1)},e.prototype.toggleSelected=function(t){this[this.isSelected(t)?\"unSelect\":\"select\"](t)},e.prototype.isSelected=function(t){var e=this.option.selectedMap;return!(!e||!e[t])},e.type=\"geo\",e.layoutMode=\"box\",e.defaultOption={z:0,show:!0,left:\"center\",top:\"center\",aspectScale:null,silent:!1,map:\"\",boundingCoords:null,center:null,zoom:1,scaleLimit:null,label:{show:!1,color:\"#000\"},itemStyle:{borderWidth:.5,borderColor:\"#444\"},emphasis:{label:{show:!0,color:\"rgb(100,0,0)\"},itemStyle:{color:\"rgba(255,215,0,0.8)\"}},select:{label:{show:!0,color:\"rgb(100,0,0)\"},itemStyle:{color:\"rgba(255,215,0,0.8)\"}},regions:[]},e}(Rp);function dC(t,e){return t.pointToProjected?t.pointToProjected(e):t.pointToData(e)}function fC(t,e,n,i){var r=t.getZoom(),o=t.getCenter(),a=e.zoom,s=t.projectedToPoint?t.projectedToPoint(o):t.dataToPoint(o);if(null!=e.dx&&null!=e.dy&&(s[0]-=e.dx,s[1]-=e.dy,t.setCenter(dC(t,s),i)),null!=a){if(n){var l=n.min||0,u=n.max||1/0;a=Math.max(Math.min(r*a,u),l)/r}t.scaleX*=a,t.scaleY*=a;var h=(e.originX-t.x)*(a-1),c=(e.originY-t.y)*(a-1);t.x-=h,t.y-=c,t.updateTransform(),t.setCenter(dC(t,s),i),t.setZoom(a*r)}return{center:t.getCenter(),zoom:t.getZoom()}}var gC=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.focusBlurEnabled=!0,n}return n(e,t),e.prototype.init=function(t,e){this._api=e},e.prototype.render=function(t,e,n,i){if(this._model=t,!t.get(\"show\"))return this._mapDraw&&this._mapDraw.remove(),void(this._mapDraw=null);this._mapDraw||(this._mapDraw=new XT(n));var r=this._mapDraw;r.draw(t,e,n,this,i),r.group.on(\"click\",this._handleRegionClick,this),r.group.silent=t.get(\"silent\"),this.group.add(r.group),this.updateSelectStatus(t,e,n)},e.prototype._handleRegionClick=function(t){var e;ky(t.target,(function(t){return null!=(e=Qs(t).eventData)}),!0),e&&this._api.dispatchAction({type:\"geoToggleSelect\",geoId:this._model.id,name:e.name})},e.prototype.updateSelectStatus=function(t,e,n){var i=this;this._mapDraw.group.traverse((function(t){var e=Qs(t).eventData;if(e)return i._model.isSelected(e.name)?n.enterSelect(t):n.leaveSelect(t),!0}))},e.prototype.findHighDownDispatchers=function(t){return this._mapDraw&&this._mapDraw.findHighDownDispatchers(t,this._model)},e.prototype.dispose=function(){this._mapDraw&&this._mapDraw.remove()},e.type=\"geo\",e}(Tg);function yC(t,e,n){RT(t,e,n)}function vC(t){function e(e,n){n.update=\"geo:updateSelectStatus\",t.registerAction(n,(function(t,n){var i={},r=[];return n.eachComponent({mainType:\"geo\",query:t},(function(n){n[e](t.name),E(n.coordinateSystem.regions,(function(t){i[t.name]=n.isSelected(t.name)||!1}));var o=[];E(i,(function(t,e){i[e]&&o.push(e)})),r.push({geoIndex:n.componentIndex,name:o})})),{selected:i,allSelected:r,name:t.name}}))}t.registerCoordinateSystem(\"geo\",cC),t.registerComponentModel(pC),t.registerComponentView(gC),t.registerImpl(\"registerMap\",yC),t.registerImpl(\"getMap\",(function(t){return ET(t)})),e(\"toggleSelected\",{type:\"geoToggleSelect\",event:\"geoselectchanged\"}),e(\"select\",{type:\"geoSelect\",event:\"geoselected\"}),e(\"unSelect\",{type:\"geoUnSelect\",event:\"geounselected\"}),t.registerAction({type:\"geoRoam\",event:\"geoRoam\",update:\"updateTransform\"},(function(t,e,n){var i=t.componentType||\"series\";e.eachComponent({mainType:i,query:t},(function(e){var r=e.coordinateSystem;if(\"geo\"===r.type){var o=fC(r,t,e.get(\"scaleLimit\"),n);e.setCenter&&e.setCenter(o.center),e.setZoom&&e.setZoom(o.zoom),\"series\"===i&&E(e.seriesGroup,(function(t){t.setCenter(o.center),t.setZoom(o.zoom)}))}}))}))}function mC(t,e){var n=t.isExpand?t.children:[],i=t.parentNode.children,r=t.hierNode.i?i[t.hierNode.i-1]:null;if(n.length){!function(t){var e=t.children,n=e.length,i=0,r=0;for(;--n>=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;p<i-1;p++){var d=n[p];t.moveTo(d[0],d[1]),c[l]=d[l],t.lineTo(c[0],c[1])}},e}(Is),AC=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._mainGroup=new zr,n}return n(e,t),e.prototype.init=function(t,e){this._controller=new UI(e.getZr()),this._controllerHost={target:this.group},this.group.add(this._mainGroup)},e.prototype.render=function(t,e,n){var i=t.getData(),r=t.layoutInfo,o=this._mainGroup;\"radial\"===t.get(\"layout\")?(o.x=r.x+r.width/2,o.y=r.y+r.height/2):(o.x=r.x,o.y=r.y),this._updateViewCoordSys(t,n),this._updateController(t,e,n);var a=this._data;i.diff(a).add((function(e){kC(i,e)&&LC(i,e,null,o,t)})).update((function(e,n){var r=a.getItemGraphicEl(n);kC(i,e)?LC(i,e,r,o,t):r&&RC(a,n,r,o,t)})).remove((function(e){var n=a.getItemGraphicEl(e);n&&RC(a,e,n,o,t)})).execute(),this._nodeScaleRatio=t.get(\"nodeScaleRatio\"),this._updateNodeAndLinkScale(t),!0===t.get(\"expandAndCollapse\")&&i.eachItemGraphicEl((function(e,i){e.off(\"click\").on(\"click\",(function(){n.dispatchAction({type:\"treeExpandAndCollapse\",seriesId:t.id,dataIndex:i})}))})),this._data=i},e.prototype._updateViewCoordSys=function(t,e){var n=t.getData(),i=[];n.each((function(t){var e=n.getItemLayout(t);!e||isNaN(e.x)||isNaN(e.y)||i.push([+e.x,+e.y])}));var r=[],o=[];Ra(i,r,o);var a=this._min,s=this._max;o[0]-r[0]==0&&(r[0]=a?a[0]:r[0]-1,o[0]=s?s[0]:o[0]+1),o[1]-r[1]==0&&(r[1]=a?a[1]:r[1]-1,o[1]=s?s[1]:o[1]+1);var l=t.coordinateSystem=new iC;l.zoomLimit=t.get(\"scaleLimit\"),l.setBoundingRect(r[0],r[1],o[0]-r[0],o[1]-r[1]),l.setCenter(t.get(\"center\"),e),l.setZoom(t.get(\"zoom\")),this.group.attr({x:l.x,y:l.y,scaleX:l.scaleX,scaleY:l.scaleY}),this._min=r,this._max=o},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)})),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:\"treeRoam\",dx:e.dx,dy:e.dy})})).on(\"zoom\",(function(e){$I(o,e.scale,e.originX,e.originY),n.dispatchAction({seriesId:t.id,type:\"treeRoam\",zoom:e.scale,originX:e.originX,originY:e.originY}),i._updateNodeAndLinkScale(t),n.updateLabelLayout()}))},e.prototype._updateNodeAndLinkScale=function(t){var e=t.getData(),n=this._getNodeGlobalScale(t);e.eachItemGraphicEl((function(t,e){t.setSymbolScale(n)}))},e.prototype._getNodeGlobalScale=function(t){var e=t.coordinateSystem;if(\"view\"!==e.type)return 1;var n=this._nodeScaleRatio,i=e.scaleX||1;return((e.getZoom()-1)*n+1)/i},e.prototype.dispose=function(){this._controller&&this._controller.dispose(),this._controllerHost=null},e.prototype.remove=function(){this._mainGroup.removeAll(),this._data=null},e.type=\"tree\",e}(kg);function kC(t,e){var n=t.getItemLayout(e);return n&&!isNaN(n.x)&&!isNaN(n.y)}function LC(t,e,n,i,r){var o=!n,a=t.tree.getNodeByDataIndex(e),s=a.getModel(),l=a.getVisual(\"style\").fill,u=!1===a.isExpand&&0!==a.children.length?l:\"#fff\",h=t.tree.root,c=a.parentNode===h?a:a.parentNode||a,p=t.getItemGraphicEl(c.dataIndex),d=c.getLayout(),f=p?{x:p.__oldX,y:p.__oldY,rawX:p.__radialOldRawX,rawY:p.__radialOldRawY}:d,g=a.getLayout();o?((n=new oS(t,e,null,{symbolInnerColor:u,useNameLabel:!0})).x=f.x,n.y=f.y):n.updateData(t,e,null,{symbolInnerColor:u,useNameLabel:!0}),n.__radialOldRawX=n.__radialRawX,n.__radialOldRawY=n.__radialRawY,n.__radialRawX=g.rawX,n.__radialRawY=g.rawY,i.add(n),t.setItemGraphicEl(e,n),n.__oldX=n.x,n.__oldY=n.y,fh(n,{x:g.x,y:g.y},r);var y=n.getSymbolPath();if(\"radial\"===r.get(\"layout\")){var v=h.children[0],m=v.getLayout(),x=v.children.length,_=void 0,b=void 0;if(g.x===m.x&&!0===a.isExpand&&v.children.length){var w={x:(v.children[0].getLayout().x+v.children[x-1].getLayout().x)/2,y:(v.children[0].getLayout().y+v.children[x-1].getLayout().y)/2};(_=Math.atan2(w.y-m.y,w.x-m.x))<0&&(_=2*Math.PI+_),(b=w.x<m.x)&&(_-=Math.PI)}else(_=Math.atan2(g.y-m.y,g.x-m.x))<0&&(_=2*Math.PI+_),0===a.children.length||0!==a.children.length&&!1===a.isExpand?(b=g.x<m.x)&&(_-=Math.PI):(b=g.x>m.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;m<y.length;m++){var x=y[m].getLayout();v.push([x.x,x.y])}g||(g=i.__edge=new DC({shape:{parentPoint:[a.x,a.y],childPoints:[[a.x,a.y]],orient:c,forkPosition:d}})),fh(g,{shape:{parentPoint:[a.x,a.y],childPoints:v}},t)}}else 0;g&&(\"polyline\"!==u||e.isExpand)&&(g.useStyle(k({strokeNoScale:!0,fill:null},f)),jl(g,l,\"lineStyle\"),Cl(g),s.add(g))}(r,a,h,n,f,d,g,i),n.__edge&&(n.onHoverStateChange=function(e){if(\"blur\"!==e){var i=a.parentNode&&t.getItemGraphicEl(a.parentNode.dataIndex);i&&1===i.hoverState||Il(n.__edge,e)}})}function PC(t,e,n,i,r){var o=OC(e.tree.root,t),a=o.source,s=o.sourceLayout,l=e.getItemGraphicEl(t.dataIndex);if(l){var u=e.getItemGraphicEl(a.dataIndex).__edge,h=l.__edge||(!1===a.isExpand||1===a.children.length?u:void 0),c=i.get(\"edgeShape\"),p=i.get(\"layout\"),d=i.get(\"orient\"),f=i.get([\"lineStyle\",\"curveness\"]);h&&(\"curve\"===c?vh(h,{shape:NC(p,d,f,s,s),style:{opacity:0}},i,{cb:function(){n.remove(h)},removeOpt:r}):\"polyline\"===c&&\"orthogonal\"===i.get(\"layout\")&&vh(h,{shape:{parentPoint:[s.x,s.y],childPoints:[[s.x,s.y]]},style:{opacity:0}},i,{cb:function(){n.remove(h)},removeOpt:r}))}}function OC(t,e){for(var n,i=e.parentNode===t?e:e.parentNode||e;null==(n=i.getLayout());)i=i.parentNode===t?i:i.parentNode||i;return{source:i,sourceLayout:n}}function RC(t,e,n,i,r){var o=t.tree.getNodeByDataIndex(e),a=OC(t.tree.root,o).sourceLayout,s={duration:r.get(\"animationDurationUpdate\"),easing:r.get(\"animationEasingUpdate\")};vh(n,{x:a.x+1,y:a.y+1},r,{cb:function(){i.remove(n),t.setItemGraphicEl(e,null)},removeOpt:s}),n.fadeOut(null,t.hostModel,{fadeLabel:!0,animation:s}),o.children.forEach((function(e){PC(e,t,i,r,s)})),PC(o,t,i,r,s)}function NC(t,e,n,i,r){var o,a,s,l,u,h,c,p;if(\"radial\"===t){u=i.rawX,c=i.rawY,h=r.rawX,p=r.rawY;var d=bC(u,c),f=bC(u,c+(p-c)*n),g=bC(h,p+(c-p)*n),y=bC(h,p);return{x1:d.x||0,y1:d.y||0,x2:y.x||0,y2:y.y||0,cpx1:f.x||0,cpy1:f.y||0,cpx2:g.x||0,cpy2:g.y||0}}return u=i.x,c=i.y,h=r.x,p=r.y,\"LR\"!==e&&\"RL\"!==e||(o=u+(h-u)*n,a=c,s=h+(u-h)*n,l=p),\"TB\"!==e&&\"BT\"!==e||(o=u,a=c+(p-c)*n,s=h,l=p+(c-p)*n),{x1:u,y1:c,x2:h,y2:p,cpx1:o,cpy1:a,cpx2:s,cpy2:l}}var EC=Oo();function zC(t){var e=t.mainData,n=t.datas;n||(n={main:e},t.datasAttr={main:\"data\"}),t.datas=t.mainData=null,HC(e,n,t),E(n,(function(n){E(e.TRANSFERABLE_METHODS,(function(e){n.wrapMethod(e,H(VC,t))}))})),e.wrapMethod(\"cloneShallow\",H(FC,t)),E(e.CHANGABLE_METHODS,(function(n){e.wrapMethod(n,H(BC,t))})),lt(n[e.dataType]===e)}function VC(t,e){if(EC(i=this).mainData===i){var n=A({},EC(this).datas);n[this.dataType]=e,HC(e,n,t)}else YC(e,this.dataType,EC(this).mainData,t);var i;return e}function BC(t,e){return t.struct&&t.struct.update(),e}function FC(t,e){return E(EC(e).datas,(function(n,i){n!==e&&YC(n.cloneShallow(),i,e,t)})),e}function GC(t){var e=EC(this).mainData;return null==t||null==e?e:EC(e).datas[t]}function WC(){var t=EC(this).mainData;return null==t?[{data:t}]:z(G(EC(t).datas),(function(e){return{type:e,data:EC(t).datas[e]}}))}function HC(t,e,n){EC(t).datas={},E(e,(function(e,i){YC(e,i,t,n)}))}function YC(t,e,n,i){EC(n).datas[e]=t,EC(t).mainData=n,t.dataType=e,i.struct&&(t[i.structAttr]=i.struct,i.struct[i.datasAttr[e]]=t),t.getLinkedData=GC,t.getLinkedDataAll=WC}var XC=function(){function t(t,e){this.depth=0,this.height=0,this.dataIndex=-1,this.children=[],this.viewChildren=[],this.isExpand=!1,this.name=t||\"\",this.hostTree=e}return t.prototype.isRemoved=function(){return this.dataIndex<0},t.prototype.eachNode=function(t,e,n){X(t)&&(n=e,e=t,t=null),U(t=t||{})&&(t={order:t});var i,r=t.order||\"preorder\",o=this[t.attr||\"children\"];\"preorder\"===r&&(i=e.call(n,this));for(var a=0;!i&&a<o.length;a++)o[a].eachNode(t,e,n);\"postorder\"===r&&e.call(n,this)},t.prototype.updateDepthAndHeight=function(t){var e=0;this.depth=t;for(var n=0;n<this.children.length;n++){var i=this.children[n];i.updateDepthAndHeight(t+1),i.height>e&&(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<i;e++){var r=n[e].getNodeById(t);if(r)return r}},t.prototype.contains=function(t){if(t===this)return!0;for(var e=0,n=this.children,i=n.length;e<i;e++){var r=n[e].contains(t);if(r)return r}},t.prototype.getAncestors=function(t){for(var e=[],n=t?this:this.parentNode;n;)e.push(n),n=n.parentNode;return e.reverse(),e},t.prototype.getAncestorsIndices=function(){for(var t=[],e=this;e;)t.push(e.dataIndex),e=e.parentNode;return t.reverse(),t},t.prototype.getDescendantIndices=function(){var t=[];return this.eachNode((function(e){t.push(e.dataIndex)})),t},t.prototype.getValue=function(t){var e=this.hostTree.data;return e.getStore().get(e.getDimensionIndex(t||\"value\"),this.dataIndex)},t.prototype.setLayout=function(t,e){this.dataIndex>=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<t.length;++e)if(t[e]===this)return e;return-1}return-1},t.prototype.isAncestorOf=function(t){for(var e=t.parentNode;e;){if(e===this)return!0;e=e.parentNode}return!1},t.prototype.isDescendantOf=function(t){return t!==this&&t.isAncestorOf(this)},t}(),UC=function(){function t(t){this.type=\"tree\",this._nodes=[],this.hostModel=t}return t.prototype.eachNode=function(t,e,n){this.root.eachNode(t,e,n)},t.prototype.getNodeByDataIndex=function(t){var e=this.data.getRawIndex(t);return this._nodes[e]},t.prototype.getNodeById=function(t){return this.root.getNodeById(t)},t.prototype.update=function(){for(var t=this.data,e=this._nodes,n=0,i=e.length;n<i;n++)e[n].dataIndex=-1;for(n=0,i=t.count();n<i;n++)e[t.getRawIndex(n)].dataIndex=n},t.prototype.clearLayouts=function(){this.data.clearItemLayouts()},t.createTree=function(e,n,i){var r=new t(n),o=[],a=1;!function t(e,n){var i=e.value;a=Math.max(a,Y(i)?i.length:1),o.push(e);var s=new XC(Ao(e.name,\"\"),r);n?function(t,e){var n=e.children;if(t.parentNode===e)return;n.push(t),t.parentNode=e}(s,n):r.root=s,r._nodes.push(s);var l=e.children;if(l)for(var u=0;u<l.length;u++)t(l[u],s)}(e),r.root.updateDepthAndHeight(0);var s=ux(o,{coordDimensions:[\"value\"],dimensionsCount:a}).dimensions,l=new lx(s,n);return l.initData(o),i&&i(l),zC({mainData:l,struct:r,structAttr:\"tree\"}),r.update(),r},t}();function ZC(t,e,n){if(t&&P(e,t.type)>=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;s<a.length;s++)r.push(a[s])}for(;i=o.pop();)e(i,n)}(l,mC,a),s.hierNode.modifier=-l.hierNode.prelim,JC(l,xC);var u=l,h=l,c=l;JC(l,(function(t){var e=t.getLayout().x;e<u.getLayout().x&&(u=t),e>h.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;i<r;i++){var o=this._storage[i];o.el.animateTo(o.target,{duration:o.duration,delay:o.delay,easing:o.easing,setToFinal:!0,done:n,aborted:n})}return this},t}();var uD=zr,hD=zs,cD=\"label\",pD=\"upperLabel\",dD=Jo([[\"fill\",\"color\"],[\"stroke\",\"strokeColor\"],[\"lineWidth\",\"strokeWidth\"],[\"shadowBlur\"],[\"shadowOffsetX\"],[\"shadowOffsetY\"],[\"shadowColor\"]]),fD=function(t){var e=dD(t);return e.stroke=e.fill=e.lineWidth=null,e},gD=Oo(),yD=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._state=\"ready\",n._storage={nodeGroup:[],background:[],content:[]},n}return n(e,t),e.prototype.render=function(t,e,n,i){if(!(P(e.findComponents({mainType:\"series\",subType:\"treemap\",query:i}),t)<0)){this.seriesModel=t,this.api=n,this.ecModel=e;var r=ZC(i,[\"treemapZoomToNode\",\"treemapRootToNode\"],t),o=i&&i.type,a=t.layoutInfo,s=!this._oldTree,l=this._storage,u=\"treemapRootToNode\"===o&&r&&l?{rootNodeGroup:l.nodeGroup[r.node.getRawIndex()],direction:i.direction}:null,h=this._giveContainerGroup(a),c=t.get(\"animation\"),p=this._doRender(h,t,u);!c||s||o&&\"treemapZoomToNode\"!==o&&\"treemapRootToNode\"!==o?p.renderFinally():this._doAnimation(h,p,t,u),this._resetController(n),this._renderBreadcrumb(t,n,r)}},e.prototype._giveContainerGroup=function(t){var e=this._containerGroup;return e||(e=this._containerGroup=new uD,this._initEvents(e),this.group.add(e)),e.x=t.x,e.y=t.y,e},e.prototype._doRender=function(t,e,n){var i=e.getData().tree,r=this._oldTree,o={nodeGroup:[],background:[],content:[]},a={nodeGroup:[],background:[],content:[]},s=this._storage,l=[];function u(t,i,r,u){return function(t,e,n,i,r,o,a,s,l,u){if(!a)return;var h=a.getLayout(),c=t.getData(),p=a.getModel();if(c.setItemGraphicEl(a.dataIndex,null),!h||!h.isInView)return;var d=h.width,f=h.height,g=h.borderWidth,y=h.invisible,v=a.getRawIndex(),m=s&&s.getRawIndex(),x=a.viewChildren,_=h.upperHeight,b=x&&x.length,w=p.getModel(\"itemStyle\"),S=p.getModel([\"emphasis\",\"itemStyle\"]),M=p.getModel([\"blur\",\"itemStyle\"]),I=p.getModel([\"select\",\"itemStyle\"]),T=w.get(\"borderRadius\")||0,C=G(\"nodeGroup\",uD);if(!C)return;if(l.add(C),C.x=h.x||0,C.y=h.y||0,C.markRedraw(),gD(C).nodeWidth=d,gD(C).nodeHeight=f,h.isAboveViewRoot)return C;var D=G(\"background\",hD,u,20);D&&E(C,D,b&&h.upperLabelHeight);var k=p.getModel(\"emphasis\"),L=k.get(\"focus\"),P=k.get(\"blurScope\"),O=k.get(\"disabled\"),R=\"ancestor\"===L?a.getAncestorsIndices():\"descendant\"===L?a.getDescendantIndices():L;if(b)Kl(C)&&ql(C,!1),D&&(ql(D,!O),c.setItemGraphicEl(a.dataIndex,D),Xl(D,R,P));else{var N=G(\"content\",hD,u,30);N&&z(C,N),D.disableMorphing=!0,D&&Kl(D)&&ql(D,!1),ql(C,!O),c.setItemGraphicEl(a.dataIndex,C),Xl(C,R,P)}return C;function E(e,n,i){var r=Qs(n);if(r.dataIndex=a.dataIndex,r.seriesIndex=t.seriesIndex,n.setShape({x:0,y:0,width:d,height:f,r:T}),y)V(n);else{n.invisible=!1;var o=a.getVisual(\"style\"),s=o.stroke,l=fD(w);l.fill=s;var u=dD(S);u.fill=S.get(\"borderColor\");var h=dD(M);h.fill=M.get(\"borderColor\");var c=dD(I);if(c.fill=I.get(\"borderColor\"),i){var p=d-2*g;B(n,s,o.opacity,{x:g,y:0,width:p,height:_})}else n.removeTextContent();n.setStyle(l),n.ensureState(\"emphasis\").style=u,n.ensureState(\"blur\").style=h,n.ensureState(\"select\").style=c,Cl(n)}e.add(n)}function z(e,n){var i=Qs(n);i.dataIndex=a.dataIndex,i.seriesIndex=t.seriesIndex;var r=Math.max(d-2*g,0),o=Math.max(f-2*g,0);if(n.culling=!0,n.setShape({x:g,y:g,width:r,height:o,r:T}),y)V(n);else{n.invisible=!1;var s=a.getVisual(\"style\"),l=s.fill,u=fD(w);u.fill=l,u.decal=s.decal;var h=dD(S),c=dD(M),p=dD(I);B(n,l,s.opacity,null),n.setStyle(u),n.ensureState(\"emphasis\").style=h,n.ensureState(\"blur\").style=c,n.ensureState(\"select\").style=p,Cl(n)}e.add(n)}function V(t){!t.invisible&&o.push(t)}function B(e,n,i,r){var o=p.getModel(r?pD:cD),s=Ao(p.get(\"name\"),null),l=o.getShallow(\"show\");tc(e,ec(p,r?pD:cD),{defaultText:l?s:null,inheritColor:n,defaultOpacity:i,labelFetcher:t,labelDataIndex:a.dataIndex});var u=e.getTextContent();if(u){var c=u.style,d=st(c.padding||0);r&&(e.setTextConfig({layoutRect:r}),u.disableLabelLayout=!0),u.beforeUpdate=function(){var t=Math.max((r?r.width:e.shape.width)-d[1]-d[3],0),n=Math.max((r?r.height:e.shape.height)-d[0]-d[2],0);c.width===t&&c.height===n||u.setStyle({width:t,height:n})},c.truncateMinChar=2,c.lineOverflow=\"truncate\",F(c,r,h);var f=u.getState(\"emphasis\");F(f?f.style:null,r,h)}}function F(e,n,i){var r=e?e.text:null;if(!n&&i.isLeafRoot&&null!=r){var o=t.get(\"drillDownIcon\",!0);e.text=o?o+\" \"+r:r}}function G(t,i,o,a){var s=null!=m&&n[t][m],l=r[t];return s?(n[t][m]=null,W(l,s)):y||((s=new i)instanceof Sa&&(s.z2=function(t,e){return 100*t+e}(o,a)),H(l,s)),e[t][v]=s}function W(t,e){var n=t[v]={};e instanceof uD?(n.oldX=e.x,n.oldY=e.y):n.oldShape=A({},e.shape)}function H(t,e){var n=t[v]={},o=a.parentNode,s=e instanceof zr;if(o&&(!i||\"drillDown\"===i.direction)){var l=0,u=0,h=r.background[o.getRawIndex()];!i&&h&&h.oldShape&&(l=h.oldShape.width,u=h.oldShape.height),s?(n.oldX=0,n.oldY=u):n.oldShape={x:l,y:u,width:0,height:0}}n.fadein=!s}}(e,a,s,n,o,l,t,i,r,u)}!function t(e,n,i,r,o){r?(n=e,E(e,(function(t,e){!t.isRemoved()&&s(e,e)}))):new Vm(n,e,a,a).add(s).update(s).remove(H(s,null)).execute();function a(t){return t.getId()}function s(a,s){var l=null!=a?e[a]:null,h=null!=s?n[s]:null,c=u(l,h,i,o);c&&t(l&&l.viewChildren||[],h&&h.viewChildren||[],c,r,o+1)}}(i.root?[i.root]:[],r&&r.root?[r.root]:[],t,i===r||!r,0);var h=function(t){var e={nodeGroup:[],background:[],content:[]};return t&&E(t,(function(t,n){var i=e[n];E(t,(function(t){t&&(i.push(t),gD(t).willDelete=!0)}))})),e}(s);return this._oldTree=i,this._storage=a,{lastsForAnimation:o,willDeleteEls:h,renderFinally:function(){E(h,(function(t){E(t,(function(t){t.parent&&t.parent.remove(t)}))})),E(l,(function(t){t.invisible=!0,t.dirty()}))}}},e.prototype._doAnimation=function(t,e,n,i){var r=n.get(\"animationDurationUpdate\"),o=n.get(\"animationEasing\"),a=(X(r)?0:r)||0,s=(X(o)?null:o)||\"cubicOut\",l=new lD;E(e.willDeleteEls,(function(t,e){E(t,(function(t,n){if(!t.invisible){var r,o=t.parent,u=gD(o);if(i&&\"drillDown\"===i.direction)r=o===i.rootNodeGroup?{shape:{x:0,y:0,width:u.nodeWidth,height:u.nodeHeight},style:{opacity:0}}:{style:{opacity:0}};else{var h=0,c=0;u.willDelete||(h=u.nodeWidth/2,c=u.nodeHeight/2),r=\"nodeGroup\"===e?{x:h,y:c,style:{opacity:0}}:{shape:{x:h,y:c,width:0,height:0},style:{opacity:0}}}r&&l.add(t,r,a,0,s)}}))})),E(this._storage,(function(t,n){E(t,(function(t,i){var r=e.lastsForAnimation[n][i],o={};r&&(t instanceof zr?null!=r.oldX&&(o.x=t.x,o.y=t.y,t.x=r.oldX,t.y=r.oldY):(r.oldShape&&(o.shape=A({},t.shape),t.setShape(r.oldShape)),r.fadein?(t.setStyle(\"opacity\",0),o.style={opacity:1}):1!==t.style.opacity&&(o.style={opacity:1})),l.add(t,o,a,0,s))}))}),this),this._state=\"animating\",l.finished(W((function(){this._state=\"ready\",e.renderFinally()}),this)).start()},e.prototype._resetController=function(t){var e=this._controller;e||((e=this._controller=new UI(t.getZr())).enable(this.seriesModel.get(\"roam\")),e.on(\"pan\",W(this._onPan,this)),e.on(\"zoom\",W(this._onZoom,this)));var n=new ze(0,0,t.getWidth(),t.getHeight());e.setPointerChecker((function(t,e,i){return n.contain(e,i)}))},e.prototype._clearController=function(){var t=this._controller;t&&(t.dispose(),t=null)},e.prototype._onPan=function(t){if(\"animating\"!==this._state&&(Math.abs(t.dx)>3||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;o<a;o++){var s=e[o].value;if(null!=s){if(s===t||U(s)&&s===t+\"\")return o;n&&c(s,o)}}for(o=0,a=e.length;o<a;o++){var l=e[o],u=l.interval,h=l.close;if(u){if(u[0]===-1/0){if(LD(h[1],t,u[1]))return o}else if(u[1]===1/0){if(LD(h[0],u[0],t))return o}else if(LD(h[0],u[0],t)&&LD(h[1],t,u[1]))return o;n&&c(u[0],o),n&&c(u[1],o)}}if(n)return t===1/0?e.length-1:t===-1/0?0:i;function c(e,n){var o=Math.abs(e-t);o<r&&(r=o,i=n)}},t.visualHandlers={color:{applyVisual:MD(\"color\"),getColorMapper:function(){var t=this.option;return W(\"category\"===t.mappingMethod?function(t,e){return!e&&(t=this._normalizeData(t)),ID.call(this,t)}:function(e,n,i){var r=!!i;return!n&&(e=this._normalizeData(e)),i=Jn(e,t.parsedVisual,i),r?i:ri(i,\"rgba\")},this)},_normalizedToVisual:{linear:function(t){return ri(Jn(t,this.option.parsedVisual),\"rgba\")},category:ID,piecewise:function(t,e){var n=DD.call(this,e);return null==n&&(n=ri(Jn(t,this.option.parsedVisual),\"rgba\")),n},fixed:TD}},colorHue:wD((function(t,e){return ni(t,e)})),colorSaturation:wD((function(t,e){return ni(t,null,e)})),colorLightness:wD((function(t,e){return ni(t,null,null,e)})),colorAlpha:wD((function(t,e){return ii(t,e)})),decal:{applyVisual:MD(\"decal\"),_normalizedToVisual:{linear:null,category:ID,piecewise:null,fixed:null}},opacity:{applyVisual:MD(\"opacity\"),_normalizedToVisual:CD([0,1])},liftZ:{applyVisual:MD(\"liftZ\"),_normalizedToVisual:{linear:TD,category:TD,piecewise:TD,fixed:TD}},symbol:{applyVisual:function(t,e,n){n(\"symbol\",this.mapValueToVisual(t))},_normalizedToVisual:{linear:SD,category:ID,piecewise:function(t,e){var n=DD.call(this,e);return null==n&&(n=SD.call(this,t)),n},fixed:TD}},symbolSize:{applyVisual:MD(\"symbolSize\"),_normalizedToVisual:CD([0,1])}},t}();function bD(t,e){var n=t.visual,i=[];q(n)?vD(n,(function(t){i.push(t)})):null!=n&&i.push(n);e||1!==i.length||{color:1,symbol:1}.hasOwnProperty(t.type)||(i[1]=i[0]),AD(t,i)}function wD(t){return{applyVisual:function(e,n,i){var r=this.mapValueToVisual(e);i(\"color\",t(n(\"color\"),r))},_normalizedToVisual:CD([0,1])}}function SD(t){var e=this.option.visual;return e[Math.round(Xr(t,[0,1],[0,e.length-1],!0))]||{}}function MD(t){return function(e,n,i){i(t,this.mapValueToVisual(e))}}function ID(t){var e=this.option.visual;return e[this.option.loop&&t!==xD?t%e.length:t]}function TD(){return this.option.visual[0]}function CD(t){return{linear:function(e){return Xr(e,t,this.option.visual,!0)},category:ID,piecewise:function(e,n){var i=DD.call(this,n);return null==i&&(i=Xr(e,t,this.option.visual,!0)),i},fixed:TD}}function DD(t){var e=this.option,n=e.pieceList;if(e.hasSpecialVisual){var i=n[_D.findPieceIndex(t,n)];if(i&&i.visual)return i.visual[this.type]}}function AD(t,e){return t.visual=e,\"color\"===t.type&&(t.parsedVisual=z(e,(function(t){var e=qn(t);return e||[0,0,0,1]}))),e}var kD={linear:function(t){return Xr(t,this.option.dataExtent,[0,1],!0)},piecewise:function(t){var e=this.option.pieceList,n=_D.findPieceIndex(t,e,!0);if(null!=n)return Xr(n,[0,e.length-1],[0,1],!0)},category:function(t){var e=this.option.categories?this.option.categoryMap[t]:t;return null==e?xD:e},fixed:bt};function LD(t,e,n){return t?e<=n:e<n}var PD=Oo(),OD={seriesType:\"treemap\",reset:function(t){var e=t.getData().tree.root;e.isRemoved()||RD(e,{},t.getViewRoot().getAncestors(),t)}};function RD(t,e,n,i){var r=t.getModel(),o=t.getLayout(),a=t.hostTree.data;if(o&&!o.invisible&&o.isInView){var s,l=r.getModel(\"itemStyle\"),u=function(t,e,n){var i=A({},e),r=n.designatedVisualItemStyle;return E([\"color\",\"colorAlpha\",\"colorSaturation\"],(function(n){r[n]=e[n];var o=t.get(n);r[n]=null,null!=o&&(i[n]=o)})),i}(l,e,i),h=a.ensureUniqueItemVisual(t.dataIndex,\"style\"),c=l.get(\"borderColor\"),p=l.get(\"borderColorSaturation\");null!=p&&(c=function(t,e){return null!=e?ni(e,null,null,t):null}(p,s=ND(u))),h.stroke=c;var d=t.viewChildren;if(d&&d.length){var f=function(t,e,n,i,r,o){if(!o||!o.length)return;var a=zD(e,\"color\")||null!=r.color&&\"none\"!==r.color&&(zD(e,\"colorAlpha\")||zD(e,\"colorSaturation\"));if(!a)return;var s=e.get(\"visualMin\"),l=e.get(\"visualMax\"),u=n.dataExtent.slice();null!=s&&s<u[0]&&(u[0]=s),null!=l&&l>u[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;p<d;p++)h+=c[p].getValue();var f=a.getValue();if(0===f)return s;u*=h/f;var g=o.getModel(),y=g.get(WD);(u+=4*y*y+(3*y+Math.max(y,$D(g)))*Math.pow(u,.5))>to&&(u=to),a=o}u<l&&(u=l);var v=Math.pow(u/l,.5);return[i*v,r*v]}(t,p,f,u,h):d?[d.width,d.height]:[u,h],v=a.sort;v&&\"asc\"!==v&&\"desc\"!==v&&(v=\"desc\");var m={squareRatio:a.squareRatio,sort:v,leafDepth:a.leafDepth};f.hostTree.clearLayouts();var x={x:0,y:0,width:y[0],height:y[1],area:y[0]*y[1]};f.setLayout(x),ZD(f,m,!1,0),x=f.getLayout(),GD(g,(function(t,e){var n=(g[e+1]||f).getValue();t.setLayout(A({dataExtent:[n,n],borderWidth:0,upperHeight:0},x))}))}var _=t.getData().tree.root;_.setLayout(function(t,e,n){if(e)return{x:e.x,y:e.y};var i={x:0,y:0};if(!n)return i;var r=n.node,o=r.getLayout();if(!o)return i;var a=[o.width/2,o.height/2],s=r;for(;s;){var l=s.getLayout();a[0]+=l.x,a[1]+=l.y,s=s.parentNode}return{x:t.width/2-a[0],y:t.height/2-a[1]}}(s,d,p),!0),t.setLayoutInfo(s),KD(_,new ze(-s.x,-s.y,r,o),g,f,0)}};function ZD(t,e,n,i){var r,o;if(!t.isRemoved()){var a=t.getLayout();r=a.width,o=a.height;var s=t.getModel(),l=s.get(WD),u=s.get(HD)/2,h=$D(s),c=Math.max(l,h),p=l-u,d=c-u;t.setLayout({borderWidth:l,upperHeight:c,upperLabelHeight:h},!0);var f=(r=VD(r-2*p,0))*(o=VD(o-p-d,0)),g=function(t,e,n,i,r,o){var a=t.children||[],s=i.sort;\"asc\"!==s&&\"desc\"!==s&&(s=null);var l=null!=i.leafDepth&&i.leafDepth<=o;if(r&&!l)return t.viewChildren=[];a=B(a,(function(t){return!t.isRemoved()})),function(t,e){e&&t.sort((function(t,n){var i=\"asc\"===e?t.getValue()-n.getValue():n.getValue()-t.getValue();return 0===i?\"asc\"===e?t.dataIndex-n.dataIndex:n.dataIndex-t.dataIndex:i}))}(a,s);var u=function(t,e,n){for(var i=0,r=0,o=e.length;r<o;r++)i+=e[r].getValue();var a,s=t.get(\"visualDimension\");e&&e.length?\"value\"===s&&n?(a=[e[e.length-1].getValue(),e[0].getValue()],\"asc\"===n&&a.reverse()):(a=[1/0,-1/0],GD(e,(function(t){var e=t.getValue(s);e<a[0]&&(a[0]=e),e>a[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*e<o&&(s=l,n-=u)}return\"asc\"===i?r.splice(0,a-s):r.splice(s,a-s),n}(e,n,u.sum,s,a),0===u.sum)return t.viewChildren=[];for(var h=0,c=a.length;h<c;h++){var p=a[h].getValue()/u.sum*n;a[h].setLayout({area:p})}l&&(a.length&&t.setLayout({isLeafRoot:!0},!0),a.length=0);return t.viewChildren=a,t.setLayout({dataExtent:u.dataExtent},!0),a}(t,s,f,e,n,i);if(g.length){var y={x:p,y:d,width:r,height:o},v=BD(r,o),m=1/0,x=[];x.area=0;for(var _=0,b=g.length;_<b;){var w=g[_];x.push(w),x.area+=w.getLayout().area;var S=jD(x,v,e.squareRatio);S<=m?(_++,m=S):(x.area-=x.pop().getLayout().area,qD(x,v,y,u,!1),v=BD(y.width,y.height),x.length=x.area=0,m=1/0)}if(x.length&&qD(x,v,y,u,!0),!n){var M=s.get(\"childrenVisibleMin\");null!=M&&f<M&&(n=!0)}for(_=0,b=g.length;_<b;_++)ZD(g[_],e,n,i+1)}}}function jD(t,e,n){for(var i=0,r=1/0,o=0,a=void 0,s=t.length;o<s;o++)(a=t[o].getLayout().area)&&(a<r&&(r=a),a>i&&(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;c<p;c++){var d=t[c],f={},g=h?d.getLayout().area/h:0,y=f[l[a]]=VD(h-2*i,0),v=n[s[o]]+n[l[o]]-u,m=c===p-1||v<g?v:g,x=f[l[o]]=VD(m-2*i,0);f[s[a]]=n[s[a]]+BD(i,y/2),f[s[o]]=u+BD(i,x/2),u+=m,d.setLayout(f,!0)}n[s[a]]+=h,n[l[a]]-=h}function KD(t,e,n,i,r){var o=t.getLayout(),a=n[r],s=a&&a===t;if(!(a&&!s||r===n.length&&t!==i)){t.setLayout({isInView:!0,invisible:!s&&!e.intersect(o),isAboveViewRoot:s},!0);var l=new ze(e.x-o.x,e.y-o.y,e.width,e.height);GD(t.viewChildren||[],(function(t){KD(t,l,n,i,r+1)}))}}function $D(t){return t.get(YD)?t.get(XD):0}function JD(t){var e=t.findComponents({mainType:\"legend\"});e&&e.length&&t.eachSeriesByType(\"graph\",(function(t){var n=t.getCategoriesData(),i=t.getGraph().data,r=n.mapArray(n.getName);i.filterSelf((function(t){var n=i.getItemModel(t).getShallow(\"category\");if(null!=n){j(n)&&(n=r[n]);for(var o=0;o<e.length;o++)if(!e[o].isSelected(n))return!1}return!0}))}))}function QD(t){var e={};t.eachSeriesByType(\"graph\",(function(t){var n=t.getCategoriesData(),i=t.getData(),r={};n.each((function(i){var o=n.getName(i);r[\"ec-\"+o]=i;var a=n.getItemModel(i),s=a.getModel(\"itemStyle\").getItemStyle();s.fill||(s.fill=t.getColorFromPalette(o,e)),n.setItemVisual(i,\"style\",s);for(var l=[\"symbol\",\"symbolSize\",\"symbolKeepAspect\"],u=0;u<l.length;u++){var h=a.getShallow(l[u],!0);null!=h&&n.setItemVisual(i,l[u],h)}})),n.count()&&i.each((function(t){var e=i.getItemModel(t).getShallow(\"category\");if(null!=e){U(e)&&(e=r[\"ec-\"+e]);var o=n.getItemVisual(e,\"style\");A(i.ensureUniqueItemVisual(t,\"style\"),o);for(var a=[\"symbol\",\"symbolSize\",\"symbolKeepAspect\"],s=0;s<a.length;s++)i.setItemVisual(t,a[s],n.getItemVisual(e,a[s]))}}))}))}function tA(t){return t instanceof Array||(t=[t,t]),t}function eA(t){t.eachSeriesByType(\"graph\",(function(t){var e=t.getGraph(),n=t.getEdgeData(),i=tA(t.get(\"edgeSymbol\")),r=tA(t.get(\"edgeSymbolSize\"));n.setVisual(\"fromSymbol\",i&&i[0]),n.setVisual(\"toSymbol\",i&&i[1]),n.setVisual(\"fromSymbolSize\",r&&r[0]),n.setVisual(\"toSymbolSize\",r&&r[1]),n.setVisual(\"style\",t.getModel(\"lineStyle\").getLineStyle()),n.each((function(t){var i=n.getItemModel(t),r=e.getEdgeByIndex(t),o=tA(i.getShallow(\"symbol\",!0)),a=tA(i.getShallow(\"symbolSize\",!0)),s=i.getModel(\"lineStyle\").getLineStyle(),l=n.ensureUniqueItemVisual(t,\"style\");switch(A(l,s),l.stroke){case\"source\":var u=r.node1.getVisual(\"style\");l.stroke=u&&u.fill;break;case\"target\":u=r.node2.getVisual(\"style\");l.stroke=u&&u.fill}o[0]&&r.setVisual(\"fromSymbol\",o[0]),o[1]&&r.setVisual(\"toSymbol\",o[1]),a[0]&&r.setVisual(\"fromSymbolSize\",a[0]),a[1]&&r.setVisual(\"toSymbolSize\",a[1])}))}))}var nA=\"--\\x3e\",iA=function(t){return t.get(\"autoCurveness\")||null},rA=function(t,e){var n=iA(t),i=20,r=[];if(j(n))i=n;else if(Y(n))return void(t.__curvenessList=n);e>i&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a<o;a++)r.push((a%2?a+1:a)/10*(a%2?-1:1));t.__curvenessList=r},oA=function(t,e,n){var i=[t.id,t.dataIndex].join(\".\"),r=[e.id,e.dataIndex].join(\".\");return[n.uid,i,r].join(nA)},aA=function(t){var e=t.split(nA);return[e[0],e[2],e[1]].join(nA)},sA=function(t,e){var n=e.__edgeMap;return n[t]?n[t].length:0};function lA(t,e,n,i){var r=iA(e),o=Y(r);if(!r)return null;var a=function(t,e){var n=oA(t.node1,t.node2,e);return e.__edgeMap[n]}(t,e);if(!a)return null;for(var s=-1,l=0;l<a.length;l++)if(a[l]===n){s=l;break}var u=function(t,e){return sA(oA(t.node1,t.node2,e),e)+sA(oA(t.node2,t.node1,e),e)}(t,e);rA(e,u),t.lineStyle=t.lineStyle||{};var h=oA(t.node1,t.node2,e),c=e.__curvenessList,p=o||u%2?0:1;if(a.isForward)return c[p+s];var d=aA(h),f=sA(d,e),g=c[s+f+p];return i?o?r&&0===r[0]?(f+p)%2?g:-g:((f%2?0:1)+p)%2?g:-g:(f+p)%2?g:-g:c[s+f+p]}function uA(t){var e=t.coordinateSystem;if(!e||\"view\"===e.type){var n=t.getGraph();n.eachNode((function(t){var e=t.getModel();t.setLayout([+e.get(\"x\"),+e.get(\"y\")])})),hA(n,t)}}function hA(t,e){t.eachEdge((function(t,n){var i=ot(t.getModel().get([\"lineStyle\",\"curveness\"]),-lA(t,e,n,!0),0),r=Tt(t.node1.getLayout()),o=Tt(t.node2.getLayout()),a=[r,o];+i&&a.push([(r[0]+o[0])/2-(r[1]-o[1])*i,(r[1]+o[1])/2-(o[0]-r[0])*i]),t.setLayout(a)}))}function cA(t,e){t.eachSeriesByType(\"graph\",(function(t){var e=t.get(\"layout\"),n=t.coordinateSystem;if(n&&\"view\"!==n.type){var i=t.getData(),r=[];E(n.dimensions,(function(t){r=r.concat(i.mapDimensionsAll(t))}));for(var o=0;o<i.count();o++){for(var a=[],s=!1,l=0;l<r.length;l++){var u=i.get(r[l],o);isNaN(u)||(s=!0),a.push(u)}s?i.setItemLayout(o,n.dataToPoint(a)):i.setItemLayout(o,[NaN,NaN])}hA(i.graph,t)}else e&&\"none\"!==e||uA(t)}))}function pA(t){var e=t.coordinateSystem;if(\"view\"!==e.type)return 1;var n=t.option.nodeScaleRatio,i=e.scaleX;return((e.getZoom()-1)*n+1)/i}function dA(t){var e=t.getVisual(\"symbolSize\");return e instanceof Array&&(e=(e[0]+e[1])/2),+e}var fA=Math.PI,gA=[];function yA(t,e,n,i){var r=t.coordinateSystem;if(!r||\"view\"===r.type){var o=r.getBoundingRect(),a=t.getData(),s=a.graph,l=o.width/2+o.x,u=o.height/2+o.y,h=Math.min(o.width,o.height)/2,c=a.count();if(a.setLayout({cx:l,cy:u}),c){if(n){var p=r.pointToData(i),d=p[0],f=p[1],g=[d-l,f-u];Et(g,g),Nt(g,g,h),n.setLayout([l+g[0],u+g[1]],!0),mA(n,t.get([\"circular\",\"rotateLabel\"]),l,u)}vA[e](t,s,a,h,l,u,c),s.eachEdge((function(e,n){var i,r=ot(e.getModel().get([\"lineStyle\",\"curveness\"]),lA(e,t,n),0),o=Tt(e.node1.getLayout()),a=Tt(e.node2.getLayout()),s=(o[0]+a[0])/2,h=(o[1]+a[1])/2;+r&&(i=[l*(r*=3)+s*(1-r),u*r+h*(1-r)]),e.setLayout([o,a,i])}))}}}var vA={value:function(t,e,n,i,r,o,a){var s=0,l=n.getSum(\"value\"),u=2*Math.PI/(l||a);e.eachNode((function(t){var e=t.getValue(\"value\"),n=u*(l?e:1)/2;s+=n,t.setLayout([i*Math.cos(s)+r,i*Math.sin(s)+o]),s+=n}))},symbolSize:function(t,e,n,i,r,o,a){var s=0;gA.length=a;var l=pA(t);e.eachNode((function(t){var e=dA(t);isNaN(e)&&(e=2),e<0&&(e=0),e*=l;var n=Math.asin(e/2/i);isNaN(n)&&(n=fA/2),gA[t.dataIndex]=n,s+=2*n}));var u=(2*fA-s)/a/2,h=0;e.eachNode((function(t){var e=u+gA[t.dataIndex];h+=e,(!t.getLayout()||!t.getLayout().fixed)&&t.setLayout([i*Math.cos(h)+r,i*Math.sin(h)+o]),h+=e}))}};function mA(t,e,n,i){var r=t.getGraphicEl();if(r){var o=t.getModel().get([\"label\",\"rotate\"])||0,a=r.getSymbolPath();if(e){var s=t.getLayout(),l=Math.atan2(s[1]-i,s[0]-n);l<0&&(l=2*Math.PI+l);var u=s[0]<n;u&&(l-=Math.PI);var h=u?\"left\":\"right\";a.setTextConfig({rotation:-l,position:h,origin:\"center\"});var c=a.ensureState(\"emphasis\");A(c.textConfig||(c.textConfig={}),{position:h})}else a.setTextConfig({rotation:o*=Math.PI/180})}}function xA(t){t.eachSeriesByType(\"graph\",(function(t){\"circular\"===t.get(\"layout\")&&yA(t,\"symbolSize\")}))}var _A=At;function bA(t){t.eachSeriesByType(\"graph\",(function(t){var e=t.coordinateSystem;if(!e||\"view\"===e.type)if(\"force\"===t.get(\"layout\")){var n=t.preservedPoints||{},i=t.getGraph(),r=i.data,o=i.edgeData,a=t.getModel(\"force\"),s=a.get(\"initLayout\");t.preservedPoints?r.each((function(t){var e=r.getId(t);r.setItemLayout(t,n[e]||[NaN,NaN])})):s&&\"none\"!==s?\"circular\"===s&&yA(t,\"value\"):uA(t);var l=r.getDataExtent(\"value\"),u=o.getDataExtent(\"value\"),h=a.get(\"repulsion\"),c=a.get(\"edgeLength\"),p=Y(h)?h:[h,h],d=Y(c)?c:[c,c];d=[d[1],d[0]];var f=r.mapArray(\"value\",(function(t,e){var n=r.getItemLayout(e),i=Xr(t,l,p);return isNaN(i)&&(i=(p[0]+p[1])/2),{w:i,rep:i,fixed:r.getItemModel(e).get(\"fixed\"),p:!n||isNaN(n[0])||isNaN(n[1])?null:n}})),g=o.mapArray(\"value\",(function(e,n){var r=i.getEdgeByIndex(n),o=Xr(e,u,d);isNaN(o)&&(o=(d[0]+d[1])/2);var a=r.getModel(),s=ot(r.getModel().get([\"lineStyle\",\"curveness\"]),-lA(r,t,n,!0),0);return{n1:f[r.node1.dataIndex],n2:f[r.node2.dataIndex],d:o,curveness:s,ignoreForceLayout:a.get(\"ignoreForceLayout\")}})),y=e.getBoundingRect(),v=function(t,e,n){for(var i=t,r=e,o=n.rect,a=o.width,s=o.height,l=[o.x+a/2,o.y+s/2],u=null==n.gravity?.1:n.gravity,h=0;h<i.length;h++){var c=i[h];c.p||(c.p=Mt(a*(Math.random()-.5)+l[0],s*(Math.random()-.5)+l[1])),c.pp=Tt(c.p),c.edges=null}var p,d,f=null==n.friction?.6:n.friction,g=f;return{warmUp:function(){g=.8*f},setFixed:function(t){i[t].fixed=!0},setUnfixed:function(t){i[t].fixed=!1},beforeStep:function(t){p=t},afterStep:function(t){d=t},step:function(t){p&&p(i,r);for(var e=[],n=i.length,o=0;o<r.length;o++){var a=r[o];if(!a.ignoreForceLayout){var s=a.n1;kt(e,(y=a.n2).p,s.p);var h=Lt(e)-a.d,c=y.w/(s.w+y.w);isNaN(c)&&(c=0),Et(e,e),!s.fixed&&_A(s.p,s.p,e,c*h*g),!y.fixed&&_A(y.p,y.p,e,-(1-c)*h*g)}}for(o=0;o<n;o++)(x=i[o]).fixed||(kt(e,l,x.p),_A(x.p,x.p,e,u*g));for(o=0;o<n;o++){s=i[o];for(var f=o+1;f<n;f++){var y;kt(e,(y=i[f]).p,s.p),0===(h=Lt(e))&&(Ct(e,Math.random()-.5,Math.random()-.5),h=1);var v=(s.rep+y.rep)/h/h;!s.fixed&&_A(s.pp,s.pp,e,v),!y.fixed&&_A(y.pp,y.pp,e,-v)}}var m=[];for(o=0;o<n;o++){var x;(x=i[o]).fixed||(kt(m,x.p,x.pp),_A(x.p,x.p,m,g),It(x.pp,x.p))}var _=(g*=.992)<.01;d&&d(i,r,_),t&&t(_)}}}(f,g,{rect:y,gravity:a.get(\"gravity\"),friction:a.get(\"friction\")});v.beforeStep((function(t,e){for(var n=0,r=t.length;n<r;n++)t[n].fixed&&It(t[n].p,i.getNodeByIndex(n).getLayout())})),v.afterStep((function(t,e,o){for(var a=0,s=t.length;a<s;a++)t[a].fixed||i.getNodeByIndex(a).setLayout(t[a].p),n[r.getId(a)]=t[a].p;for(a=0,s=e.length;a<s;a++){var l=e[a],u=i.getEdgeByIndex(a),h=l.n1.p,c=l.n2.p,p=u.getLayout();(p=p?p.slice():[])[0]=p[0]||[],p[1]=p[1]||[],It(p[0],h),It(p[1],c),+l.curveness&&(p[2]=[(h[0]+c[0])/2-(h[1]-c[1])*l.curveness,(h[1]+c[1])/2-(c[0]-h[0])*l.curveness]),u.setLayout(p)}})),t.forceLayout=v,t.preservedPoints=n,v.step()}else t.forceLayout=null}))}function wA(t,e){var n=[];return t.eachSeriesByType(\"graph\",(function(t){var i=t.get(\"coordinateSystem\");if(!i||\"view\"===i){var r=t.getData(),o=[],a=[];Ra(r.mapArray((function(t){var e=r.getItemModel(t);return[+e.get(\"x\"),+e.get(\"y\")]})),o,a),a[0]-o[0]==0&&(a[0]+=1,o[0]-=1),a[1]-o[1]==0&&(a[1]+=1,o[1]-=1);var s=(a[0]-o[0])/(a[1]-o[1]),l=function(t,e,n){return Cp(A(t.getBoxLayoutParams(),{aspect:n}),{width:e.getWidth(),height:e.getHeight()})}(t,e,s);isNaN(s)&&(o=[l.x,l.y],a=[l.x+l.width,l.y+l.height]);var u=a[0]-o[0],h=a[1]-o[1],c=l.width,p=l.height,d=t.coordinateSystem=new iC;d.zoomLimit=t.get(\"scaleLimit\"),d.setBoundingRect(o[0],o[1],u,h),d.setViewRect(l.x,l.y,c,p),d.setCenter(t.get(\"center\"),e),d.setZoom(t.get(\"zoom\")),n.push(d)}})),n}var SA=Zu.prototype,MA=$u.prototype,IA=function(){this.x1=0,this.y1=0,this.x2=0,this.y2=0,this.percent=1};!function(t){function e(){return null!==t&&t.apply(this,arguments)||this}n(e,t)}(IA);function TA(t){return isNaN(+t.cpx1)||isNaN(+t.cpy1)}var CA=function(t){function e(e){var n=t.call(this,e)||this;return n.type=\"ec-line\",n}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:\"#000\",fill:null}},e.prototype.getDefaultShape=function(){return new IA},e.prototype.buildPath=function(t,e){TA(e)?SA.buildPath.call(this,t,e):MA.buildPath.call(this,t,e)},e.prototype.pointAt=function(t){return TA(this.shape)?SA.pointAt.call(this,t):MA.pointAt.call(this,t)},e.prototype.tangentAt=function(t){var e=this.shape,n=TA(e)?[e.x2-e.x1,e.y2-e.y1]:MA.tangentAt.call(this,t);return Et(n,n)},e}(Is),DA=[\"fromSymbol\",\"toSymbol\"];function AA(t){return\"_\"+t+\"Type\"}function kA(t,e,n){var i=e.getItemVisual(n,t);if(!i||\"none\"===i)return i;var r=e.getItemVisual(n,t+\"Size\"),o=e.getItemVisual(n,t+\"Rotate\"),a=e.getItemVisual(n,t+\"Offset\"),s=e.getItemVisual(n,t+\"KeepAspect\"),l=Hy(r);return i+l+Yy(a||0,l)+(o||\"\")+(s||\"\")}function LA(t,e,n){var i=e.getItemVisual(n,t);if(i&&\"none\"!==i){var r=e.getItemVisual(n,t+\"Size\"),o=e.getItemVisual(n,t+\"Rotate\"),a=e.getItemVisual(n,t+\"Offset\"),s=e.getItemVisual(n,t+\"KeepAspect\"),l=Hy(r),u=Yy(a||0,l),h=Wy(i,-l[0]/2+u[0],-l[1]/2+u[1],l[0],l[1],null,s);return h.__specifiedRotation=null==o||isNaN(o)?void 0:+o*Math.PI/180||0,h.name=t,h}}function PA(t,e){t.x1=e[0][0],t.y1=e[0][1],t.x2=e[1][0],t.y2=e[1][1],t.percent=1;var n=e[2];n?(t.cpx1=n[0],t.cpy1=n[1]):(t.cpx1=NaN,t.cpy1=NaN)}var OA=function(t){function e(e,n,i){var r=t.call(this)||this;return r._createLine(e,n,i),r}return n(e,t),e.prototype._createLine=function(t,e,n){var i=t.hostModel,r=function(t){var e=new CA({name:\"line\",subPixelOptimize:!0});return PA(e.shape,t),e}(t.getItemLayout(e));r.shape.percent=0,gh(r,{shape:{percent:1}},i,e),this.add(r),E(DA,(function(n){var i=LA(n,t,e);this.add(i),this[AA(n)]=kA(n,t,e)}),this),this._updateCommonStl(t,e,n)},e.prototype.updateData=function(t,e,n){var i=t.hostModel,r=this.childOfName(\"line\"),o=t.getItemLayout(e),a={shape:{}};PA(a.shape,o),fh(r,a,i,e),E(DA,(function(n){var i=kA(n,t,e),r=AA(n);if(this[r]!==i){this.remove(this.childOfName(n));var o=LA(n,t,e);this.add(o)}this[r]=i}),this),this._updateCommonStl(t,e,n)},e.prototype.getLinePath=function(){return this.childAt(0)},e.prototype._updateCommonStl=function(t,e,n){var i=t.hostModel,r=this.childOfName(\"line\"),o=n&&n.emphasisLineStyle,a=n&&n.blurLineStyle,s=n&&n.selectLineStyle,l=n&&n.labelStatesModels,u=n&&n.emphasisDisabled,h=n&&n.focus,c=n&&n.blurScope;if(!n||t.hasItemOption){var p=t.getItemModel(e),d=p.getModel(\"emphasis\");o=d.getModel(\"lineStyle\").getLineStyle(),a=p.getModel([\"blur\",\"lineStyle\"]).getLineStyle(),s=p.getModel([\"select\",\"lineStyle\"]).getLineStyle(),u=d.get(\"disabled\"),h=d.get(\"focus\"),c=d.get(\"blurScope\"),l=ec(p)}var f=t.getItemVisual(e,\"style\"),g=f.stroke;r.useStyle(f),r.style.fill=null,r.style.strokeNoScale=!0,r.ensureState(\"emphasis\").style=o,r.ensureState(\"blur\").style=a,r.ensureState(\"select\").style=s,E(DA,(function(t){var e=this.childOfName(t);if(e){e.setColor(g),e.style.opacity=f.opacity;for(var n=0;n<ol.length;n++){var i=ol[n],o=r.getState(i);if(o){var a=o.style||{},s=e.ensureState(i),l=s.style||(s.style={});null!=a.stroke&&(l[e.__isEmptyBrush?\"stroke\":\"fill\"]=a.stroke),null!=a.opacity&&(l.opacity=a.opacity)}}e.markRedraw()}}),this);var y=i.getRawValue(e);tc(this,l,{labelDataIndex:e,labelFetcher:{getFormattedLabel:function(e,n){return i.getFormattedLabel(e,n,t.dataType)}},inheritColor:g||\"#000\",defaultOpacity:f.opacity,defaultText:(null==y?t.getName(e):isFinite(y)?Zr(y):y)+\"\"});var v=this.getTextContent();if(v){var m=l.normal;v.__align=v.style.align,v.__verticalAlign=v.style.verticalAlign,v.__position=m.get(\"position\")||\"middle\";var x=m.get(\"distance\");Y(x)||(x=[x,x]),v.__labelDistance=x}this.setTextConfig({position:null,local:!0,inside:!1}),Yl(this,h,c,u)},e.prototype.highlight=function(){kl(this)},e.prototype.downplay=function(){Ll(this)},e.prototype.updateLayout=function(t,e){this.setLinePoints(t.getItemLayout(e))},e.prototype.setLinePoints=function(t){var e=this.childOfName(\"line\");PA(e.shape,t),e.dirty()},e.prototype.beforeUpdate=function(){var t=this,e=t.childOfName(\"fromSymbol\"),n=t.childOfName(\"toSymbol\"),i=t.getTextContent();if(e||n||i&&!i.ignore){for(var r=1,o=this.parent;o;)o.scaleX&&(r/=o.scaleX),o=o.parent;var a=t.childOfName(\"line\");if(this.__dirty||a.__dirty){var s=a.shape.percent,l=a.pointAt(0),u=a.pointAt(s),h=kt([],u,l);if(Et(h,h),e&&(e.setPosition(l),S(e,0),e.scaleX=e.scaleY=r*s,e.markRedraw()),n&&(n.setPosition(u),S(n,1),n.scaleX=n.scaleY=r*s,n.markRedraw()),i&&!i.ignore){i.x=i.y=0,i.originX=i.originY=0;var c=void 0,p=void 0,d=i.__labelDistance,f=d[0]*r,g=d[1]*r,y=s/2,v=a.tangentAt(y),m=[v[1],-v[0]],x=a.pointAt(y);m[1]>0&&(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]<l[0]&&(b=Math.PI+b),i.rotation=b}var w=void 0;switch(i.__position){case\"insideStartTop\":case\"insideMiddleTop\":case\"insideEndTop\":case\"middle\":w=-g,p=\"bottom\";break;case\"insideStartBottom\":case\"insideMiddleBottom\":case\"insideEndBottom\":w=g,p=\"top\";break;default:w=0,p=\"middle\"}switch(i.__position){case\"end\":i.x=h[0]*f+u[0],i.y=h[1]*g+u[1],c=h[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<t.end;i++){if(zA(e.getItemLayout(i))){var r=new this._LineCtor(e,i,this._seriesScope);r.traverse(n),this.group.add(r),e.setItemGraphicEl(i,r),this._progressiveEls.push(r)}}},t.prototype.remove=function(){this.group.removeAll()},t.prototype.eachRendered=function(t){qh(this._progressiveEls||this.group,t)},t.prototype._doAdd=function(t,e,n){if(zA(t.getItemLayout(e))){var i=new this._LineCtor(t,e,n);t.setItemGraphicEl(e,i),this.group.add(i)}},t.prototype._doUpdate=function(t,e,n,i,r){var o=t.getItemGraphicEl(n);zA(e.getItemLayout(i))?(o?o.updateData(e,i,r):o=new this._LineCtor(e,i,r),e.setItemGraphicEl(i,o),this.group.add(o)):this.group.remove(o)},t}();function NA(t){var e=t.hostModel,n=e.getModel(\"emphasis\");return{lineStyle:e.getModel(\"lineStyle\").getLineStyle(),emphasisLineStyle:n.getModel([\"lineStyle\"]).getLineStyle(),blurLineStyle:e.getModel([\"blur\",\"lineStyle\"]).getLineStyle(),selectLineStyle:e.getModel([\"select\",\"lineStyle\"]).getLineStyle(),emphasisDisabled:n.get(\"disabled\"),blurScope:n.get(\"blurScope\"),focus:n.get(\"focus\"),labelStatesModels:ec(e)}}function EA(t){return isNaN(t[0])||isNaN(t[1])}function zA(t){return t&&!EA(t[0])&&!EA(t[1])}var VA=[],BA=[],FA=[],GA=In,WA=Ft,HA=Math.abs;function YA(t,e,n){for(var i,r=t[0],o=t[1],a=t[2],s=1/0,l=n*n,u=.1,h=.1;h<=.9;h+=.1){VA[0]=GA(r[0],o[0],a[0],h),VA[1]=GA(r[1],o[1],a[1],h),(d=HA(WA(VA,e)-l))<s&&(s=d,i=h)}for(var c=0;c<32;c++){var p=i+u;BA[0]=GA(r[0],o[0],a[0],i),BA[1]=GA(r[1],o[1],a[1],i),FA[0]=GA(r[0],o[0],a[0],p),FA[1]=GA(r[1],o[1],a[1],p);var d=WA(BA,e)-l;if(HA(d)<.01)break;var f=WA(FA,e)-l;u/=2,d<0?f>=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<i;r++)n[r].dataIndex>=0&&t.call(e,n[r],r)},t.prototype.eachEdge=function(t,e){for(var n=this.edges,i=n.length,r=0;r<i;r++)n[r].dataIndex>=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<this.nodes.length;o++)this.nodes[o].__visited=!1;if(!t.call(i,e,null))for(var a=[e];a.length;){var s=a.shift(),l=s[r];for(o=0;o<l.length;o++){var u=l[o],h=u.node1===s?u.node2:u.node1;if(!h.__visited){if(t.call(i,h,s))return;a.push(h),h.__visited=!0}}}}},t.prototype.update=function(){for(var t=this.data,e=this.edgeData,n=this.nodes,i=this.edges,r=0,o=n.length;r<o;r++)n[r].dataIndex=-1;for(r=0,o=t.count();r<o;r++)n[t.getRawIndex(r)].dataIndex=r;e.filterSelf((function(t){var n=i[e.getRawIndex(t)];return n.node1.dataIndex>=0&&n.node2.dataIndex>=0}));for(r=0,o=i.length;r<o;r++)i[r].dataIndex=-1;for(r=0,o=e.count();r<o;r++)i[e.getRawIndex(r)].dataIndex=r},t.prototype.clone=function(){for(var e=new t(this._directed),n=this.nodes,i=this.edges,r=0;r<n.length;r++)e.addNode(n[r].id,n[r].dataIndex);for(r=0;r<i.length;r++){var o=i[r];e.addEdge(o.node1.id,o.node2.id,o.dataIndex)}return e},t}(),KA=function(){function t(t,e){this.inEdges=[],this.outEdges=[],this.edges=[],this.dataIndex=-1,this.id=null==t?\"\":t,this.dataIndex=null==e?-1:e}return t.prototype.degree=function(){return this.edges.length},t.prototype.inDegree=function(){return this.inEdges.length},t.prototype.outDegree=function(){return this.outEdges.length},t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostGraph.data.getItemModel(this.dataIndex).getModel(t)},t.prototype.getAdjacentDataIndices=function(){for(var t={edge:[],node:[]},e=0;e<this.edges.length;e++){var n=this.edges[e];n.dataIndex<0||(t.edge.push(n.dataIndex),t.node.push(n.node1.dataIndex,n.node2.dataIndex))}return t},t.prototype.getTrajectoryDataIndices=function(){for(var t=yt(),e=yt(),n=0;n<this.edges.length;n++){var i=this.edges[n];if(!(i.dataIndex<0)){t.set(i.dataIndex,!0);for(var r=[i.node1],o=[i.node2],a=0;a<r.length;){var s=r[a];a++,e.set(s.dataIndex,!0);for(var l=0;l<s.inEdges.length;l++)t.set(s.inEdges[l].dataIndex,!0),r.push(s.inEdges[l].node1)}for(a=0;a<o.length;){var u=o[a];a++,e.set(u.dataIndex,!0);for(l=0;l<u.outEdges.length;l++)t.set(u.outEdges[l].dataIndex,!0),o.push(u.outEdges[l].node2)}}}return{edge:t.keys(),node:e.keys()}},t}(),$A=function(){function t(t,e,n){this.dataIndex=-1,this.node1=t,this.node2=e,this.dataIndex=null==n?-1:n}return t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostGraph.edgeData.getItemModel(this.dataIndex).getModel(t)},t.prototype.getAdjacentDataIndices=function(){return{edge:[this.dataIndex],node:[this.node1.dataIndex,this.node2.dataIndex]}},t.prototype.getTrajectoryDataIndices=function(){var t=yt(),e=yt();t.set(this.dataIndex,!0);for(var n=[this.node1],i=[this.node2],r=0;r<n.length;){var o=n[r];r++,e.set(o.dataIndex,!0);for(var a=0;a<o.inEdges.length;a++)t.set(o.inEdges[a].dataIndex,!0),n.push(o.inEdges[a].node1)}for(r=0;r<i.length;){var s=i[r];r++,e.set(s.dataIndex,!0);for(a=0;a<s.outEdges.length;a++)t.set(s.outEdges[a].dataIndex,!0),i.push(s.outEdges[a].node2)}return{edge:t.keys(),node:e.keys()}},t}();function JA(t,e){return{getValue:function(n){var i=this[t][e];return i.getStore().get(i.getDimensionIndex(n||\"value\"),this.dataIndex)},setVisual:function(n,i){this.dataIndex>=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<t.length;a++)o.addNode(it(t[a].id,t[a].name,a),a);var s=[],l=[],u=0;for(a=0;a<e.length;a++){var h=e[a],c=h.source,p=h.target;o.addEdge(c,p,u)&&(l.push(h),s.push(it(Ao(h.id,null),c+\" > \"+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<i.length;m++){var x=new h({shape:{startAngle:y,endAngle:l=s+g*Math.min(Math.max(i[m][0],0),1),cx:r.cx,cy:r.cy,clockwise:a,r0:r.r-d,r:r.r},silent:!0});x.setStyle({fill:i[m][1]}),x.setStyle(p.getLineStyle([\"color\",\"width\"])),v.push(x),y=l}v.reverse(),E(v,(function(t){return o.add(t)}));var _=function(t){if(t<=0)return i[0][1];var e;for(e=0;e<i.length;e++)if(i[e][0]>=t&&(0===e?0:i[e-1][0])<t)return i[e][1];return i[e-1][1]};this._renderTicks(t,e,n,_,r,s,l,a,d),this._renderTitleAndDetail(t,e,n,_,r),this._renderAnchor(t,r),this._renderPointer(t,e,n,_,r,s,l,a,d)},e.prototype._renderTicks=function(t,e,n,i,r,o,a,s,l){for(var u,h,c=this.group,p=r.cx,d=r.cy,f=r.r,g=+t.get(\"min\"),y=+t.get(\"max\"),v=t.getModel(\"splitLine\"),m=t.getModel(\"axisTick\"),x=t.getModel(\"axisLabel\"),_=t.get(\"splitNumber\"),b=m.get(\"splitNumber\"),w=Ur(v.get(\"length\"),f),S=Ur(m.get(\"length\"),f),M=o,I=(a-o)/_,T=I/b,C=v.getModel(\"lineStyle\").getLineStyle(),D=m.getModel(\"lineStyle\").getLineStyle(),A=v.get(\"distance\"),k=0;k<=_;k++){if(u=Math.cos(M),h=Math.sin(M),v.get(\"show\")){var L=new Zu({shape:{x1:u*(f-(P=A?A+l:l))+p,y1:h*(f-P)+d,x2:u*(f-w-P)+p,y2:h*(f-w-P)+d},style:C,silent:!0});\"auto\"===C.stroke&&L.setStyle({stroke:i(k/_)}),c.add(L)}if(x.get(\"show\")){var P=x.get(\"distance\")+A,O=rk(Zr(k/_*(y-g)+g),x.get(\"formatter\")),R=i(k/_),N=u*(f-w-P)+p,E=h*(f-w-P)+d,z=x.get(\"rotate\"),V=0;\"radial\"===z?(V=-M+2*Math.PI)>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();a<s;a++)r[a]=a;return X(e)?r.sort(e):\"none\"!==e&&r.sort((function(t,e){return o?i[t]-i[e]:i[e]-i[t]})),r}(n,r),h=o.x,c=o.y,p=\"horizontal\"===a?[Ur(t.get(\"minSize\"),l),Ur(t.get(\"maxSize\"),l)]:[Ur(t.get(\"minSize\"),s),Ur(t.get(\"maxSize\"),s)],d=n.getDataExtent(i),f=t.get(\"min\"),g=t.get(\"max\");null==f&&(f=Math.min(d[0],0)),null==g&&(g=d[1]);var y=t.get(\"funnelAlign\"),v=t.get(\"gap\"),m=((\"horizontal\"===a?s:l)-v*(n.count()-1))/n.count(),x=function(t,e){if(\"horizontal\"===a){var r=Xr(n.get(i,t)||0,[f,g],p,!0),o=void 0;switch(y){case\"top\":o=c;break;case\"center\":o=c+(l-r)/2;break;case\"bottom\":o=c+(l-r)}return[[e,o],[e,o+r]]}var u,d=Xr(n.get(i,t)||0,[f,g],p,!0);switch(y){case\"left\":u=h;break;case\"center\":u=h+(s-d)/2;break;case\"right\":u=h+s-d}return[[u,e],[u+d,e]]};\"ascending\"===r&&(m=-m,v=-v,\"horizontal\"===a?h+=s:c+=l,u=u.reverse());for(var _=0;_<u.length;_++){var b=u[_],w=u[_+1],S=n.getItemModel(b);if(\"horizontal\"===a){var M=S.get([\"itemStyle\",\"width\"]);null==M?M=m:(M=Ur(M,s),\"ascending\"===r&&(M=-M));var I=x(b,h),T=x(w,h+M);h+=M+v,n.setItemLayout(b,{points:I.concat(T.slice().reverse())})}else{var C=S.get([\"itemStyle\",\"height\"]);null==C?C=m:(C=Ur(C,l),\"ascending\"===r&&(C=-C));I=x(b,c),T=x(w,c+C);c+=C+v,n.setItemLayout(b,{points:I.concat(T.slice().reverse())})}}!function(t){var e=t.hostModel.get(\"orient\");t.each((function(n){var i,r,o,a,s=t.getItemModel(n),l=s.getModel(\"label\").get(\"position\"),u=s.getModel(\"labelLine\"),h=t.getItemLayout(n),c=h.points,p=\"inner\"===l||\"inside\"===l||\"center\"===l||\"insideLeft\"===l||\"insideRight\"===l;if(p)\"insideLeft\"===l?(r=(c[0][0]+c[3][0])/2+5,o=(c[0][1]+c[3][1])/2,i=\"left\"):\"insideRight\"===l?(r=(c[1][0]+c[2][0])/2-5,o=(c[1][1]+c[2][1])/2,i=\"right\"):(r=(c[0][0]+c[1][0]+c[2][0]+c[3][0])/4,o=(c[0][1]+c[1][1]+c[2][1]+c[3][1])/4,i=\"center\"),a=[[r,o],[r,o]];else{var d=void 0,f=void 0,g=void 0,y=void 0,v=u.get(\"length\");\"left\"===l?(d=(c[3][0]+c[0][0])/2,f=(c[3][1]+c[0][1])/2,r=(g=d-v)-5,i=\"right\"):\"right\"===l?(d=(c[1][0]+c[2][0])/2,f=(c[1][1]+c[2][1])/2,r=(g=d+v)+5,i=\"left\"):\"top\"===l?(d=(c[3][0]+c[0][0])/2,o=(y=(f=(c[3][1]+c[0][1])/2)-v)-5,i=\"center\"):\"bottom\"===l?(d=(c[1][0]+c[2][0])/2,o=(y=(f=(c[1][1]+c[2][1])/2)+v)+5,i=\"center\"):\"rightTop\"===l?(d=\"horizontal\"===e?c[3][0]:c[1][0],f=\"horizontal\"===e?c[3][1]:c[1][1],\"horizontal\"===e?(o=(y=f-v)-5,i=\"center\"):(r=(g=d+v)+5,i=\"top\")):\"rightBottom\"===l?(d=c[2][0],f=c[2][1],\"horizontal\"===e?(o=(y=f+v)+5,i=\"center\"):(r=(g=d+v)+5,i=\"bottom\")):\"leftTop\"===l?(d=c[0][0],f=\"horizontal\"===e?c[0][1]:c[1][1],\"horizontal\"===e?(o=(y=f-v)-5,i=\"center\"):(r=(g=d-v)-5,i=\"right\")):\"leftBottom\"===l?(d=\"horizontal\"===e?c[1][0]:c[3][0],f=\"horizontal\"===e?c[1][1]:c[2][1],\"horizontal\"===e?(o=(y=f+v)+5,i=\"center\"):(r=(g=d-v)-5,i=\"right\")):(d=(c[1][0]+c[2][0])/2,f=(c[1][1]+c[2][1])/2,\"horizontal\"===e?(o=(y=f+v)+5,i=\"center\"):(r=(g=d+v)+5,i=\"left\")),\"horizontal\"===e?r=g=d:o=y=f,a=[[d,f],[g,y]]}h.label={linePoints:a,x:r,y:o,verticalAlign:\"middle\",textAlign:i,inside:p}}))}(n)}))}var pk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._dataGroup=new zr,n._initialized=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._dataGroup)},e.prototype.render=function(t,e,n,i){this._progressiveEls=null;var r=this._dataGroup,o=t.getData(),a=this._data,s=t.coordinateSystem,l=s.dimensions,u=gk(t);if(o.diff(a).add((function(t){yk(fk(o,r,t,l,s),o,t,u)})).update((function(e,n){var i=a.getItemGraphicEl(n),r=dk(o,e,l,s);o.setItemGraphicEl(e,i),fh(i,{shape:{points:r}},t,e),_h(i),yk(i,o,e,u)})).remove((function(t){var e=a.getItemGraphicEl(t);r.remove(e)})).execute(),!this._initialized){this._initialized=!0;var h=function(t,e,n){var i=t.model,r=t.getRect(),o=new zs({shape:{x:r.x,y:r.y,width:r.width,height:r.height}}),a=\"horizontal\"===i.get(\"layout\")?\"width\":\"height\";return o.setShape(a,0),gh(o,{shape:{width:r.width,height:r.height}},e,n),o}(s,t,(function(){setTimeout((function(){r.removeClipPath()}))}));r.setClipPath(h)}this._data=o},e.prototype.incrementalPrepareRender=function(t,e,n){this._initialized=!0,this._data=null,this._dataGroup.removeAll()},e.prototype.incrementalRender=function(t,e,n){for(var i=e.getData(),r=e.coordinateSystem,o=r.dimensions,a=gk(e),s=this._progressiveEls=[],l=t.start;l<t.end;l++){var u=fk(i,this._dataGroup,l,o,r);u.incremental=!0,yk(u,i,l,a),s.push(u)}},e.prototype.remove=function(){this._dataGroup&&this._dataGroup.removeAll(),this._data=null},e.type=\"parallel\",e}(kg);function dk(t,e,n,i){for(var r,o=[],a=0;a<n.length;a++){var s=n[a],l=t.get(t.mapDimension(s),e);r=l,(\"category\"===i.getAxis(s).type?null==r:null==r||isNaN(r))||o.push(i.dataToPoint(l,s))}return o}function fk(t,e,n,i,r){var o=dk(t,n,i,r),a=new Yu({shape:{points:o},z2:10});return e.add(a),t.setItemGraphicEl(n,a),a}function gk(t){var e=t.get(\"smooth\",!0);return!0===e&&(e=.3),nt(e=ho(e))&&(e=0),{smooth:e}}function yk(t,e,n,i){t.useStyle(e.getItemVisual(n,\"style\")),t.style.fill=null,t.setShape(\"smooth\",i.smooth);var r=e.getItemModel(n),o=r.getModel(\"emphasis\");jl(t,r,\"lineStyle\"),Yl(t,o.get(\"focus\"),o.get(\"blurScope\"),o.get(\"disabled\"))}var vk=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.getInitialData=function(t,e){return vx(null,this,{useEncodeDefaulter:W(mk,null,this)})},e.prototype.getRawIndicesByActiveState=function(t){var e=this.coordinateSystem,n=this.getData(),i=[];return e.eachActiveState(n,(function(e,r){t===e&&i.push(n.getRawIndex(r))})),i},e.type=\"series.parallel\",e.dependencies=[\"parallel\"],e.defaultOption={z:2,coordinateSystem:\"parallel\",parallelIndex:0,label:{show:!1},inactiveOpacity:.05,activeOpacity:1,lineStyle:{width:1,opacity:.45,type:\"solid\"},emphasis:{label:{show:!1}},progressive:500,smooth:!1,animationEasing:\"linear\"},e}(mg);function mk(t){var e=t.ecModel.getComponent(\"parallel\",t.get(\"parallelIndex\"));if(e){var n={};return E(e.dimensions,(function(t){var e=+t.replace(\"dim\",\"\");n[t]=e})),n}}var xk=[\"lineStyle\",\"opacity\"],_k={seriesType:\"parallel\",reset:function(t,e){var n=t.coordinateSystem,i={normal:t.get([\"lineStyle\",\"opacity\"]),active:t.get(\"activeOpacity\"),inactive:t.get(\"inactiveOpacity\")};return{progress:function(t,e){n.eachActiveState(e,(function(t,n){var r=i[t];if(\"normal\"===t&&e.hasItemOption){var o=e.getItemModel(n).get(xk,!0);null!=o&&(r=o)}e.ensureUniqueItemVisual(n,\"style\").opacity=r}),t.start,t.end)}}}};function bk(t){!function(t){if(t.parallel)return;var e=!1;E(t.series,(function(t){t&&\"parallel\"===t.type&&(e=!0)})),e&&(t.parallel=[{}])}(t),function(t){var e=bo(t.parallelAxis);E(e,(function(e){if(q(e)){var n=e.parallelIndex||0,i=bo(t.parallel)[n];i&&i.parallelAxisDefault&&C(e,i.parallelAxisDefault,!1)}}))}(t)}var wk=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._model=t,this._api=n,this._handlers||(this._handlers={},E(Sk,(function(t,e){n.getZr().on(e,this._handlers[e]=W(t,this))}),this)),Fg(this,\"_throttledDispatchExpand\",t.get(\"axisExpandRate\"),\"fixRate\")},e.prototype.dispose=function(t,e){Gg(this,\"_throttledDispatchExpand\"),E(this._handlers,(function(t,n){e.getZr().off(n,t)})),this._handlers=null},e.prototype._throttledDispatchExpand=function(t){this._dispatchExpand(t)},e.prototype._dispatchExpand=function(t){t&&this._api.dispatchAction(A({type:\"parallelAxisExpand\"},t))},e.type=\"parallel\",e}(Tg),Sk={mousedown:function(t){Mk(this,\"click\")&&(this._mouseDownPoint=[t.offsetX,t.offsetY])},mouseup:function(t){var e=this._mouseDownPoint;if(Mk(this,\"click\")&&e){var n=[t.offsetX,t.offsetY];if(Math.pow(e[0]-n[0],2)+Math.pow(e[1]-n[1],2)>5)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.span<r)&&(e[1-i]=e[i]+l.sign*r),u=Dk(e,i),null!=o&&u.span>o&&(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;u<i;u++){var h=void 0;if(l){h=\"active\";for(var c=t.getValues(a,u),p=0,d=o.length;p<d;p++){if(\"inactive\"===s[p].getActiveState(c[p])){h=\"inactive\";break}}}else h=\"normal\";e(h,u)}},t.prototype.hasAxisBrushed=function(){for(var t=this.dimensions,e=this._axesMap,n=!1,i=0,r=t.length;i<r;i++)\"normal\"!==e.get(t[i]).model.getActiveState()&&(n=!0);return n},t.prototype.axisCoordToPoint=function(t,e){return zh([t,0],this._axesLayout[e].transform)},t.prototype.getAxisLayout=function(t){return T(this._axesLayout[t])},t.prototype.getSlidedAxisExpandWindow=function(t){var e=this._makeLayoutInfo(),n=e.pixelDimIndex,i=e.axisExpandWindow.slice(),r=i[1]-i[0],o=[0,e.axisExpandWidth*(e.axisCount-1)];if(!this.containPoint(t))return{behavior:\"none\",axisExpandWindow:i};var a,s=t[n]-e.layoutBase-e.axisExpandWindow0Pos,l=\"slide\",u=e.axisCollapseWidth,h=this._model.get(\"axisExpandSlideTriggerArea\"),c=null!=h[0];if(u)c&&u&&s<r*h[0]?(l=\"jump\",a=s-r*h[2]):c&&u&&s>r*(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<l[0]?(n=t*s,i=s):t<=l[1]?(n=e.axisExpandWindow0Pos+t*o-e.axisExpandWindow[0],u=o,h=!0):(n=r-(a-1-t)*s,i=s),{position:n,axisNameAvailableWidth:u,axisLabelShow:h,nameTruncateMaxWidth:i}}var Gk={create:function(t,e){var n=[];return t.eachComponent(\"parallel\",(function(i,r){var o=new zk(i,t,e);o.name=\"parallel_\"+r,o.resize(i,e),i.coordinateSystem=o,o.model=i,n.push(o)})),t.eachSeries((function(t){if(\"parallel\"===t.get(\"coordinateSystem\")){var e=t.getReferringComponents(\"parallel\",zo).models[0];t.coordinateSystem=e.coordinateSystem}})),n}},Wk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.activeIntervals=[],n}return n(e,t),e.prototype.getAreaSelectStyle=function(){return Jo([[\"fill\",\"color\"],[\"lineWidth\",\"borderWidth\"],[\"stroke\",\"borderColor\"],[\"width\",\"width\"],[\"opacity\",\"opacity\"]])(this.getModel(\"areaSelectStyle\"))},e.prototype.setActiveIntervals=function(t){var e=this.activeIntervals=T(t);if(e)for(var n=e.length-1;n>=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;i<r;i++)if(e[i][0]<=t&&t<=e[i][1])return\"active\";return\"inactive\"},e}(Rp);R(Wk,I_);var Hk=!0,Yk=Math.min,Xk=Math.max,Uk=Math.pow,Zk=\"globalPan\",jk={w:[0,0],e:[0,1],n:[1,0],s:[1,1]},qk={w:\"ew\",e:\"ew\",n:\"ns\",s:\"ns\",ne:\"nesw\",sw:\"nesw\",nw:\"nwse\",se:\"nwse\"},Kk={brushStyle:{lineWidth:2,stroke:\"rgba(210,219,238,0.3)\",fill:\"#D2DBEE\"},transformable:!0,brushMode:\"single\",removeOnClick:!1},$k=0,Jk=function(t){function e(e){var n=t.call(this)||this;return n._track=[],n._covers=[],n._handlers={},n._zr=e,n.group=new zr,n._uid=\"brushController_\"+$k++,E(IL,(function(t,e){this._handlers[e]=W(t,this)}),n),n}return n(e,t),e.prototype.enableBrush=function(t){return this._brushType&&this._doDisableBrush(),t.brushType&&this._doEnableBrush(t),this},e.prototype._doEnableBrush=function(t){var e=this._zr;this._enableGlobalPan||function(t,e,n){XI(t)[e]=n}(e,Zk,this._uid),E(this._handlers,(function(t,n){e.on(n,t)})),this._brushType=t.brushType,this._brushOption=C(T(Kk),t,!0)},e.prototype._doDisableBrush=function(){var t=this._zr;!function(t,e,n){var i=XI(t);i[e]===n&&(i[e]=null)}(t,Zk,this._uid),E(this._handlers,(function(e,n){t.off(n,e)})),this._brushType=this._brushOption=null},e.prototype.setPanels=function(t){if(t&&t.length){var e=this._panels={};E(t,(function(t){e[t.panelId]=T(t)}))}else this._panels=null;return this},e.prototype.mount=function(t){t=t||{},this._enableGlobalPan=t.enableGlobalPan;var e=this.group;return this._zr.add(e),e.attr({x:t.x||0,y:t.y||0,rotation:t.rotation||0,scaleX:t.scaleX||1,scaleY:t.scaleY||1}),this._transform=e.getLocalTransform(),this},e.prototype.updateCovers=function(t){t=z(t,(function(t){return C(T(Kk),t,!0)}));var e=this._covers,n=this._covers=[],i=this,r=this._creatingCover;return new Vm(e,t,(function(t,e){return o(t.__brushOption,e)}),o).add(a).update(a).remove((function(t){e[t]!==r&&i.group.remove(e[t])})).execute(),this;function o(t,e){return(null!=t.id?t.id:\"\\0-brush-index-\"+e)+\"-\"+t.brushType}function a(o,a){var s=t[o];if(null!=a&&e[a]===r)n[o]=e[a];else{var l=n[o]=null!=a?(e[a].__brushOption=s,e[a]):tL(i,Qk(i,s));iL(i,l)}}},e.prototype.unmount=function(){return this.enableBrush(!1),sL(this),this._zr.remove(this.group),this},e.prototype.dispose=function(){this.unmount(),this.off()},e}(jt);function Qk(t,e){var n=CL[e.brushType].createCover(t,e);return n.__brushOption=e,nL(n,e),t.group.add(n),n}function tL(t,e){var n=rL(e);return n.endCreating&&(n.endCreating(t,e),nL(e,e.__brushOption)),e}function eL(t,e){var n=e.__brushOption;rL(e).updateCoverShape(t,e,n.range,n)}function nL(t,e){var n=e.z;null==n&&(n=1e4),t.traverse((function(t){t.z=n,t.z2=n}))}function iL(t,e){rL(e).updateCommon(t,e),eL(t,e)}function rL(t){return CL[t.__brushOption.brushType]}function oL(t,e,n){var i,r=t._panels;if(!r)return Hk;var o=t._transform;return E(r,(function(t){t.isTargetByCursor(e,n,o)&&(i=t)})),i}function aL(t,e){var n=t._panels;if(!n)return Hk;var i=e.__brushOption.panelId;return null!=i?n[i]:Hk}function sL(t){var e=t._covers,n=e.length;return E(e,(function(e){t.group.remove(e)}),t),e.length=0,!!n}function lL(t,e){var n=z(t._covers,(function(t){var e=t.__brushOption,n=T(e.range);return{brushType:e.brushType,panelId:e.panelId,range:n}}));t.trigger(\"brush\",{areas:n,isEnd:!!e.isEnd,removeOnClick:!!e.removeOnClick})}function uL(t){var e=t.length-1;return e<0&&(e=0),[t[0],t[e]]}function hL(t,e,n,i){var r=new zr;return r.add(new zs({name:\"main\",style:fL(n),silent:!0,draggable:!0,cursor:\"move\",drift:H(vL,t,e,r,[\"n\",\"s\",\"w\",\"e\"]),ondragend:H(lL,e,{isEnd:!0})})),E(i,(function(n){r.add(new zs({name:n.join(\"\"),style:{opacity:0},draggable:!0,silent:!0,invisible:!0,drift:H(vL,t,e,r,n),ondragend:H(lL,e,{isEnd:!0})}))})),r}function cL(t,e,n,i){var r=i.brushStyle.lineWidth||0,o=Xk(r,6),a=n[0][0],s=n[1][0],l=a-r/2,u=s-r/2,h=n[0][1],c=n[1][1],p=h-o+r/2,d=c-o+r/2,f=h-a,g=c-s,y=f+r,v=g+r;dL(t,e,\"main\",a,s,f,g),i.transformable&&(dL(t,e,\"w\",l,u,o,v),dL(t,e,\"e\",p,u,o,v),dL(t,e,\"n\",l,u,y,o),dL(t,e,\"s\",l,d,y,o),dL(t,e,\"nw\",l,u,o,o),dL(t,e,\"ne\",p,u,o,o),dL(t,e,\"sw\",l,d,o,o),dL(t,e,\"se\",p,d,o,o))}function pL(t,e){var n=e.__brushOption,i=n.transformable,r=e.childAt(0);r.useStyle(fL(n)),r.attr({silent:!i,cursor:i?\"move\":\"default\"}),E([[\"w\"],[\"e\"],[\"n\"],[\"s\"],[\"s\",\"e\"],[\"s\",\"w\"],[\"n\",\"e\"],[\"n\",\"w\"]],(function(n){var r=e.childOfName(n.join(\"\")),o=1===n.length?yL(t,n[0]):function(t,e){var n=[yL(t,e[0]),yL(t,e[1])];return(\"e\"===n[0]||\"w\"===n[0])&&n.reverse(),n.join(\"\")}(t,n);r&&r.attr({silent:!i,invisible:!i,cursor:i?qk[o]+\"-resize\":null})}))}function dL(t,e,n,i,r,o,a){var s=e.childOfName(n);s&&s.setShape(function(t){var e=Yk(t[0][0],t[1][0]),n=Yk(t[0][1],t[1][1]),i=Xk(t[0][0],t[1][0]),r=Xk(t[0][1],t[1][1]);return{x:e,y:n,width:i-e,height:r-n}}(_L(t,e,[[i,r],[i+o,r+a]])))}function fL(t){return k({strokeNoScale:!0},t.brushStyle)}function gL(t,e,n,i){var r=[Yk(t,n),Yk(e,i)],o=[Xk(t,n),Xk(e,i)];return[[r[0],o[0]],[r[1],o[1]]]}function yL(t,e){var n=Vh({w:\"left\",e:\"right\",n:\"top\",s:\"bottom\"}[e],function(t){return Eh(t.group)}(t));return{left:\"w\",right:\"e\",top:\"n\",bottom:\"s\"}[n]}function vL(t,e,n,i,r,o){var a=n.__brushOption,s=t.toRectRange(a.range),l=xL(e,r,o);E(i,(function(t){var e=jk[t];s[e[0]][e[1]]+=l[e[0]]})),a.range=t.fromRectRange(gL(s[0][0],s[1][0],s[0][1],s[1][1])),iL(e,n),lL(e,{isEnd:!1})}function mL(t,e,n,i){var r=e.__brushOption.range,o=xL(t,n,i);E(r,(function(t){t[0]+=o[0],t[1]+=o[1]})),iL(t,e),lL(t,{isEnd:!1})}function xL(t,e,n){var i=t.group,r=i.transformCoordToLocal(e,n),o=i.transformCoordToLocal(0,0);return[r[0]-o[0],r[1]-o[1]]}function _L(t,e,n){var i=aL(t,e);return i&&i!==Hk?i.clipPath(n,t._transform):T(n)}function bL(t){var e=t.event;e.preventDefault&&e.preventDefault()}function wL(t,e,n){return t.childOfName(\"main\").contain(e,n)}function SL(t,e,n,i){var r,o=t._creatingCover,a=t._creatingPanel,s=t._brushOption;if(t._track.push(n.slice()),function(t){var e=t._track;if(!e.length)return!1;var n=e[e.length-1],i=e[0],r=n[0]-i[0],o=n[1]-i[1];return Uk(r*r+o*o,.5)>6}(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<r.length;a++){var s=r[a].__brushOption;if(o&&(o===Hk||s.panelId===o.panelId)&&CL[s.brushType].contain(r[a],n[0],n[1]))return}o&&i.setCursorStyle(\"crosshair\")}}(this,t,i),this._dragging){bL(t);var r=SL(this,t,i,!1);r&&lL(this,r)}},mouseup:function(t){TL(this,t)}};function TL(t,e){if(t._dragging){bL(e);var n=e.offsetX,i=e.offsetY,r=t.group.transformCoordToLocal(n,i),o=SL(t,e,r,!0);t._dragging=!1,t._track=[],t._creatingCover=null,o&&lL(t,o)}}var CL={lineX:DL(0),lineY:DL(1),rect:{createCover:function(t,e){function n(t){return t}return hL({toRectRange:n,fromRectRange:n},t,e,[[\"w\"],[\"e\"],[\"n\"],[\"s\"],[\"s\",\"e\"],[\"s\",\"w\"],[\"n\",\"e\"],[\"n\",\"w\"]])},getCreatingRange:function(t){var e=uL(t);return gL(e[1][0],e[1][1],e[0][0],e[0][1])},updateCoverShape:function(t,e,n,i){cL(t,e,n,i)},updateCommon:pL,contain:wL},polygon:{createCover:function(t,e){var n=new zr;return n.add(new Yu({name:\"main\",style:fL(e),silent:!0})),n},getCreatingRange:function(t){return t},endCreating:function(t,e){e.remove(e.childAt(0)),e.add(new Wu({name:\"main\",draggable:!0,drift:H(mL,t,e),ondragend:H(lL,t,{isEnd:!0})}))},updateCoverShape:function(t,e,n,i){e.childAt(0).setShape({points:_L(t,e,n)})},updateCommon:pL,contain:wL}};function DL(t){return{createCover:function(e,n){return hL({toRectRange:function(e){var n=[e,[0,100]];return t&&n.reverse(),n},fromRectRange:function(e){return e[t]}},e,n,[[[\"w\"],[\"e\"]],[[\"n\"],[\"s\"]]][t])},getCreatingRange:function(e){var n=uL(e);return[Yk(n[0][t],n[1][t]),Xk(n[0][t],n[1][t])]},updateCoverShape:function(e,n,i,r){var o,a=aL(e,n);if(a!==Hk&&a.getLinearBrushOtherExtent)o=a.getLinearBrushOtherExtent(t);else{var s=e._zr;o=[0,[s.getWidth(),s.getHeight()][1-t]]}var l=[i,o];t&&l.reverse(),cL(e,n,l,r)},updateCommon:pL,contain:wL}}function AL(t){return t=PL(t),function(e){return Gh(e,t)}}function kL(t,e){return t=PL(t),function(n){var i=null!=e?e:n,r=i?t.width:t.height,o=i?t.x:t.y;return[o,o+(r||0)]}}function LL(t,e,n){var i=PL(t);return function(t,r){return i.contain(r[0],r[1])&&!tT(t,e,n)}}function PL(t){return ze.create(t)}var OL=[\"axisLine\",\"axisTickLabel\",\"axisName\"],RL=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){t.prototype.init.apply(this,arguments),(this._brushController=new Jk(n.getZr())).on(\"brush\",W(this._onBrush,this))},e.prototype.render=function(t,e,n,i){if(!function(t,e,n){return n&&\"axisAreaSelect\"===n.type&&e.findComponents({mainType:\"parallelAxis\",query:n})[0]===t}(t,e,i)){this.axisModel=t,this.api=n,this.group.removeAll();var r=this._axisGroup;if(this._axisGroup=new zr,this.group.add(this._axisGroup),t.get(\"show\")){var o=function(t,e){return e.getComponent(\"parallel\",t.get(\"parallelIndex\"))}(t,e),a=o.coordinateSystem,s=t.getAreaSelectStyle(),l=s.width,u=t.axis.dim,h=A({strokeContainThreshold:l},a.getAxisLayout(u)),c=new iI(t,h);E(OL,c.add,c),this._axisGroup.add(c.getGroup()),this._refreshBrushController(h,s,t,o,l,n),Fh(r,this._axisGroup,t)}}},e.prototype._refreshBrushController=function(t,e,n,i,r,o){var a=n.axis.getExtent(),s=a[1]-a[0],l=Math.min(30,.1*Math.abs(s)),u=ze.create({x:a[0],y:-r/2,width:s,height:r});u.x-=l,u.width+=2*l,this._brushController.mount({enableGlobalPan:!0,rotation:t.rotation,x:t.position[0],y:t.position[1]}).setPanels([{panelId:\"pl\",clipPath:AL(u),isTargetByCursor:LL(u,o,i),getLinearBrushOtherExtent:kL(u,0)}]).enableBrush({brushType:\"lineX\",brushStyle:e,removeOnClick:!0}).updateCovers(function(t){var e=t.axis;return z(t.activeIntervals,(function(t){return{brushType:\"lineX\",panelId:\"pl\",range:[e.dataToCoord(t[0],!0),e.dataToCoord(t[1],!0)]}}))}(n))},e.prototype._onBrush=function(t){var e=t.areas,n=this.axisModel,i=n.axis,r=z(e,(function(t){return[i.coordToData(t.range[0],!0),i.coordToData(t.range[1],!0)]}));(!n.option.realtime===t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:\"axisAreaSelect\",parallelAxisId:n.id,intervals:r})},e.prototype.dispose=function(){this._brushController.dispose()},e.type=\"parallelAxis\",e}(Tg);var NL={type:\"axisAreaSelect\",event:\"axisAreaSelected\"};var EL={type:\"value\",areaSelectStyle:{width:20,borderWidth:1,borderColor:\"rgba(160,197,232)\",color:\"rgba(160,197,232)\",opacity:.3},realtime:!0,z:10};function zL(t){t.registerComponentView(wk),t.registerComponentModel(Ik),t.registerCoordinateSystem(\"parallel\",Gk),t.registerPreprocessor(bk),t.registerComponentModel(Wk),t.registerComponentView(RL),FM(t,\"parallel\",Wk,EL),function(t){t.registerAction(NL,(function(t,e){e.eachComponent({mainType:\"parallelAxis\",query:t},(function(e){e.axis.model.setActiveIntervals(t.intervals)}))})),t.registerAction(\"parallelAxisExpand\",(function(t,e){e.eachComponent({mainType:\"parallel\",query:t},(function(e){e.setAxisExpand(t)}))}))}(t)}var VL=function(){this.x1=0,this.y1=0,this.x2=0,this.y2=0,this.cpx1=0,this.cpy1=0,this.cpx2=0,this.cpy2=0,this.extent=0},BL=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new VL},e.prototype.buildPath=function(t,e){var n=e.extent;t.moveTo(e.x1,e.y1),t.bezierCurveTo(e.cpx1,e.cpy1,e.cpx2,e.cpy2,e.x2,e.y2),\"vertical\"===e.orient?(t.lineTo(e.x2+n,e.y2),t.bezierCurveTo(e.cpx2+n,e.cpy2,e.cpx1+n,e.cpy1,e.x1+n,e.y1)):(t.lineTo(e.x2,e.y2+n),t.bezierCurveTo(e.cpx2,e.cpy2+n,e.cpx1,e.cpy1+n,e.x1,e.y1+n)),t.closePath()},e.prototype.highlight=function(){kl(this)},e.prototype.downplay=function(){Ll(this)},e}(Is),FL=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._focusAdjacencyDisabled=!1,n}return n(e,t),e.prototype.render=function(t,e,n){var i=this,r=t.getGraph(),o=this.group,a=t.layoutInfo,s=a.width,l=a.height,u=t.getData(),h=t.getData(\"edge\"),c=t.get(\"orient\");this._model=t,o.removeAll(),o.x=a.x,o.y=a.y,r.eachEdge((function(e){var n=new BL,i=Qs(n);i.dataIndex=e.dataIndex,i.seriesIndex=t.seriesIndex,i.dataType=\"edge\";var r,a,u,p,d,f,g,y,v=e.getModel(),m=v.getModel(\"lineStyle\"),x=m.get(\"curveness\"),_=e.node1.getLayout(),b=e.node1.getModel(),w=b.get(\"localX\"),S=b.get(\"localY\"),M=e.node2.getLayout(),I=e.node2.getModel(),T=I.get(\"localX\"),C=I.get(\"localY\"),D=e.getLayout();n.shape.extent=Math.max(1,D.dy),n.shape.orient=c,\"vertical\"===c?(r=(null!=w?w*s:_.x)+D.sy,a=(null!=S?S*l:_.y)+_.dy,u=(null!=T?T*s:M.x)+D.ty,d=r,f=a*(1-x)+(p=null!=C?C*l:M.y)*x,g=u,y=a*x+p*(1-x)):(r=(null!=w?w*s:_.x)+_.dx,a=(null!=S?S*l:_.y)+D.sy,d=r*(1-x)+(u=null!=T?T*s:M.x)*x,f=a,g=r*x+u*(1-x),y=p=(null!=C?C*l:M.y)+D.ty),n.setShape({x1:r,y1:a,x2:u,y2:p,cpx1:d,cpy1:f,cpx2:g,cpy2:y}),n.useStyle(m.getItemStyle()),GL(n.style,c,e);var A=\"\"+v.get(\"value\"),k=ec(v,\"edgeLabel\");tc(n,k,{labelFetcher:{getFormattedLabel:function(e,n,i,r,o,a){return t.getFormattedLabel(e,n,\"edge\",r,ot(o,k.normal&&k.normal.get(\"formatter\"),A),a)}},labelDataIndex:e.dataIndex,defaultText:A}),n.setTextConfig({position:\"inside\"});var L=v.getModel(\"emphasis\");jl(n,v,\"lineStyle\",(function(t){var n=t.getItemStyle();return GL(n,c,e),n})),o.add(n),h.setItemGraphicEl(e.dataIndex,n);var P=L.get(\"focus\");Yl(n,\"adjacency\"===P?e.getAdjacentDataIndices():\"trajectory\"===P?e.getTrajectoryDataIndices():P,L.get(\"blurScope\"),L.get(\"disabled\"))})),r.eachNode((function(e){var n=e.getLayout(),i=e.getModel(),r=i.get(\"localX\"),a=i.get(\"localY\"),h=i.getModel(\"emphasis\"),c=new zs({shape:{x:null!=r?r*s:n.x,y:null!=a?a*l:n.y,width:n.dx,height:n.dy},style:i.getModel(\"itemStyle\").getItemStyle(),z2:10});tc(c,ec(i),{labelFetcher:{getFormattedLabel:function(e,n){return t.getFormattedLabel(e,n,\"node\")}},labelDataIndex:e.dataIndex,defaultText:e.id}),c.disableLabelAnimation=!0,c.setStyle(\"fill\",e.getVisual(\"color\")),c.setStyle(\"decal\",e.getVisual(\"style\").decal),jl(c,i),o.add(c),u.setItemGraphicEl(e.dataIndex,c),Qs(c).dataType=\"node\";var p=h.get(\"focus\");Yl(c,\"adjacency\"===p?e.getAdjacentDataIndices():\"trajectory\"===p?e.getTrajectoryDataIndices():p,h.get(\"blurScope\"),h.get(\"disabled\"))})),u.eachItemGraphicEl((function(e,r){u.getItemModel(r).get(\"draggable\")&&(e.drift=function(e,o){i._focusAdjacencyDisabled=!0,this.shape.x+=e,this.shape.y+=o,this.dirty(),n.dispatchAction({type:\"dragNode\",seriesId:t.id,dataIndex:u.getRawIndex(r),localX:this.shape.x/s,localY:this.shape.y/l})},e.ondragend=function(){i._focusAdjacencyDisabled=!1},e.draggable=!0,e.cursor=\"move\")})),!this._data&&t.isAnimationEnabled()&&o.setClipPath(function(t,e,n){var i=new zs({shape:{x:t.x-10,y:t.y-10,width:0,height:t.height+20}});return gh(i,{shape:{width:t.width+20}},e,n),i}(o.getBoundingRect(),t,(function(){o.removeClipPath()}))),this._data=t.getData()},e.prototype.dispose=function(){},e.type=\"sankey\",e}(kg);function GL(t,e,n){switch(t.fill){case\"source\":t.fill=n.node1.getVisual(\"color\"),t.decal=n.node1.getVisual(\"style\").decal;break;case\"target\":t.fill=n.node2.getVisual(\"color\"),t.decal=n.node2.getVisual(\"style\").decal;break;case\"gradient\":var i=n.node1.getVisual(\"color\"),r=n.node2.getVisual(\"color\");U(i)&&U(r)&&(t.fill=new nh(0,0,+(\"horizontal\"===e),+(\"vertical\"===e),[{color:i,offset:0},{color:r,offset:1}]))}}var WL=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){var n=t.edges||t.links,i=t.data||t.nodes,r=t.levels;this.levelModels=[];for(var o=this.levelModels,a=0;a<r.length;a++)null!=r[a].depth&&r[a].depth>=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<e.length;p++)s[p]=1;for(p=0;p<t.length;p++)l[p]=t[p].inEdges.length,0===l[p]&&u.push(t[p]);var d=-1;for(;u.length;){for(var f=0;f<u.length;f++){var g=u[f],y=g.hostGraph.data.getRawDataItem(g.dataIndex),v=null!=y.depth&&y.depth>=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;m<g.outEdges.length;m++){var x=g.outEdges[m];s[e.indexOf(x)]=0;var _=x.node2;0==--l[t.indexOf(_)]&&h.indexOf(_)<0&&h.push(_)}}++c,u=h,h=[]}for(p=0;p<s.length;p++)if(1===s[p])throw new Error(\"Sankey is a DAG, the original data has cycle!\");var b=d>c-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;s<o.length;s++){var l=o[s];l.setLayout({skNodeHeight:a},!0);for(var u=0;u<l.inEdges.length;u++){var h=l.inEdges[u];r.indexOf(h.node1)<0&&r.push(h.node1)}}o=r,r=[],++a}E(t,(function(t){YL(t)||t.setLayout({depth:Math.max(0,i-t.getLayout().skNodeHeight)},!0)}))}else\"justify\"===e&&function(t,e){E(t,(function(t){YL(t)||t.outEdges.length||t.setLayout({depth:e},!0)}))}(t,i)}(t,a,0,b);var w=\"vertical\"===o?(r-n)/b:(i-n)/b;!function(t,e,n){E(t,(function(t){var i=t.getLayout().depth*e;\"vertical\"===n?t.setLayout({y:i},!0):t.setLayout({x:i},!0)}))}(t,w,o)})(t,e,n,r,o,s,l),function(t,e,n,i,r,o,a){var s=function(t,e){var n=[],i=\"vertical\"===e?\"y\":\"x\",r=Go(t,(function(t){return t.getLayout()[i]}));return r.keys.sort((function(t,e){return t-e})),E(r.keys,(function(t){n.push(r.buckets.get(t))})),n}(t,a);(function(t,e,n,i,r,o){var a=1/0;E(t,(function(t){var e=t.length,s=0;E(t,(function(t){s+=t.getLayout().value}));var l=\"vertical\"===o?(i-(e-1)*r)/s:(n-(e-1)*r)/s;l<a&&(a=l)})),E(t,(function(t){E(t,(function(t,e){var n=t.getLayout().value*a;\"vertical\"===o?(t.setLayout({x:e},!0),t.setLayout({dx:n},!0)):(t.setLayout({y:e},!0),t.setLayout({dy:n},!0))}))})),E(e,(function(t){var e=+t.getValue()*a;t.setLayout({dy:e},!0)}))})(s,e,n,i,r,a),XL(s,r,n,i,a);for(var l=1;o>0;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;p<h;p++)(l=u-(s=t[p]).getLayout()[o])>0&&(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;++o<r;){var a=+e(t[o],n);isNaN(a)||(i+=a)}return i}function tP(t,e,n){E(t,(function(t){E(t,(function(t){if(t.inEdges.length){var i=QL(t.inEdges,qL,n)/QL(t.inEdges,JL);if(isNaN(i)){var r=t.inEdges.length;i=r?QL(t.inEdges,KL,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 eP(t){t.eachSeriesByType(\"sankey\",(function(t){var e=t.getGraph(),n=e.nodes,i=e.edges;if(n.length){var r=1/0,o=-1/0;E(n,(function(t){var e=t.getLayout().value;e<r&&(r=e),e>o&&(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();i<n.length;i++)t.moveTo(n[i][0],n[i][1]),i++,t.lineTo(n[i][0],n[i][1])},e}(Is);function sP(t,e,n,i,r){var o=t.ends,a=new aP({shape:{points:r?uP(o,i,t):o}});return lP(t,a,e,n,r),a}function lP(t,e,n,i,r){var o=n.hostModel;(0,Kh[r?\"initProps\":\"updateProps\"])(e,{shape:{points:t.ends}},o,i),e.useStyle(n.getItemVisual(i,\"style\")),e.style.strokeNoScale=!0,e.z2=100;var a=n.getItemModel(i),s=a.getModel(\"emphasis\");jl(e,a),Yl(e,s.get(\"focus\"),s.get(\"blurScope\"),s.get(\"disabled\"))}function uP(t,e,n){return z(t,(function(t){return(t=t.slice())[e]=n.initBaseline,t}))}var hP=E;function cP(t){var e=function(t){var e=[],n=[];return t.eachSeriesByType(\"boxplot\",(function(t){var i=t.getBaseAxis(),r=P(n,i);r<0&&(r=n.length,n[r]=i,e[r]={axis:i,seriesModels:[]}),e[r].seriesModels.push(t)})),e}(t);hP(e,(function(t){var e=t.seriesModels;e.length&&(!function(t){var e,n=t.axis,i=t.seriesModels,r=i.length,o=t.boxWidthList=[],a=t.boxOffsetList=[],s=[];if(\"category\"===n.type)e=n.getBandWidth();else{var l=0;hP(i,(function(t){l=Math.max(l,t.getData().count())}));var u=n.getExtent();e=Math.abs(u[1]-u[0])/l}hP(i,(function(t){var n=t.get(\"boxWidth\");Y(n)||(n=[n,n]),s.push([Ur(n[0],e)||0,Ur(n[1],e)||0])}));var h=.8*e-2,c=h/r*.3,p=(h-c*(r-1))/r,d=p/2-h/2;hP(i,(function(t,e){a.push(d),d+=c+p,o.push(Math.min(Math.max(p,s[e][0]),s[e][1]))}))}(t),hP(e,(function(e,n){!function(t,e,n){var i=t.coordinateSystem,r=t.getData(),o=n/2,a=\"horizontal\"===t.get(\"layout\")?0:1,s=1-a,l=[\"x\",\"y\"],u=r.mapDimension(l[a]),h=r.mapDimensionsAll(l[s]);if(null==u||h.length<5)return;for(var c=0;c<r.count();c++){var p=r.get(u,c),d=x(p,h[2],c),f=x(p,h[0],c),g=x(p,h[1],c),y=x(p,h[3],c),v=x(p,h[4],c),m=[];_(m,g,!1),_(m,y,!0),m.push(f,g,v,y),b(m,f),b(m,v),b(m,d),r.setItemLayout(c,{initBaseline:d[s],ends:m})}function x(t,n,o){var l,u=r.get(n,o),h=[];return h[a]=t,h[s]=u,isNaN(t)||isNaN(u)?l=[NaN,NaN]:(l=i.dataToPoint(h))[a]+=e,l}function _(t,e,n){var i=e.slice(),r=e.slice();i[a]+=o,r[a]-=o,n?t.push(i,r):t.push(r,i)}function b(t,e){var n=e.slice(),i=e.slice();n[a]-=o,i[a]+=o,t.push(n,i)}}(e,t.boxOffsetList[n],t.boxWidthList[n])})))}))}var pP={type:\"echarts:boxplot\",transform:function(t){var e=t.upstream;if(e.sourceFormat!==Fp){var n=\"\";0,vo(n)}var i=function(t,e){for(var n=[],i=[],r=(e=e||{}).boundIQR,o=\"none\"===r||0===r,a=0;a<t.length;a++){var s=jr(t[a].slice()),l=lo(s,.25),u=lo(s,.5),h=lo(s,.75),c=s[0],p=s[s.length-1],d=(null==r?1.5:r)*(h-l),f=o?c:Math.max(c,l-d),g=o?p:Math.min(p,h+d),y=e.itemNameFormatter,v=X(y)?y({value:a}):U(y)?y.replace(\"{value}\",a+\"\"):a+\"\";n.push([v,f,l,u,h,g]);for(var m=0;m<s.length;m++){var x=s[m];if(x<f||x>g){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;i<e.ends.length;i++)if(t.contain(e.ends[i][0],e.ends[i][1])){n=!1;break}return n}function xP(t,e,n,i){var r=e.getItemModel(n);t.useStyle(e.getItemVisual(n,\"style\")),t.style.strokeNoScale=!0,t.__simpleBox=i,jl(t,r)}function _P(t,e){return z(t,(function(t){return(t=t.slice())[1]=e.initBaseline,t}))}var bP=function(){},wP=function(t){function e(e){var n=t.call(this,e)||this;return n.type=\"largeCandlestickBox\",n}return n(e,t),e.prototype.getDefaultShape=function(){return new bP},e.prototype.buildPath=function(t,e){for(var n=e.points,i=0;i<n.length;)if(this.__sign===n[i++]){var r=n[i++];t.moveTo(r,n[i++]),t.lineTo(r,n[i++])}else i+=3},e}(Is);function SP(t,e,n,i){var r=t.getData().getLayout(\"largePoints\"),o=new wP({shape:{points:r},__sign:1,ignoreCoarsePointer:!0});e.add(o);var a=new wP({shape:{points:r},__sign:-1,ignoreCoarsePointer:!0});e.add(a);var s=new wP({shape:{points:r},__sign:0,ignoreCoarsePointer:!0});e.add(s),MP(1,o,t),MP(-1,a,t),MP(0,s,t),i&&(o.incremental=!0,a.incremental=!0),n&&n.push(o,a)}function MP(t,e,n,i){var r=n.get([\"itemStyle\",t>0?\"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:n<i?1:o?0:e>0?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;o<i;o++){var a=Wy(e,-1,-1,2,2,n);a.attr({style:{strokeNoScale:!0},z2:99,silent:!0,scaleX:.5,scaleY:.5});var s=-o/i*t.period+t.effectOffset;a.animate(\"\",!0).when(t.period,{scaleX:t.rippleScale/2,scaleY:t.rippleScale/2}).delay(s).start(),a.animateStyle(!0).when(t.period,{opacity:0}).delay(s).start(),r.add(a)}NP(r,t)},e.prototype.updateEffectAnimation=function(t){for(var e=this._effectCfg,n=this.childAt(1),i=[\"symbolType\",\"period\",\"rippleScale\",\"rippleNumber\"],r=0;r<i.length;r++){var o=i[r];if(e[o]!==t[o])return this.stopEffectAnimation(),void this.startEffectAnimation(t)}NP(n,t)},e.prototype.highlight=function(){kl(this)},e.prototype.downplay=function(){Ll(this)},e.prototype.getSymbolType=function(){var t=this.childAt(0);return t&&t.getSymbolType()},e.prototype.updateData=function(t,e){var n=this,i=t.hostModel;this.childAt(0).updateData(t,e);var r=this.childAt(1),o=t.getItemModel(e),a=t.getItemVisual(e,\"symbol\"),s=Hy(t.getItemVisual(e,\"symbolSize\")),l=t.getItemVisual(e,\"style\"),u=l&&l.fill,h=o.getModel(\"emphasis\");r.setScale(s),r.traverse((function(t){t.setStyle(\"fill\",u)}));var c=Yy(t.getItemVisual(e,\"symbolOffset\"),s);c&&(r.x=c[0],r.y=c[1]);var p=t.getItemVisual(e,\"symbolRotate\");r.rotation=(p||0)*Math.PI/180||0;var d={};d.showEffectOn=i.get(\"showEffectOn\"),d.rippleScale=o.get([\"rippleEffect\",\"scale\"]),d.brushType=o.get([\"rippleEffect\",\"brushType\"]),d.period=1e3*o.get([\"rippleEffect\",\"period\"]),d.effectOffset=e/t.count(),d.z=i.getShallow(\"z\")||0,d.zlevel=i.getShallow(\"zlevel\")||0,d.symbolType=a,d.color=u,d.rippleEffectColor=o.get([\"rippleEffect\",\"color\"]),d.rippleNumber=o.get([\"rippleEffect\",\"number\"]),\"render\"===d.showEffectOn?(this._effectCfg?this.updateEffectAnimation(d):this.startEffectAnimation(d),this._effectCfg=d):(this._effectCfg=null,this.stopEffectAnimation(),this.onHoverStateChange=function(t){\"emphasis\"===t?\"render\"!==d.showEffectOn&&n.startEffectAnimation(d):\"normal\"===t&&\"render\"!==d.showEffectOn&&n.stopEffectAnimation()}),this._effectCfg=d,Yl(this,h.get(\"focus\"),h.get(\"blurScope\"),h.get(\"disabled\"))},e.prototype.fadeOut=function(t){t&&t()},e}(zr),zP=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._symbolDraw=new hS(EP)},e.prototype.render=function(t,e,n){var i=t.getData(),r=this._symbolDraw;r.updateData(i,{clipShape:this._getClipShape(t)}),this.group.add(r.group)},e.prototype._getClipShape=function(t){var e=t.coordinateSystem,n=e&&e.getArea&&e.getArea();return t.get(\"clip\",!0)?n:null},e.prototype.updateTransform=function(t,e,n){var i=t.getData();this.group.dirty();var r=ES(\"\").reset(t,e,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout()},e.prototype._updateGroupTransform=function(t){var e=t.coordinateSystem;e&&e.getRoamTransform&&(this.group.transform=Te(e.getRoamTransform()),this.group.decomposeTransform())},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(!0)},e.type=\"effectScatter\",e}(kg),VP=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.brushSelector=function(t,e,n){return n.point(e.getItemLayout(t))},e.type=\"series.effectScatter\",e.dependencies=[\"grid\",\"polar\"],e.defaultOption={coordinateSystem:\"cartesian2d\",z:2,legendHoverLink:!0,effectType:\"ripple\",progressive:0,showEffectOn:\"render\",clip:!0,rippleEffect:{period:4,scale:2.5,brushType:\"fill\",number:3},universalTransition:{divideShape:\"clone\"},symbolSize:10},e}(mg);var BP=function(t){function e(e,n,i){var r=t.call(this)||this;return r.add(r.createLine(e,n,i)),r._updateEffectSymbol(e,n),r}return n(e,t),e.prototype.createLine=function(t,e,n){return new OA(t,e,n)},e.prototype._updateEffectSymbol=function(t,e){var n=t.getItemModel(e).getModel(\"effect\"),i=n.get(\"symbolSize\"),r=n.get(\"symbol\");Y(i)||(i=[i,i]);var o=t.getItemVisual(e,\"style\"),a=n.get(\"color\")||o&&o.stroke,s=this.childAt(1);this._symbolType!==r&&(this.remove(s),(s=Wy(r,-.5,-.5,1,1,a)).z2=100,s.culling=!0,this.add(s)),s&&(s.setStyle(\"shadowColor\",a),s.setStyle(n.getItemStyle([\"color\"])),s.scaleX=i[0],s.scaleY=i[1],s.setColor(a),this._symbolType=r,this._symbolScale=i,this._updateEffectAnimation(t,n,e))},e.prototype._updateEffectAnimation=function(t,e,n){var i=this.childAt(1);if(i){var r=t.getItemLayout(n),o=1e3*e.get(\"period\"),a=e.get(\"loop\"),s=e.get(\"roundTrip\"),l=e.get(\"constantSpeed\"),u=it(e.get(\"delay\"),(function(e){return e/t.count()*o/3}));if(i.ignore=!0,this._updateAnimationPoints(i,r),l>0&&(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<t.__t?(t.scaleY=1.05*Vt(a,o),1===r&&(o[0]=a[0]+(o[0]-a[0])/2,o[1]=a[1]+(o[1]-a[1])/2)):1===t.__lastT?t.scaleY=2*Vt(e,o):t.scaleY=this._symbolScale[1]),t.__lastT=t.__t,t.ignore=!1,t.x=o[0],t.y=o[1]},e.prototype.updateLayout=function(t,e){this.childAt(0).updateLayout(t,e);var n=t.getItemModel(e).getModel(\"effect\");this._updateEffectAnimation(t,n,e)},e}(zr),FP=function(t){function e(e,n,i){var r=t.call(this)||this;return r._createPolyline(e,n,i),r}return n(e,t),e.prototype._createPolyline=function(t,e,n){var i=t.getItemLayout(e),r=new Yu({shape:{points:i}});this.add(r),this._updateCommonStl(t,e,n)},e.prototype.updateData=function(t,e,n){var i=t.hostModel;fh(this.childAt(0),{shape:{points:t.getItemLayout(e)}},i,e),this._updateCommonStl(t,e,n)},e.prototype._updateCommonStl=function(t,e,n){var i=this.childAt(0),r=t.getItemModel(e),o=n&&n.emphasisLineStyle,a=n&&n.focus,s=n&&n.blurScope,l=n&&n.emphasisDisabled;if(!n||t.hasItemOption){var u=r.getModel(\"emphasis\");o=u.getModel(\"lineStyle\").getLineStyle(),l=u.get(\"disabled\"),a=u.get(\"focus\"),s=u.get(\"blurScope\")}i.useStyle(t.getItemVisual(e,\"style\")),i.style.fill=null,i.style.strokeNoScale=!0,i.ensureState(\"emphasis\").style=o,Yl(this,a,s,l)},e.prototype.updateLayout=function(t,e){this.childAt(0).setShape(\"points\",t.getItemLayout(e))},e}(zr),GP=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._lastFrame=0,e._lastFramePercent=0,e}return n(e,t),e.prototype.createLine=function(t,e,n){return new FP(t,e,n)},e.prototype._updateAnimationPoints=function(t,e){this._points=e;for(var n=[0],i=0,r=1;r<e.length;r++){var o=e[r-1],a=e[r];i+=Vt(o,a),n.push(i)}if(0!==i){for(r=0;r<n.length;r++)n[r]/=i;this._offsets=n,this._length=i}else this._length=0},e.prototype._getLineLength=function(){return this._length},e.prototype._updateSymbolPosition=function(t){var e=t.__t<1?t.__t:2-t.__t,n=this._points,i=this._offsets,r=n.length;if(i){var o,a=this._lastFrame;if(e<this._lastFramePercent){for(o=Math.min(a+1,r-1);o>=0&&!(i[o]<=e);o--);o=Math.min(o,r-2)}else{for(o=a;o<r&&!(i[o]>e);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;n<i.length;){var o=i[n++];if(o>0){t.moveTo(i[n++],i[n++]);for(var a=1;a<o;a++)t.lineTo(i[n++],i[n++])}}else for(n=this._off;n<i.length;){var s=i[n++],l=i[n++],u=i[n++],h=i[n++];if(t.moveTo(s,l),r>0){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;s<i.length;){var l=i[s++];if(l>0)for(var u=i[s++],h=i[s++],c=1;c<l;c++){if(as(u,h,p=i[s++],d=i[s++],o,t,e))return a}a++}else for(a=0,s=0;s<i.length;){u=i[s++],h=i[s++];var p=i[s++],d=i[s++];if(r>0){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;a<e.length;){var s=e[a++],l=e[a++];n=Math.min(s,n),r=Math.max(s,r),i=Math.min(l,i),o=Math.max(l,o)}t=this._rect=new ze(n,i,r,o)}return t},e}(Is),YP=function(){function t(){this.group=new zr}return t.prototype.updateData=function(t){this._clear();var e=this._create();e.setShape({segs:t.getLayout(\"linesPoints\")}),this._setCommon(e,t)},t.prototype.incrementalPrepareUpdate=function(t){this.group.removeAll(),this._clear()},t.prototype.incrementalUpdate=function(t,e){var n=this._newAdded[0],i=e.getLayout(\"linesPoints\"),r=n&&n.shape.segs;if(r&&r.length<2e4){var o=r.length,a=new Float32Array(o+i.length);a.set(r),a.set(i,o),n.setShape({segs:a})}else{this._newAdded=[];var s=this._create();s.incremental=!0,s.setShape({segs:i}),this._setCommon(s,e),s.__startIndex=t.start}},t.prototype.remove=function(){this._clear()},t.prototype.eachRendered=function(t){this._newAdded[0]&&t(this._newAdded[0])},t.prototype._create=function(){var t=new HP({cursor:\"default\",ignoreCoarsePointer:!0});return this._newAdded.push(t),this.group.add(t),t},t.prototype._setCommon=function(t,e,n){var i=e.hostModel;t.setShape({polyline:i.get(\"polyline\"),curveness:i.get([\"lineStyle\",\"curveness\"])}),t.useStyle(i.getModel(\"lineStyle\").getLineStyle()),t.style.strokeNoScale=!0;var r=e.getVisual(\"style\");r&&r.stroke&&t.setStyle(\"stroke\",r.stroke),t.setStyle(\"fill\",null);var o=Qs(t);o.seriesIndex=i.seriesIndex,t.on(\"mousemove\",(function(e){o.dataIndex=null;var n=t.hoverDataIdx;n>0&&(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;h<r.end;h++)u+=t.getLineCoordsCount(h);s=new Float32Array(l+2*u)}else s=new Float32Array(4*l);var c=0,p=[];for(h=r.start;h<r.end;h++){var d=t.getLineCoords(h,a);n&&(s[c++]=d);for(var f=0;f<d;f++)p=e.dataToPoint(a[f],!1,p),s[c++]=p[0],s[c++]=p[1]}o.setLayout(\"linesPoints\",s)}else for(h=r.start;h<r.end;h++){var g=o.getItemModel(h),y=(d=t.getLineCoords(h,a),[]);if(n)for(var v=0;v<d;v++)y.push(e.dataToPoint(a[v]));else{y[0]=e.dataToPoint(a[0]),y[1]=e.dataToPoint(a[1]);var m=g.get([\"lineStyle\",\"curveness\"]);+m&&(y[2]=[(y[0][0]+y[1][0])/2-(y[0][1]-y[1][1])*m,(y[0][1]+y[1][1])/2-(y[1][0]-y[0][0])*m])}o.setItemLayout(h,y)}}}}}},UP=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._updateLineDraw(i,t),o=t.get(\"zlevel\"),a=t.get([\"effect\",\"trailLength\"]),s=n.getZr(),l=\"svg\"===s.painter.getType();l||s.painter.getLayer(o).clear(!0),null==this._lastZlevel||l||s.configLayer(this._lastZlevel,{motionBlur:!1}),this._showEffect(t)&&a>0&&(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<i;r++)e[r]=e[r]||[],e[r][0]=this._flatCoords[n+2*r],e[r][1]=this._flatCoords[n+2*r+1];return i}var o=this._getCoordsFromItemModel(t);for(r=0;r<o.length;r++)e[r]=e[r]||[],e[r][0]=o[r][0],e[r][1]=o[r][1];return o.length},e.prototype._processFlatCoordsArray=function(t){var e=0;if(this._flatCoords&&(e=this._flatCoords.length),j(t[0])){for(var n=t.length,i=new ZP(n),r=new jP(n),o=0,a=0,s=0,l=0;l<n;){s++;var u=t[l++];i[a++]=o+e,i[a++]=u;for(var h=0;h<u;h++){var c=t[l++],p=t[l++];r[o++]=c,r[o++]=p}}return{flatCoordsOffset:new Uint32Array(i.buffer,0,a),flatCoords:r,count:s}}return{flatCoordsOffset:null,flatCoords:null,count:t.length}},e.prototype.getInitialData=function(t,e){var n=new lx([\"value\"],this);return n.hasItemOption=!1,n.initData(t.data,[],(function(t,e,i,r){if(t instanceof Array)return NaN;n.hasItemOption=!0;var o=t.value;return null!=o?o instanceof Array?o[r]:o:void 0})),n},e.prototype.formatTooltip=function(t,e,n){var i=this.getData().getItemModel(t),r=i.get(\"name\");if(r)return r;var o=i.get(\"fromName\"),a=i.get(\"toName\"),s=[];return null!=o&&s.push(o),null!=a&&s.push(a),ng(\"nameValue\",{name:s.join(\" > \")})},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;d<p;++d){var f=t[d],g=f[0],y=f[1],v=i(f[2]);c.globalAlpha=v,c.drawImage(a,g-u,y-u)}if(!h.width||!h.height)return h;for(var m=c.getImageData(0,0,h.width,h.height),x=m.data,_=0,b=x.length,w=this.minOpacity,S=this.maxOpacity-w;_<b;){v=x[_+3]/256;var M=4*Math.floor(255*v);if(v>0){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;I<i;I++){var T=void 0,C=f.getItemVisual(I,\"style\");if(h){var D=f.get(M[0],I),A=f.get(M[1],I);if(isNaN(f.get(M[2],I))||isNaN(D)||isNaN(A)||D<s[0]||D>s[1]||A<l[0]||A>l[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<r;i++)if((a=e[i].interval)[0]<=t&&t<=a[1]){o=i;break}if(i===r)for(i=o-1;i>=0;i--){var a;if((a=e[i].interval)[0]<=t&&t<=a[1]){o=i;break}}return i>=0&&i<r&&n[i]}}(m,n.getPieceList(),n.option.selected);s.update(v,f,g,r.color.getNormalizer(),{inRange:r.color.getColorMapper(),outOfRange:o.color.getColorMapper()},x);var _=new ks({style:{width:f,height:g,x:h,y:c,image:s.canvas},silent:!0});this.group.add(_)},e.type=\"heatmap\",e}(kg),nO=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,{generateCoord:\"value\"})},e.prototype.preventIncremental=function(){var t=xd.get(this.get(\"coordinateSystem\"));if(t&&t.dimensions)return\"lng\"===t.dimensions[0]&&\"lat\"===t.dimensions[1]},e.type=\"series.heatmap\",e.dependencies=[\"grid\",\"geo\",\"calendar\"],e.defaultOption={coordinateSystem:\"cartesian2d\",z:2,geoIndex:0,blurSize:30,pointSize:20,maxOpacity:1,minOpacity:0,select:{itemStyle:{borderColor:\"#212121\"}}},e}(mg);var iO=[\"itemStyle\",\"borderWidth\"],rO=[{xy:\"x\",wh:\"width\",index:0,posDesc:[\"left\",\"right\"]},{xy:\"y\",wh:\"height\",index:1,posDesc:[\"top\",\"bottom\"]}],oO=new _u,aO=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,r=t.getData(),o=this._data,a=t.coordinateSystem,s=a.getBaseAxis().isHorizontal(),l=a.master.getRect(),u={ecSize:{width:n.getWidth(),height:n.getHeight()},seriesModel:t,coordSys:a,coordSysExtent:[[l.x,l.x+l.width],[l.y,l.y+l.height]],isHorizontal:s,valueDim:rO[+s],categoryDim:rO[1-+s]};return r.diff(o).add((function(t){if(r.hasValue(t)){var e=fO(r,t),n=sO(r,t,e,u),o=vO(r,u,n);r.setItemGraphicEl(t,o),i.add(o),wO(o,u,n)}})).update((function(t,e){var n=o.getItemGraphicEl(e);if(r.hasValue(t)){var a=fO(r,t),s=sO(r,t,a,u),l=xO(r,s);n&&l!==n.__pictorialShapeStr&&(i.remove(n),r.setItemGraphicEl(t,null),n=null),n?function(t,e,n){var i=n.animationModel,r=n.dataIndex,o=t.__pictorialBundle;fh(o,{x:n.bundlePosition[0],y:n.bundlePosition[1]},i,r),n.symbolRepeat?hO(t,e,n,!0):cO(t,e,n,!0);pO(t,n,!0),dO(t,e,n,!0)}(n,u,s):n=vO(r,u,s,!0),r.setItemGraphicEl(t,n),n.__pictorialSymbolMeta=s,i.add(n),wO(n,u,s)}else i.remove(n)})).remove((function(t){var e=o.getItemGraphicEl(t);e&&mO(o,t,e.__pictorialSymbolMeta.animationModel,e)})).execute(),this._data=r,this.group},e.prototype.remove=function(t,e){var n=this.group,i=this._data;t.get(\"animation\")?i&&i.eachItemGraphicEl((function(e){mO(i,Qs(e).dataIndex,t,e)})):n.removeAll()},e.type=\"pictorialBar\",e}(kg);function sO(t,e,n,i){var r=t.getItemLayout(e),o=n.get(\"symbolRepeat\"),a=n.get(\"symbolClip\"),s=n.get(\"symbolPosition\")||\"start\",l=(n.get(\"symbolRotate\")||0)*Math.PI/180||0,u=n.get(\"symbolPatternSize\")||2,h=n.isAnimationEnabled(),c={dataIndex:e,layout:r,itemModel:n,symbolType:t.getItemVisual(e,\"symbol\")||\"circle\",style:t.getItemVisual(e,\"style\"),symbolClip:a,symbolRepeat:o,symbolRepeatDirection:n.get(\"symbolRepeatDirection\"),symbolPatternSize:u,rotation:l,animationModel:h?n:null,hoverScale:h&&n.get([\"emphasis\",\"scale\"]),z2:n.getShallow(\"z\",!0)||0};!function(t,e,n,i,r){var o,a=i.valueDim,s=t.get(\"symbolBoundingData\"),l=i.coordSys.getOtherAxis(i.coordSys.getBaseAxis()),u=l.toGlobalCoord(l.dataToCoord(0)),h=1-+(n[a.wh]<=0);if(Y(s)){var c=[lO(l,s[0])-u,lO(l,s[1])-u];c[1]<c[0]&&c.reverse(),o=c[h]}else o=null!=s?lO(l,s)-u:e?i.coordSysExtent[a.index][h]-u:n[a.wh];r.boundingLength=o,e&&(r.repeatCutLength=n[a.wh]);r.pxSign=o>0?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,h<u?bO(t,null,f(h),n,i):bO(t,null,{scaleX:0,scaleY:0},n,i,(function(){r.remove(t)})),h++}));h<u;h++){var p=uO(n);p.__pictorialAnimationIndex=h,p.__pictorialRepeatTimes=u,r.add(p);var d=f(h);bO(p,{x:d.x,y:d.y,scaleX:0,scaleY:0},{scaleX:d.scaleX,scaleY:d.scaleY,rotation:d.rotation},n,i)}function f(t){var e=s.slice(),i=n.pxSign,r=t;return(\"start\"===n.symbolRepeatDirection?i>0: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;g<f.length;g++){var y=i.getItemLayout(f[g]),v=y.x,m=y.y0,x=y.y;c.push(v,m),d.push(v,m+x),u=i.getItemVisual(f[g],\"style\")}var _=i.getItemLayout(f[0]),b=t.getModel(\"label\").get(\"margin\"),w=t.getModel(\"emphasis\");if(\"add\"===e){var S=p[n]=new zr;h=new _S({shape:{points:c,stackedOnPoints:d,smooth:.4,stackedOnSmooth:.4,smoothConstraint:!1},z2:0}),S.add(h),o.add(S),t.isAnimationEnabled()&&h.setClipPath(function(t,e,n){var i=new zs({shape:{x:t.x-10,y:t.y-10,width:0,height:t.height+20}});return gh(i,{shape:{x:t.x-50,width:t.width+100,height:t.height+20}},e,n),i}(h.getBoundingRect(),t,(function(){h.removeClipPath()})))}else{S=l[s];h=S.childAt(0),o.add(S),p[n]=S,fh(h,{shape:{points:c,stackedOnPoints:d}},t),_h(h)}tc(h,ec(t),{labelDataIndex:f[g-1],defaultText:i.getName(f[g-1]),inheritColor:u.fill},{normal:{verticalAlign:\"middle\"}}),h.setTextConfig({position:null,local:!0});var M=h.getTextContent();M&&(M.x=_.x-b,M.y=_.y0+_.y/2),h.useStyle(u),i.setItemGraphicEl(n,h),jl(h,t),Yl(h,w.get(\"focus\"),w.get(\"blurScope\"),w.get(\"disabled\"))}else o.remove(l[n])}c.add(W(d,this,\"add\")).update(W(d,this,\"update\")).remove(W(d,this,\"remove\")).execute(),this._layersSeries=a,this._layers=p},e.type=\"themeRiver\",e}(kg);var TO=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))},e.prototype.fixData=function(t){var e=t.length,n={},i=Go(t,(function(t){return n.hasOwnProperty(t[0]+\"\")||(n[t[0]+\"\"]=-1),t[2]})),r=[];i.buckets.each((function(t,e){r.push({name:e,dataList:t})}));for(var o=r.length,a=0;a<o;++a){for(var s=r[a].name,l=0;l<r[a].dataList.length;++l){var u=r[a].dataList[l][0]+\"\";n[u]=a}for(var u in n)n.hasOwnProperty(u)&&n[u]!==a&&(n[u]=a,t[e]=[u,0,s],e++)}return t},e.prototype.getInitialData=function(t,e){for(var n=this.getReferringComponents(\"singleAxis\",zo).models[0].get(\"type\"),i=B(t.data,(function(t){return void 0!==t[2]})),r=this.fixData(i||[]),o=[],a=this.nameMap=yt(),s=0,l=0;l<r.length;++l)o.push(r[l][2]),a.get(r[l][2])||(a.set(r[l][2],s),s++);var u=ux(r,{coordDimensions:[\"single\"],dimensionsDefine:[{name:\"time\",type:Gm(n)},{name:\"value\",type:\"float\"},{name:\"name\",type:\"ordinal\"}],encodeDefine:{single:0,value:1,itemName:2}}).dimensions,h=new lx(u,this);return h.initData(r),h},e.prototype.getLayerSeries=function(){for(var t=this.getData(),e=t.count(),n=[],i=0;i<e;++i)n[i]=i;var r=t.mapDimension(\"single\"),o=Go(n,(function(e){return t.get(\"name\",e)})),a=[];return o.buckets.each((function(e,n){e.sort((function(e,n){return t.get(r,e)-t.get(r,n)})),a.push({name:n,indices:e})})),a},e.prototype.getAxisTooltipData=function(t,e,n){Y(t)||(t=t?[t]:[]);for(var i,r=this.getData(),o=this.getLayerSeries(),a=[],s=o.length,l=0;l<s;++l){for(var u=Number.MAX_VALUE,h=-1,c=o[l].indices.length,p=0;p<c;++p){var d=r.get(t[0],o[l].indices[p]),f=Math.abs(d-e);f<=u&&(i=d,u=f,h=o[l].indices[p])}a.push(h)}return{dataIndices:a,nestestValue:i}},e.prototype.formatTooltip=function(t,e,n){var i=this.getData();return ng(\"nameValue\",{name:i.getName(t),value:i.get(i.mapDimension(\"value\"),t)})},e.type=\"series.themeRiver\",e.dependencies=[\"singleAxis\"],e.defaultOption={z:2,colorBy:\"data\",coordinateSystem:\"singleAxis\",boundaryGap:[\"10%\",\"10%\"],singleAxisIndex:0,animationEasing:\"linear\",label:{margin:4,show:!0,position:\"left\",fontSize:11},emphasis:{label:{show:!0}}},e}(mg);function CO(t,e){t.eachSeriesByType(\"themeRiver\",(function(t){var e=t.getData(),n=t.coordinateSystem,i={},r=n.getRect();i.rect=r;var o=t.get(\"boundaryGap\"),a=n.getAxis();(i.boundaryGap=o,\"horizontal\"===a.orient)?(o[0]=Ur(o[0],r.height),o[1]=Ur(o[1],r.height),DO(e,t,r.height-o[0]-o[1])):(o[0]=Ur(o[0],r.width),o[1]=Ur(o[1],r.width),DO(e,t,r.width-o[0]-o[1]));e.setLayout(\"layoutInfo\",i)}))}function DO(t,e,n){if(t.count())for(var i,r=e.coordinateSystem,o=e.getLayerSeries(),a=t.mapDimension(\"single\"),s=t.mapDimension(\"value\"),l=z(o,(function(e){return z(e.indices,(function(e){var n=r.dataToPoint(t.get(a,e));return n[1]=t.get(s,e),n}))})),u=function(t){for(var e=t.length,n=t[0].length,i=[],r=[],o=0,a=0;a<n;++a){for(var s=0,l=0;l<e;++l)s+=t[l][a][1];s>o&&(o=s),i.push(s)}for(var u=0;u<n;++u)r[u]=(o-i[u])/2;o=0;for(var h=0;h<n;++h){var c=i[h]+r[h];c>o&&(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;f<d;++f){i=h[f]*c,t.setItemLayout(o[0].indices[f],{layerIndex:0,x:l[0][f][0],y0:i,y:l[0][f][1]*c});for(var g=1;g<p;++g)i+=l[g-1][f][1]*c,t.setItemLayout(o[g].indices[f],{layerIndex:g,x:l[g][f][0],y0:i,y:l[g][f][1]*c})}}var AO=function(t){function e(e,n,i,r){var o=t.call(this)||this;o.z2=2,o.textConfig={inside:!0},Qs(o).seriesIndex=n.seriesIndex;var a=new Fs({z2:4,silent:e.getModel().get([\"label\",\"silent\"])});return o.setTextContent(a),o.updateData(!0,e,n,i,r),o}return n(e,t),e.prototype.updateData=function(t,e,n,i,r){this.node=e,e.piece=this,n=n||this._seriesModel,i=i||this._ecModel;var o=this;Qs(o).dataIndex=e.dataIndex;var a=e.getModel(),s=a.getModel(\"emphasis\"),l=e.getLayout(),u=A({},l);u.label=null;var h=e.getVisual(\"style\");h.lineJoin=\"bevel\";var c=e.getVisual(\"decal\");c&&(h.decal=gv(c,r));var p=US(a.getModel(\"itemStyle\"),u,!0);A(u,p),E(ol,(function(t){var e=o.ensureState(t),n=a.getModel([t,\"itemStyle\"]);e.style=n.getItemStyle();var i=US(n,u);i&&(e.shape=i)})),t?(o.setShape(u),o.shape.r=l.r0,gh(o,{shape:{r:l.r}},n,e.dataIndex)):(fh(o,{shape:u},n),_h(o)),o.useStyle(h),this._updateLabel(n);var d=a.getShallow(\"cursor\");d&&o.attr(\"cursor\",d),this._seriesModel=n||this._seriesModel,this._ecModel=i||this._ecModel;var f=s.get(\"focus\");Yl(this,\"ancestor\"===f?e.getAncestorsIndices():\"descendant\"===f?e.getDescendantIndices():f,s.get(\"blurScope\"),s.get(\"disabled\"))},e.prototype._updateLabel=function(t){var e=this,n=this.node.getModel(),i=n.getModel(\"label\"),r=this.node.getLayout(),o=r.endAngle-r.startAngle,a=(r.startAngle+r.endAngle)/2,s=Math.cos(a),l=Math.sin(a),u=this,h=u.getTextContent(),c=this.node.dataIndex,p=i.get(\"minAngle\")/180*Math.PI,d=i.get(\"show\")&&!(null!=p&&Math.abs(o)<p);function f(t,e){var n=t.get(e);return null==n?i.get(e):n}h.ignore=!d,E(al,(function(i){var p=\"normal\"===i?n.getModel(\"label\"):n.getModel([i,\"label\"]),d=\"normal\"===i,g=d?h:h.ensureState(i),y=t.getFormattedLabel(c,i);d&&(y=y||e.node.name),g.style=nc(p,{},null,\"normal\"!==i,!0),y&&(g.style.text=y);var v=p.get(\"show\");null==v||d||(g.ignore=!v);var m,x=f(p,\"position\"),_=d?u:u.states[i],b=_.style.fill;_.textConfig={outsideFill:\"inherit\"===p.get(\"color\")?b:null,inside:\"outside\"!==x};var w=f(p,\"distance\")||0,S=f(p,\"align\");\"outside\"===x?(m=r.r+w,S=a>Math.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;o<p&&(o=p),i=n+I*o;var h=e.depth-g-(_?-1:1),c=u+w*h,f=u+w*(h+1),y=t.getLevelModel(e);if(y){var v=y.get(\"r0\",!0),b=y.get(\"r\",!0),C=y.get(\"radius\",!0);null!=C&&(v=C[0],b=C[1]),null!=v&&(c=Ur(v,a/2)),null!=b&&(f=Ur(b,a/2))}e.setLayout({angle:o,startAngle:n,endAngle:i,clockwise:S,cx:s,cy:l,r0:c,r:f})}if(e.children&&e.children.length){var D=0;E(e.children,(function(t){D+=T(t,n+D)}))}return i-n}};if(_){var C=u,D=u+w,A=2*Math.PI;d.setLayout({angle:A,startAngle:c,endAngle:c+A,clockwise:S,cx:s,cy:l,r0:C,r:D})}T(f,c)}))}function zO(t,e){var n=t.children||[];t.children=function(t,e){if(X(e)){var n=z(t,(function(t,e){var n=t.getValue();return{params:{depth:t.depth,height:t.height,dataIndex:t.dataIndex,getValue:function(){return n}},index:e}}));return n.sort((function(t,n){return e(t.params,n.params)})),z(n,(function(e){return t[e.index]}))}var i=\"asc\"===e;return t.sort((function(t,e){var n=(t.getValue()-e.getValue())*(i?1:-1);return 0===n?(t.dataIndex-e.dataIndex)*(i?-1:1):n}))}(n,e),n.length&&E(t.children,(function(t){zO(t,e)}))}function VO(t){var e={};t.eachSeriesByType(\"sunburst\",(function(t){var n=t.getData(),i=n.tree;i.eachNode((function(r){var o=r.getModel().getModel(\"itemStyle\").getItemStyle();o.fill||(o.fill=function(t,n,i){for(var r=t;r&&r.depth>1;)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<QO.length;i++){var r=QO[i],o=JO[r],a=e[r];a&&(n[o[0]]=a[0],n[o[1]]=a[1])}for(i=0;i<yr.length;i++){var s=yr[i];null!=e[s]&&(n[s]=e[s])}}(0,e,c),cR(\"shape\",e,c),cR(\"extra\",e,c),!o&&s&&(function(t,e,n){for(var i=e.transition,r=aR(i)?yr:bo(i||[]),o=0;o<r.length;o++){var a=r[o];if(\"style\"!==a&&\"shape\"!==a&&\"extra\"!==a){var s=t[a];0,n[a]=s}}}(t,e,h),hR(\"shape\",t,e,h),hR(\"extra\",t,e,h),function(t,e,n,i){if(!n)return;var r,o=t.style;if(o){var a=n.transition,s=e.transition;if(a&&!aR(a)){var l=bo(a);!r&&(r=i.style={});for(var u=0;u<l.length;u++){var h=o[f=l[u]];r[f]=h}}else if(t.getAnimationStyleProps&&(aR(s)||aR(a)||P(s,\"style\")>=0)){var c=t.getAnimationStyleProps(),p=c?c.style:null;if(p){!r&&(r=i.style={});var d=G(n);for(u=0;u<d.length;u++){var f;if(p[f=d[u]]){h=o[f];r[f]=h}}}}}}(t,e,u,h)),c.style=u,function(t,e,n){var i=e.style;if(!t.isGroup&&i){if(n){t.useStyle({});for(var r=t.animators,o=0;o<r.length;o++){var a=r[o];\"style\"===a.targetName&&a.changeTarget(t.style)}}t.setStyle(i)}e&&(e.style=null,e&&t.attr(e),e.style=i)}(t,c,a),function(t,e){_t(e,\"silent\")&&(t.silent=e.silent),_t(e,\"ignore\")&&(t.ignore=e.ignore),t instanceof Sa&&_t(e,\"invisible\")&&(t.invisible=e.invisible);t instanceof Is&&_t(e,\"autoBatch\")&&(t.autoBatch=e.autoBatch)}(t,e),s)if(o){var p={};E(tR,(function(t){var n=t?e[t]:e;n&&n.enterFrom&&(t&&(p[t]=p[t]||{}),A(t?p[t]:p,n.enterFrom))}));var d=nR(\"enter\",t,e,n,r);d.duration>0&&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<tR.length;i++){var r=tR[i],o=r?e[r]:e;o&&o.leaveTo&&(n||(n=eR(t).leaveToProps={}),r&&(n[r]=n[r]||{}),A(r?n[r]:n,o.leaveTo))}}function oR(t,e,n,i){if(t){var r=t.parent,o=eR(t).leaveToProps;if(o){var a=nR(\"update\",t,e,n,0);a.done=function(){r.remove(t),i&&i()},t.animateTo(o,a)}else r.remove(t),i&&i()}}function aR(t){return\"all\"===t}var sR={},lR={setTransform:function(t,e){return sR.el[t]=e,this},getTransform:function(t){return sR.el[t]},setShape:function(t,e){var n=sR.el;return(n.shape||(n.shape={}))[t]=e,n.dirtyShape&&n.dirtyShape(),this},getShape:function(t){var e=sR.el.shape;if(e)return e[t]},setStyle:function(t,e){var n=sR.el,i=n.style;return i&&(i[t]=e,n.dirtyStyle&&n.dirtyStyle()),this},getStyle:function(t){var e=sR.el.style;if(e)return e[t]},setExtra:function(t,e){return(sR.el.extra||(sR.el.extra={}))[t]=e,this},getExtra:function(t){var e=sR.el.extra;if(e)return e[t]}};function uR(){var t=this,e=t.el;if(e){var n=eR(e).userDuring,i=t.userDuring;n===i?(sR.el=e,i(lR)):t.el=t.userDuring=null}}function hR(t,e,n,i){var r=n[t];if(r){var o,a=e[t];if(a){var s=n.transition,l=r.transition;if(l)if(!o&&(o=i[t]={}),aR(l))A(o,a);else for(var u=bo(l),h=0;h<u.length;h++){var c=a[d=u[h]];o[d]=c}else if(aR(s)||P(s,t)>=0){!o&&(o=i[t]={});var p=G(a);for(h=0;h<p.length;h++){var d;c=a[d=p[h]];pR(r[d],c)&&(o[d]=c)}}}}}function cR(t,e,n){var i=e[t];if(i)for(var r=n[t]={},o=G(i),a=0;a<o.length;a++){var s=o[a];r[s]=ki(i[s])}}function pR(t,e){return N(t)?t!==e:null!=t&&isFinite(t)}var dR=Oo(),fR=[\"percent\",\"easing\",\"shape\",\"style\",\"extra\"];function gR(t){t.stopAnimation(\"keyframe\"),t.attr(dR(t))}function yR(t,e,n){if(n.isAnimationEnabled()&&e)if(Y(e))E(e,(function(e){yR(t,e,n)}));else{var i=e.keyframes,r=e.duration;if(n&&null==r){var o=ph(\"enter\",n,0);r=o&&o.duration}if(i&&r){var a=dR(t);E(tR,(function(n){if(!n||t[n]){var o;i.sort((function(t,e){return t.percent-e.percent})),E(i,(function(i){var s=t.animators,l=n?i[n]:i;if(l){var u=G(l);if(n||(u=B(u,(function(t){return P(fR,t)<0}))),u.length){o||((o=t.animate(n,e.loop,!0)).scope=\"keyframe\");for(var h=0;h<s.length;h++)s[h]!==o&&s[h].targetName===o.targetName&&s[h].stopTracks(u);n&&(a[n]=a[n]||{});var c=n?a[n]:a;E(u,(function(e){c[e]=((n?t[n]:t)||{})[e]})),o.whenWithKeys(r*i.percent,l,u,i.easing)}}})),o&&o.delay(e.delay||0).duration(r).start(e.easing)}}))}}}var vR=\"emphasis\",mR=\"normal\",xR=\"blur\",_R=\"select\",bR=[mR,vR,xR,_R],wR={normal:[\"itemStyle\"],emphasis:[vR,\"itemStyle\"],blur:[xR,\"itemStyle\"],select:[_R,\"itemStyle\"]},SR={normal:[\"label\"],emphasis:[vR,\"label\"],blur:[xR,\"label\"],select:[_R,\"label\"]},MR=[\"x\",\"y\"],IR={normal:{},emphasis:{},blur:{},select:{}},TR={cartesian2d:function(t){var e=t.master.getRect();return{coordSys:{type:\"cartesian2d\",x:e.x,y:e.y,width:e.width,height:e.height},api:{coord:function(e){return t.dataToPoint(e)},size:W(HO,t)}}},geo:function(t){var e=t.getBoundingRect();return{coordSys:{type:\"geo\",x:e.x,y:e.y,width:e.width,height:e.height,zoom:t.getZoom()},api:{coord:function(e){return t.dataToPoint(e)},size:W(YO,t)}}},single:function(t){var e=t.getRect();return{coordSys:{type:\"singleAxis\",x:e.x,y:e.y,width:e.width,height:e.height},api:{coord:function(e){return t.dataToPoint(e)},size:W(XO,t)}}},polar:function(t){var e=t.getRadiusAxis(),n=t.getAngleAxis(),i=e.getExtent();return i[0]>i[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<t.end;u++){var h=ER(null,null,u,a(u,r),e,this.group,o);h&&(h.traverse(l),s.push(h))}},e.prototype.eachRendered=function(t){qh(this._progressiveEls||this.group,t)},e.prototype.filterForExposedEvent=function(t,e,n,i){var r=e.element;if(null==r||n.name===r)return!0;for(;(n=n.__hostTarget||n.parent)&&n!==this.group;)if(n.name===r)return!0;return!1},e.type=\"custom\",e}(kg);function kR(t){var e,n=t.type;if(\"path\"===n){var i=t.shape,r=null!=i.width&&null!=i.height?{x:i.x||0,y:i.y||0,width:i.width,height:i.height}:null,o=UR(i);e=Ah(o,null,r,i.layout||\"center\"),GO(e).customPathData=o}else if(\"image\"===n)e=new ks({}),GO(e).customImagePath=t.style.image;else if(\"text\"===n)e=new Fs({});else if(\"group\"===n)e=new zr;else{if(\"compoundPath\"===n)throw new Error('\"compoundPath\" is not supported yet.');var a=Dh(n);if(!a){var s=\"\";0,vo(s)}e=new a}return GO(e).customGraphicType=n,e.name=t.name,e.z2EmphasisLift=1,e.z2SelectLift=1,e}function LR(t,e,n,i,r,o,a){gR(e);var s=r&&r.normal.cfg;s&&e.setTextConfig(s),i&&null==i.transition&&(i.transition=MR);var l=i&&i.style;if(l){if(\"text\"===e.type){var u=l;_t(u,\"textFill\")&&(u.fill=u.textFill),_t(u,\"textStroke\")&&(u.stroke=u.textStroke)}var h=void 0,c=CR(e)?l.decal:null;t&&c&&(c.dirty=!0,h=gv(c,t)),l.__decalPattern=h}DR(e)&&(l&&(h=l.__decalPattern)&&(l.decal=h));iR(e,i,o,{dataIndex:n,isInit:a,clearStyle:!0}),yR(e,i.keyframeAnimation,o)}function PR(t,e,n,i,r){var o=e.isGroup?null:e,a=r&&r[t].cfg;if(o){var s=o.ensureState(t);if(!1===i){var l=o.getState(t);l&&(l.style=null)}else s.style=i||null;a&&(s.textConfig=a),Cl(o)}}function OR(t,e,n){var i=n===mR,r=i?e:FR(e,n),o=r?r.z2:null;null!=o&&((i?t:t.ensureState(n)).z2=o||0)}function RR(t,e,n,i){var r=t.get(\"renderItem\"),o=t.coordinateSystem,a={};o&&(a=o.prepareCustoms?o.prepareCustoms(o):TR[o.type](o));for(var s,l,u=k({getWidth:i.getWidth,getHeight:i.getHeight,getZr:i.getZr,getDevicePixelRatio:i.getDevicePixelRatio,value:function(t,n){return null==n&&(n=s),e.getStore().get(e.getDimensionIndex(t||0),n)},style:function(n,i){0;null==i&&(i=s);var r=e.getItemVisual(i,\"style\"),o=r&&r.fill,a=r&&r.opacity,l=m(i,mR).getItemStyle();null!=o&&(l.fill=o),null!=a&&(l.opacity=a);var u={inheritColor:U(o)?o:\"#000\"},h=x(i,mR),c=nc(h,null,u,!1,!0);c.text=h.getShallow(\"show\")?rt(t.getFormattedLabel(i,mR),iS(e,i)):null;var p=ic(h,u,!1);return b(n,l),l=KO(l,c,p),n&&_(l,n),l.legacy=!0,l},ordinalRawValue:function(t,n){null==n&&(n=s),t=t||0;var i=e.getDimensionInfo(t);if(!i){var r=e.getDimensionIndex(t);return r>=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<t.count;o++)e.push(k({bandWidth:r,axisKey:i,stackId:zx+o},t));var a=Wx(e),s=[];for(o=0;o<t.count;o++){var l=a[i][zx+o];l.offsetCenter=l.offset+l.width/2,s.push(l)}return s}}(k({axis:o.getBaseAxis()},t))}},currentSeriesIndices:function(){return n.getCurrentSeriesIndices()},font:function(t){return lc(t,n)}},a.api||{}),h={context:{},seriesId:t.id,seriesName:t.name,seriesIndex:t.seriesIndex,coordSys:a.coordSys,dataInsideLength:e.count(),encode:NR(t.getData())},c={},p={},d={},f={},g=0;g<bR.length;g++){var y=bR[g];d[y]=t.getModel(wR[y]),f[y]=t.getModel(SR[y])}function v(t){return t===s?l||(l=e.getItemModel(t)):e.getItemModel(t)}function m(t,n){return e.hasItemOption?t===s?c[n]||(c[n]=v(t).getModel(wR[n])):v(t).getModel(wR[n]):d[n]}function x(t,n){return e.hasItemOption?t===s?p[n]||(p[n]=v(t).getModel(SR[n])):v(t).getModel(SR[n]):f[n]}return function(t,n){return s=t,l=null,c={},p={},r&&r(k({dataIndexInside:t,dataIndex:e.getRawIndex(t),actionType:n?n.type:null},h),u)};function _(t,e){for(var n in e)_t(e,n)&&(t[n]=e[n])}function b(t,e){t&&(t.textFill&&(e.textFill=t.textFill),t.textPosition&&(e.textPosition=t.textPosition))}}function NR(t){var e={};return E(t.dimensions,(function(n){var i=t.getDimensionInfo(n);if(!i.isExtraCoord){var r=i.coordDim;(e[r]=e[r]||[])[i.coordDimIndex]=t.getDimensionIndex(n)}})),e}function ER(t,e,n,i,r,o,a){if(i){var s=zR(t,e,n,i,r,o);return s&&a.setItemGraphicEl(n,s),s&&Yl(s,i.focus,i.blurScope,i.emphasisDisabled),s}o.remove(e)}function zR(t,e,n,i,r,o){var a=-1,s=e;e&&VR(e,i,r)&&(a=P(o.childrenRef(),e),e=null);var l,u,h=!e,c=e;c?c.clearStates():(c=kR(i),s&&(l=s,(u=c).copyTransform(l),DR(u)&&DR(l)&&(u.setStyle(l.style),u.z=l.z,u.z2=l.z2,u.zlevel=l.zlevel,u.invisible=l.invisible,u.ignore=l.ignore,CR(u)&&CR(l)&&u.setShape(l.shape)))),!1===i.morph?c.disableMorphing=!0:c.disableMorphing&&(c.disableMorphing=!1),IR.normal.cfg=IR.normal.conOpt=IR.emphasis.cfg=IR.emphasis.conOpt=IR.blur.cfg=IR.blur.conOpt=IR.select.cfg=IR.select.conOpt=null,IR.isLegacy=!1,function(t,e,n,i,r,o){if(t.isGroup)return;BR(n,null,o),BR(n,vR,o);var a=o.normal.conOpt,s=o.emphasis.conOpt,l=o.blur.conOpt,u=o.select.conOpt;if(null!=a||null!=s||null!=u||null!=l){var h=t.getTextContent();if(!1===a)h&&t.removeTextContent();else{a=o.normal.conOpt=a||{type:\"text\"},h?h.clearStates():(h=kR(a),t.setTextContent(h)),LR(null,h,e,a,null,i,r);for(var c=a&&a.style,p=0;p<bR.length;p++){var d=bR[p];if(d!==mR){var f=o[d].conOpt;PR(d,h,0,GR(a,f,d),null)}}c?h.dirty():h.markRedraw()}}}(c,n,i,r,h,IR),function(t,e,n,i,r){var o=n.clipPath;if(!1===o)t&&t.getClipPath()&&t.removeClipPath();else if(o){var a=t.getClipPath();a&&VR(a,o,i)&&(a=null),a||(a=kR(o),t.setClipPath(a)),LR(null,a,e,o,null,i,r)}}(c,n,i,r,h),LR(t,c,n,i,IR,r,h),_t(i,\"info\")&&(GO(c).info=i.info);for(var p=0;p<bR.length;p++){var d=bR[p];if(d!==mR){var f=FR(i,d);PR(d,c,0,GR(i,f,d),IR)}}return function(t,e,n){if(!t.isGroup){var i=t,r=n.currentZ,o=n.currentZLevel;i.z=r,i.zlevel=o;var a=e.z2;null!=a&&(i.z2=a||0);for(var s=0;s<bR.length;s++)OR(i,e,bR[s])}}(c,i,r),\"group\"===i.type&&function(t,e,n,i,r){var o=i.children,a=o?o.length:0,s=i.$mergeChildren,l=\"byName\"===s||i.diffChildrenByName,u=!1===s;if(!a&&!l&&!u)return;if(l)return h={api:t,oldChildren:e.children()||[],newChildren:o||[],dataIndex:n,seriesModel:r,group:e},void new Vm(h.oldChildren,h.newChildren,HR,HR,h).add(YR).update(YR).remove(XR).execute();var h;u&&e.removeAll();for(var c=0;c<a;c++){var p=o[c],d=e.childAt(c);p?(null==p.ignore&&(p.ignore=!1),zR(t,d,n,p,r,e)):d.ignore=!0}for(var f=e.childCount()-1;f>=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<a||d>=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=l<o?1:-1;l<o||l>a;)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;c<r.length;c++)for(var p=0;p<r[c].length;p++)h.push(new Zu({shape:ZN(n,[u,u+l],r[c][p].coord)}));t.add(Ph(h,{style:k(s.getModel(\"lineStyle\").getLineStyle(),k(a.getLineStyle(),{stroke:e.get([\"axisLine\",\"lineStyle\",\"color\"])}))}))}},axisLabel:function(t,e,n,i,r,o,a){var s=e.getCategories(!0),l=e.getModel(\"axisLabel\"),u=l.get(\"margin\"),h=e.get(\"triggerEvent\");E(a,(function(i,r){var a=l,c=i.tickValue,p=o[jN(n)],d=n.coordToPoint([p+u,i.coord]),f=n.cx,g=n.cy,y=Math.abs(d[0]-f)/p<.3?\"center\":d[0]>f?\"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<i.length;h++){var c=l++%s.length;u[c]=u[c]||[],u[c].push(new Zu({shape:ZN(n,o,i[h].coord)}))}for(h=0;h<u.length;h++)t.add(Ph(u[h],{style:k({stroke:s[h%s.length]},a.getLineStyle()),silent:!0,z:e.get(\"z\")}))},minorSplitLine:function(t,e,n,i,r,o){if(r.length){for(var a=e.getModel(\"minorSplitLine\").getModel(\"lineStyle\"),s=[],l=0;l<r.length;l++)for(var u=0;u<r[l].length;u++)s.push(new Zu({shape:ZN(n,o,r[l][u].coord)}));t.add(Ph(s,{style:a.getLineStyle(),silent:!0,z:e.get(\"z\")}))}},splitArea:function(t,e,n,i,r,o){if(i.length){var a=e.getModel(\"splitArea\").getModel(\"areaStyle\"),s=a.get(\"color\"),l=0;s=s instanceof Array?s:[s];for(var u=[],h=Math.PI/180,c=-i[0].coord*h,p=Math.min(o[0],o[1]),d=Math.max(o[0],o[1]),f=e.get(\"clockwise\"),g=1,y=i.length;g<=y;g++){var v=g===y?i[0].coord:i[g].coord,m=l++%s.length;u[m]=u[m]||[],u[m].push(new zu({shape:{cx:n.cx,cy:n.cy,r0:p,r:d,startAngle:c,endAngle:-v*h,clockwise:f},silent:!0})),c=-v*h}for(g=0;g<u.length;g++)t.add(Ph(u[g],{style:k({fill:s[g%s.length]},a.getAreaStyle()),silent:!0}))}}},JN=[\"axisLine\",\"axisTickLabel\",\"axisName\"],QN=[\"splitLine\",\"splitArea\",\"minorSplitLine\"],tE=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=this._axisGroup,i=this._axisGroup=new zr;this.group.add(i);var r=t.axis,o=r.polar,a=o.getAngleAxis(),s=r.getTicksCoords(),l=r.getMinorTicksCoords(),u=a.getExtent()[0],h=r.getExtent(),c=function(t,e,n){return{position:[t.cx,t.cy],rotation:n/180*Math.PI,labelDirection:-1,tickDirection:-1,nameDirection:1,labelRotate:e.getModel(\"axisLabel\").get(\"rotate\"),z2:1}}(o,t,u),p=new iI(t,c);E(JN,p.add,p),i.add(p.getGroup()),Fh(n,i,t),E(QN,(function(e){t.get([e,\"show\"])&&!r.scale.isBlank()&&eE[e](this.group,t,o,u,h,s,l)}),this)}},e.type=\"radiusAxis\",e}(yI),eE={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<o.length;h++){var c=l++%s.length;u[c]=u[c]||[],u[c].push(new _u({shape:{cx:n.cx,cy:n.cy,r:Math.max(o[h].coord,0)}}))}for(h=0;h<u.length;h++)t.add(Ph(u[h],{style:k({stroke:s[h%s.length],fill:null},a.getLineStyle()),silent:!0}))},minorSplitLine:function(t,e,n,i,r,o,a){if(a.length){for(var s=e.getModel(\"minorSplitLine\").getModel(\"lineStyle\"),l=[],u=0;u<a.length;u++)for(var h=0;h<a[u].length;h++)l.push(new _u({shape:{cx:n.cx,cy:n.cy,r:a[u][h].coord}}));t.add(Ph(l,{style:k({fill:null},s.getLineStyle()),silent:!0}))}},splitArea:function(t,e,n,i,r,o){if(o.length){var a=e.getModel(\"splitArea\").getModel(\"areaStyle\"),s=a.get(\"color\"),l=0;s=s instanceof Array?s:[s];for(var u=[],h=o[0].coord,c=1;c<o.length;c++){var p=l++%s.length;u[p]=u[p]||[],u[p].push(new zu({shape:{cx:n.cx,cy:n.cy,r0:h,r:o[c].coord,startAngle:0,endAngle:2*Math.PI},silent:!0})),h=o[c].coord}for(c=0;c<u.length;c++)t.add(Ph(u[c],{style:k({fill:s[c%s.length]},a.getAreaStyle()),silent:!0}))}}};function nE(t){return t.get(\"stack\")||\"__ec_stack_\"+t.seriesIndex}function iE(t,e){return e.dim+t.model.componentIndex}function rE(t,e,n){var i={},r=function(t){var e={};E(t,(function(t,n){var i=t.getData(),r=t.coordinateSystem,o=r.getBaseAxis(),a=iE(r,o),s=o.getExtent(),l=\"category\"===o.type?o.getBandWidth():Math.abs(s[1]-s[0])/i.count(),u=e[a]||{bandWidth:l,remainedWidth:l,autoWidthCount:0,categoryGap:\"20%\",gap:\"30%\",stacks:{}},h=u.stacks;e[a]=u;var c=nE(t);h[c]||u.autoWidthCount++,h[c]=h[c]||{width:0,maxWidth:0};var p=Ur(t.get(\"barWidth\"),l),d=Ur(t.get(\"barMaxWidth\"),l),f=t.get(\"barGap\"),g=t.get(\"barCategoryGap\");p&&!h[c].width&&(p=Math.min(u.remainedWidth,p),h[c].width=p,u.remainedWidth-=p),d&&(h[c].maxWidth=d),null!=f&&(u.gap=f),null!=g&&(u.categoryGap=g)}));var n={};return E(e,(function(t,e){n[e]={};var i=t.stacks,r=t.bandWidth,o=Ur(t.categoryGap,r),a=Ur(t.gap,1),s=t.remainedWidth,l=t.autoWidthCount,u=(s-o)/(l+(l-1)*a);u=Math.max(u,0),E(i,(function(t,e){var n=t.maxWidth;n&&n<u&&(n=Math.min(n,s),t.width&&(n=Math.min(n,t.width)),s-=n,t.width=n,l--)})),u=(s-o)/(l+(l-1)*a),u=Math.max(u,0);var h,c=0;E(i,(function(t,e){t.width||(t.width=u),h=t,c+=t.width*(1+a)})),h&&(c-=h.width*a);var p=-c/2;E(i,(function(t,i){n[e][i]=n[e][i]||{offset:p,width:t.width},p+=t.width*(1+a)}))})),n}(B(e.getSeriesByType(t),(function(t){return!e.isSeriesFiltered(t)&&t.coordinateSystem&&\"polar\"===t.coordinateSystem.type})));e.eachSeriesByType(t,(function(t){if(\"polar\"===t.coordinateSystem.type){var e=t.getData(),n=t.coordinateSystem,o=n.getBaseAxis(),a=iE(n,o),s=nE(t),l=r[a][s],u=l.offset,h=l.width,c=n.getOtherAxis(o),p=t.coordinateSystem.cx,d=t.coordinateSystem.cy,f=t.get(\"barMinHeight\")||0,g=t.get(\"barMinAngle\")||0;i[s]=i[s]||[];for(var y=e.mapDimension(c.dim),v=e.mapDimension(o.dim),m=gx(e,y),x=\"radius\"!==o.dim||!t.get(\"roundCap\",!0),_=c.dataToCoord(0),b=0,w=e.count();b<w;b++){var S=e.get(y,b),M=e.get(v,b),I=S>=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)<f&&(L=(L<0?-1:1)*f),C=T,D=T+L,k=(A=P-u)-h,m&&(i[s][M][I]=D)}else{var O=c.dataToCoord(S,x)-_,R=o.dataToCoord(M);Math.abs(O)<g&&(O=(O<0?-1:1)*g),D=(C=R+u)+h,A=T,k=T+O,m&&(i[s][M][I]=k)}e.setItemLayout(b,{cx:p,cy:d,r0:C,r:D,startAngle:-A*Math.PI/180,endAngle:-k*Math.PI/180,clockwise:A>=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<d.length;++y){var v=r.toGlobalCoord(d[y].coord);h?(f[0]=v,f[1]=u.y,g[0]=v,g[1]=u.y+u.height):(f[0]=u.x,f[1]=v,g[0]=u.x+u.width,g[1]=v);var m=new Zu({shape:{x1:f[0],y1:f[1],x2:g[0],y2:g[1]},silent:!0});Rh(m.shape,l);var x=p++%s.length;c[x]=c[x]||[],c[x].push(m)}var _=a.getLineStyle([\"color\"]);for(y=0;y<c.length;++y)e.add(Ph(c[y],{style:k({stroke:s[y%s.length]},_),silent:!0}))}},splitArea:function(t,e,n,i){mI(t,n,i,i)}},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.getCoordSysModel=function(){return this},e.type=\"singleAxis\",e.layoutMode=\"box\",e.defaultOption={left:\"5%\",top:\"5%\",right:\"5%\",bottom:\"5%\",type:\"value\",position:\"bottom\",orient:\"horizontal\",axisLine:{show:!0,lineStyle:{width:1,type:\"solid\"}},tooltip:{show:!0},axisTick:{show:!0,length:6,lineStyle:{width:1}},axisLabel:{show:!0,interval:\"auto\"},splitLine:{show:!0,lineStyle:{type:\"dashed\",opacity:.2}}},e}(Rp);R(dE,I_.prototype);var fE=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return 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.pointToData=function(t,e){return this.coordinateSystem.pointToData(t)[0]},e}(nb),gE=[\"single\"],yE=function(){function t(t,e,n){this.type=\"single\",this.dimension=\"single\",this.dimensions=gE,this.axisPointerEnabled=!0,this.model=t,this._init(t,e,n)}return t.prototype._init=function(t,e,n){var i=this.dimension,r=new fE(i,m_(t),[0,0],t.get(\"type\"),t.get(\"position\")),o=\"category\"===r.type;r.onBand=o&&t.get(\"boundaryGap\"),r.inverse=t.get(\"inverse\"),r.orient=t.get(\"orient\"),t.axis=r,r.model=t,r.coordinateSystem=this,this._axis=r},t.prototype.update=function(t,e){t.eachSeries((function(t){if(t.coordinateSystem===this){var e=t.getData();E(e.mapDimensionsAll(this.dimension),(function(t){this._axis.scale.unionExtentFromData(e,t)}),this),v_(this._axis.scale,this._axis.model)}}),this)},t.prototype.resize=function(t,e){this._rect=Cp({left:t.get(\"left\"),top:t.get(\"top\"),right:t.get(\"right\"),bottom:t.get(\"bottom\"),width:t.get(\"width\"),height:t.get(\"height\")},{width:e.getWidth(),height:e.getHeight()}),this._adjustAxis()},t.prototype.getRect=function(){return this._rect},t.prototype._adjustAxis=function(){var t=this._rect,e=this._axis,n=e.isHorizontal(),i=n?[0,t.width]:[0,t.height],r=e.inverse?1:0;e.setExtent(i[r],i[1-r]),this._updateAxisTransform(e,n?t.x:t.y)},t.prototype._updateAxisTransform=function(t,e){var n=t.getExtent(),i=n[0]+n[1],r=t.isHorizontal();t.toGlobalCoord=r?function(t){return t+e}:function(t){return i-t+e},t.toLocalCoord=r?function(t){return t-e}:function(t){return i-t+e}},t.prototype.getAxis=function(){return this._axis},t.prototype.getBaseAxis=function(){return this._axis},t.prototype.getAxes=function(){return[this._axis]},t.prototype.getTooltipAxes=function(){return{baseAxes:[this.getAxis()],otherAxes:[]}},t.prototype.containPoint=function(t){var e=this.getRect(),n=this.getAxis();return\"horizontal\"===n.orient?n.contain(n.toLocalCoord(t[0]))&&t[1]>=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<u[h].length-1;d++){var f=u[h][d].slice(),g=this._firstDayOfMonth[d];if(p){var y=this._firstDayPoints[d];f[c]=(y[c]+u[0][d+1][c])/2}var v=r.get(\"formatter\"),m=o[+g.m-1],x={yyyy:g.y,yy:(g.y+\"\").slice(2),MM:g.m,M:+g.m,nameMap:m},_=this._formatterLabel(v,x),b=new Fs({z2:30,style:A(nc(r,{text:_}),this._monthTextPositionControl(f,p,n,s,a))});i.add(b)}}},e.prototype._weekTextPositionControl=function(t,e,n,i,r){var o=\"center\",a=\"middle\",s=t[0],l=t[1],u=\"start\"===n;return\"horizontal\"===e?(s=s+i+(u?1:-1)*r[0]/2,o=u?\"right\":\"left\"):(l=l+i+(u?1:-1)*r[1]/2,a=u?\"bottom\":\"top\"),{x:s,y:l,align:o,verticalAlign:a}},e.prototype._renderWeekText=function(t,e,n,i,r){var o=t.getModel(\"dayLabel\");if(o.get(\"show\")){var a=t.coordinateSystem,s=o.get(\"position\"),l=o.get(\"nameMap\"),u=o.get(\"margin\"),h=a.getFirstDayOfWeek();if(!l||U(l))l&&(e=Nc(l)||e),l=e.get([\"time\",\"dayOfWeekShort\"])||z(e.get([\"time\",\"dayOfWeekAbbr\"]),(function(t){return t[0]}));var c=a.getNextNDay(n.end.time,7-n.lweek).time,p=[a.getCellWidth(),a.getCellHeight()];u=Ur(u,Math.min(p[1],p[0])),\"start\"===s&&(c=a.getNextNDay(n.start.time,-(7+n.fweek)).time,u=-u);for(var d=0;d<7;d++){var f,g=a.getNextNDay(c,d),y=a.dataToRect([g.time],!1).center;f=Math.abs((d+h)%7);var v=new Fs({z2:30,style:A(nc(o,{text:l[f]}),this._weekTextPositionControl(y,i,s,u,p))});r.add(v)}}},e.type=\"calendar\",e}(Tg),AE=864e5,kE=function(){function t(e,n,i){this.type=\"calendar\",this.dimensions=t.dimensions,this.getDimensionsInfo=t.getDimensionsInfo,this._model=e}return t.getDimensionsInfo=function(){return[{name:\"time\",type:\"time\"},\"value\"]},t.prototype.getRangeInfo=function(){return this._rangeInfo},t.prototype.getModel=function(){return this._model},t.prototype.getRect=function(){return this._rect},t.prototype.getCellWidth=function(){return this._sw},t.prototype.getCellHeight=function(){return this._sh},t.prototype.getOrient=function(){return this._orient},t.prototype.getFirstDayOfWeek=function(){return this._firstDayOfWeek},t.prototype.getDateInfo=function(t){var e=(t=ro(t)).getFullYear(),n=t.getMonth()+1,i=n<10?\"0\"+n:\"\"+n,r=t.getDate(),o=r<10?\"0\"+r:\"\"+r,a=t.getDay();return{y:e+\"\",m:i,d:o,day:a=Math.abs((a+7-this.getFirstDayOfWeek())%7),time:t.getTime(),formatedDate:e+\"-\"+i+\"-\"+o,date:t}},t.prototype.getNextNDay=function(t,e){return 0===(e=e||0)||(t=new Date(this.getDateInfo(t).time)).setDate(t.getDate()+e),this.getDateInfo(t)},t.prototype.update=function(t,e){this._firstDayOfWeek=+this._model.getModel(\"dayLabel\").get(\"firstDay\"),this._orient=this._model.get(\"orient\"),this._lineWidth=this._model.getModel(\"itemStyle\").getItemStyle().lineWidth||0,this._rangeInfo=this._getRangeInfo(this._initRangeOption());var n=this._rangeInfo.weeks||1,i=[\"width\",\"height\"],r=this._model.getCellSize().slice(),o=this._model.getBoxLayoutParams(),a=\"horizontal\"===this._orient?[n,7]:[7,n];E([0,1],(function(t){u(r,t)&&(o[i[t]]=r[t]*a[t])}));var s={width:e.getWidth(),height:e.getHeight()},l=this._rect=Cp(o,s);function u(t,e){return null!=t[e]&&\"auto\"!==t[e]}E([0,1],(function(t){u(r,t)||(r[t]=l[i[t]]/a[t])})),this._sw=r[0],this._sh=r[1]},t.prototype.dataToPoint=function(t,e){Y(t)&&(t=t[0]),null==e&&(e=!0);var n=this.getDateInfo(t),i=this._rangeInfo,r=n.formatedDate;if(e&&!(n.time>=i.start.time&&n.time<i.end.time+AE))return[NaN,NaN];var o=n.day,a=this._getRangeInfo([i.start.time,r]).nthWeek;return\"vertical\"===this._orient?[this._rect.x+o*this._sw+this._sw/2,this._rect.y+a*this._sh+this._sh/2]:[this._rect.x+a*this._sw+this._sw/2,this._rect.y+o*this._sh+this._sh/2]},t.prototype.pointToData=function(t){var e=this.pointToDate(t);return e&&e.time},t.prototype.dataToRect=function(t,e){var n=this.dataToPoint(t,e);return{contentShape:{x:n[0]-(this._sw-this._lineWidth)/2,y:n[1]-(this._sh-this._lineWidth)/2,width:this._sw-this._lineWidth,height:this._sh-this._lineWidth},center:n,tl:[n[0]-this._sw/2,n[1]-this._sh/2],tr:[n[0]+this._sw/2,n[1]-this._sh/2],br:[n[0]+this._sw/2,n[1]+this._sh/2],bl:[n[0]-this._sw/2,n[1]+this._sh/2]}},t.prototype.pointToDate=function(t){var e=Math.floor((t[0]-this._rect.x)/this._sw)+1,n=Math.floor((t[1]-this._rect.y)/this._sh)+1,i=this._rangeInfo.range;return\"vertical\"===this._orient?this._getDateByWeeksAndDay(n,e-1,i):this._getDateByWeeksAndDay(e,n-1,i)},t.prototype.convertToPixel=function(t,e,n){var i=LE(e);return i===this?i.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){var i=LE(e);return i===this?i.pointToData(n):null},t.prototype.containPoint=function(t){return console.warn(\"Not implemented.\"),!1},t.prototype._initRangeOption=function(){var t,e=this._model.get(\"range\");if(Y(e)&&1===e.length&&(e=e[0]),Y(e))t=e;else{var n=e.toString();if(/^\\d{4}$/.test(n)&&(t=[n+\"-01-01\",n+\"-12-31\"]),/^\\d{4}[\\/|-]\\d{1,2}$/.test(n)){var i=this.getDateInfo(n),r=i.date;r.setMonth(r.getMonth()+1);var o=this.getNextNDay(r,-1);t=[i.formatedDate,o.formatedDate]}/^\\d{4}[\\/|-]\\d{1,2}[\\/|-]\\d{1,2}$/.test(n)&&(t=[n,n])}if(!t)return e;var a=this._getRangeInfo(t);return a.start.time>a.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&&e<i.fweek||t===i.weeks&&e>i.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<i.length;r++){var o=i[r];null==t[o]&&null!=e[o]&&(t[o]=e[o])}}var EE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.preventAutoZ=!0,n}return n(e,t),e.prototype.mergeOption=function(e,n){var i=this.option.elements;this.option.elements=null,t.prototype.mergeOption.call(this,e,n),this.option.elements=i},e.prototype.optionUpdated=function(t,e){var n=this.option,i=(e?n:t).elements,r=n.elements=e?[]:n.elements,o=[];this._flatten(i,o,null);var a=To(r,o,\"normalMerge\"),s=this._elOptionsToUpdate=[];E(a,(function(t,e){var n=t.newOption;n&&(s.push(n),function(t,e){var n=t.existing;if(e.id=t.keyInfo.id,!e.type&&n&&(e.type=n.type),null==e.parentId){var i=e.parentOption;i?e.parentId=i.id:n&&(e.parentId=n.parentId)}e.parentOption=null}(t,n),function(t,e,n){var i=A({},n),r=t[e],o=n.$action||\"merge\";\"merge\"===o?r?(C(r,i,!0),kp(r,i,{ignoreSize:!0}),Pp(n,r),NE(n,r),NE(n,r,\"shape\"),NE(n,r,\"style\"),NE(n,r,\"extra\"),n.clipPath=r.clipPath):t[e]=i:\"replace\"===o?t[e]=i:\"remove\"===o&&r&&(t[e]=null)}(r,e,n),function(t,e){if(t&&(t.hv=e.hv=[PE(e,[\"left\",\"right\"]),PE(e,[\"top\",\"bottom\"])],\"group\"===t.type)){var n=t,i=e;null==n.width&&(n.width=i.width=0),null==n.height&&(n.height=i.height=0)}}(r[e],n))}),this),n.elements=B(r,(function(t){return t&&delete t.$action,null!=t}))},e.prototype._flatten=function(t,e,n){E(t,(function(t){if(t){n&&(t.parentOption=n),e.push(t);var i=t.children;i&&i.length&&this._flatten(i,e,t),delete t.children}}),this)},e.prototype.useElOptionsToUpdate=function(){var t=this._elOptionsToUpdate;return this._elOptionsToUpdate=null,t},e.type=\"graphic\",e.defaultOption={elements:[]},e}(Rp),zE={path:null,compoundPath:null,group:zr,image:ks,text:Fs},VE=Oo(),BE=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._elMap=yt()},e.prototype.render=function(t,e,n){t!==this._lastGraphicModel&&this._clear(),this._lastGraphicModel=t,this._updateElements(t),this._relocate(t,n)},e.prototype._updateElements=function(t){var e=t.useElOptionsToUpdate();if(e){var n=this._elMap,i=this.group,r=t.get(\"z\"),o=t.get(\"zlevel\");E(e,(function(e){var a=Ao(e.id,null),s=null!=a?n.get(a):null,l=Ao(e.parentId,null),u=null!=l?n.get(l):i,h=e.type,c=e.style;\"text\"===h&&c&&e.hv&&e.hv[1]&&(c.textVerticalAlign=c.textBaseline=c.verticalAlign=c.align=null);var p=e.textContent,d=e.textConfig;if(c&&ZO(c,h,!!d,!!p)){var f=jO(c,h,!0);!d&&f.textConfig&&(d=e.textConfig=f.textConfig),!p&&f.textContent&&(p=f.textContent)}var g=function(t){return t=A({},t),E([\"id\",\"parentId\",\"$action\",\"hv\",\"bounding\",\"textContent\",\"clipPath\"].concat(Sp),(function(e){delete t[e]})),t}(e);var y=e.$action||\"merge\",v=\"merge\"===y,m=\"replace\"===y;if(v){var x=s;(T=!s)?x=GE(a,u,e.type,n):(x&&(VE(x).isNew=!1),gR(x)),x&&(iR(x,g,t,{isInit:T}),HE(x,e,r,o))}else if(m){WE(s,e,n,t);var _=GE(a,u,e.type,n);_&&(iR(_,g,t,{isInit:!0}),HE(_,e,r,o))}else\"remove\"===y&&(rR(s,e),WE(s,e,n,t));var b=n.get(a);if(b&&p)if(v){var w=b.getTextContent();w?w.attr(p):b.setTextContent(new Fs(p))}else m&&b.setTextContent(new Fs(p));if(b){var S=e.clipPath;if(S){var M=S.type,I=void 0,T=!1;if(v){var C=b.getClipPath();I=(T=!C||VE(C).type!==M)?FE(M):C}else m&&(T=!0,I=FE(M));b.setClipPath(I),iR(I,S,t,{isInit:T}),yR(I,S.keyframeAnimation,t)}var D=VE(b);b.setTextConfig(d),D.option=e,function(t,e,n){var i=Qs(t).eventData;t.silent||t.ignore||i||(i=Qs(t).eventData={componentType:\"graphic\",componentIndex:e.componentIndex,name:t.name});i&&(i.info=n.info)}(b,t,e),Zh({el:b,componentModel:t,itemName:b.name,itemTooltipOption:e.tooltip}),yR(b,e.keyframeAnimation,t)}}))}},e.prototype._relocate=function(t,e){for(var n=t.option.elements,i=this.group,r=this._elMap,o=e.getWidth(),a=e.getHeight(),s=[\"x\",\"y\"],l=0;l<n.length;l++){if((f=null!=(d=Ao((p=n[l]).id,null))?r.get(d):null)&&f.isGroup){var u=(g=f.parent)===i,h=VE(f),c=VE(g);h.width=Ur(h.option.width,u?o:c.width)||0,h.height=Ur(h.option.height,u?a:c.height)||0}}for(l=n.length-1;l>=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;_<s.length;_++){var b=s[_],w=y[b];m&&(aR(m)||P(m,b)>=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<n.length;i++)for(var r=n[i],o=this._targetAxisInfoMap.get(r),a=0;a<o.indexList.length;a++){var s=this.getAxisProxy(r,o.indexList[a]);if(s.hostedBy(this))return s;e||(e=s)}return e},e.prototype.getRangePropMode=function(){return this._rangePropMode.slice()},e.prototype.getOrient=function(){return this._orient},e.type=\"dataZoom\",e.dependencies=[\"xAxis\",\"yAxis\",\"radiusAxis\",\"angleAxis\",\"singleAxis\",\"series\",\"toolbox\"],e.defaultOption={z:4,filterMode:\"filter\",start:0,end:100},e}(Rp);function $E(t){var e={};return E([\"start\",\"end\",\"startValue\",\"endValue\",\"throttle\"],(function(n){t.hasOwnProperty(n)&&(e[n]=t[n])})),e}var JE=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.select\",e}(KE),QE=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.dataZoomModel=t,this.ecModel=e,this.api=n},e.type=\"dataZoom\",e}(Tg),tz=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.select\",e}(QE),ez=E,nz=jr,iz=function(){function t(t,e,n,i){this._dimName=t,this._axisIndex=e,this.ecModel=i,this._dataZoomModel=n}return t.prototype.hostedBy=function(t){return this._dataZoomModel===t},t.prototype.getDataValueWindow=function(){return this._valueWindow.slice()},t.prototype.getDataPercentWindow=function(){return this._percentWindow.slice()},t.prototype.getTargetSeriesModels=function(){var t=[];return this.ecModel.eachSeries((function(e){if(function(t){var e=t.get(\"coordinateSystem\");return P(XE,e)>=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[0]&&(t[0]=i[0]),i[1]>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;l<i.length;l++){var u=a.get(s[l],t),h=!isNaN(u),c=u<o[0],p=u>o[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='<body style=\"margin:0;\"><img src=\"'+s+'\" style=\"max-width:100%;\" title=\"'+(b&&b[0]||\"\")+'\" /></body>',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<e.length;r++){var o=wz(e[r]).split(Sz);n.push(o.shift());for(var a=0;a<o.length;a++)i[a]&&(i[a].data[r]=o[a])}return{series:i,categories:n}}(t),o=e[n],a=o.axisDim+\"Axis\";o&&(i[a]=i[a]||[],i[a][o.axisIndex]={data:r.categories},i.series=i.series.concat(r.series))}else{r=function(t){for(var e=t.split(/\\n+/g),n=wz(e.shift()),i=[],r=0;r<e.length;r++){var o=wz(e[r]);if(o){var a=o.split(Sz),s=\"\",l=void 0,u=!1;isNaN(a[0])?(u=!0,s=a[0],a=a.slice(1),i[r]={name:s,value:[]},l=i[r].value):l=i[r]=[];for(var h=0;h<a.length;h++)l.push(+a[h]);1===l.length&&(u?i[r].value=l[0]:i[r]=l[0])}}return{name:n,data:i}}(t);i.series.push(r)}})),i}var Iz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.onclick=function(t,e){setTimeout((function(){e.dispatchAction({type:\"hideTip\"})}));var n=e.getDom(),i=this.model;this._dom&&n.removeChild(this._dom);var r=document.createElement(\"div\");r.style.cssText=\"position:absolute;top:0;bottom:0;left:0;right:0;padding:5px\",r.style.backgroundColor=i.get(\"backgroundColor\")||\"#fff\";var o=document.createElement(\"h4\"),a=i.get(\"lang\")||[];o.innerHTML=a[0]||i.get(\"title\"),o.style.cssText=\"margin:10px 20px\",o.style.color=i.get(\"textColor\");var s=document.createElement(\"div\"),l=document.createElement(\"textarea\");s.style.cssText=\"overflow:auto\";var u=i.get(\"optionToContent\"),h=i.get(\"contentToOption\"),c=function(t){var e,n,i,r=function(t){var e={},n=[],i=[];return t.eachRawSeries((function(t){var r=t.coordinateSystem;if(!r||\"cartesian2d\"!==r.type&&\"polar\"!==r.type)n.push(t);else{var o=r.getBaseAxis();if(\"category\"===o.type){var a=o.dim+\"_\"+o.index;e[a]||(e[a]={categoryAxis:o,valueAxis:r.getOtherAxis(o),series:[]},i.push({axisDim:o.dim,axisIndex:o.index})),e[a].series.push(t)}else n.push(t)}})),{seriesGroupByCategoryAxis:e,other:n,meta:i}}(t);return{value:B([(n=r.seriesGroupByCategoryAxis,i=[],E(n,(function(t,e){var n=t.categoryAxis,r=t.valueAxis.dim,o=[\" \"].concat(z(t.series,(function(t){return t.name}))),a=[n.model.getCategories()];E(t.series,(function(t){var e=t.getRawData();a.push(t.getRawData().mapArray(e.mapDimension(r),(function(t){return t})))}));for(var s=[o.join(bz)],l=0;l<a[0].length;l++){for(var u=[],h=0;h<a.length;h++)u.push(a[h][l]);s.push(u.join(bz))}i.push(s.join(\"\\n\"))})),i.join(\"\\n\\n\"+_z+\"\\n\\n\")),(e=r.other,z(e,(function(t){var e=t.getRawData(),n=[t.name],i=[];return e.each(e.dimensions,(function(){for(var t=arguments.length,r=arguments[t-1],o=e.getName(r),a=0;a<t-1;a++)i[a]=arguments[a];n.push((o?o+bz:\"\")+i.join(bz))})),n.join(\"\\n\")})).join(\"\\n\\n\"+_z+\"\\n\\n\"))],(function(t){return!!t.replace(/[\\n\\t\\s]/g,\"\")})).join(\"\\n\\n\"+_z+\"\\n\\n\"),meta:r.meta}}(t);if(X(u)){var p=u(e.getOption());U(p)?s.innerHTML=p:J(p)&&s.appendChild(p)}else{l.readOnly=i.get(\"readOnly\");var d=l.style;d.cssText=\"display:block;width:100%;height:100%;font-family:monospace;font-size:14px;line-height:1.6rem;resize:none;box-sizing:border-box;outline:none\",d.color=i.get(\"textColor\"),d.borderColor=i.get(\"textareaBorderColor\"),d.backgroundColor=i.get(\"textareaColor\"),l.value=c.value,s.appendChild(l)}var f=c.meta,g=document.createElement(\"div\");g.style.cssText=\"position:absolute;bottom:5px;left:0;right:0\";var y=\"float:right;margin-right:20px;border:none;cursor:pointer;padding:2px 5px;font-size:12px;border-radius:3px\",v=document.createElement(\"div\"),m=document.createElement(\"div\");y+=\";background-color:\"+i.get(\"buttonColor\"),y+=\";color:\"+i.get(\"buttonTextColor\");var x=this;function _(){n.removeChild(r),x._dom=null}pe(v,\"click\",_),pe(m,\"click\",(function(){if(null==h&&null!=u||null!=h&&null==u)_();else{var t;try{t=X(h)?h(s,e.getOption()):Mz(l.value,f)}catch(t){throw _(),new Error(\"Data view format error \"+t)}t&&e.dispatchAction({type:\"changeDataView\",newOption:t}),_()}})),v.innerHTML=a[1],m.innerHTML=a[2],m.style.cssText=v.style.cssText=y,!i.get(\"readOnly\")&&g.appendChild(m),g.appendChild(v),r.appendChild(o),r.appendChild(s),r.appendChild(g),s.style.height=n.clientHeight-80+\"px\",n.appendChild(r),this._dom=r},e.prototype.remove=function(t,e){this._dom&&e.getDom().removeChild(this._dom)},e.prototype.dispose=function(t,e){this.remove(t,e)},e.getDefaultOption=function(t){return{show:!0,readOnly:!1,optionToContent:null,contentToOption:null,icon:\"M17.5,17.3H33 M17.5,17.3H33 M45.4,29.5h-28 M11.5,2v56H51V14.8L38.4,2H11.5z M38.4,2.2v12.7H51 M45.4,41.7h-28\",title:t.getLocaleModel().get([\"toolbox\",\"dataView\",\"title\"]),lang:t.getLocaleModel().get([\"toolbox\",\"dataView\",\"lang\"]),backgroundColor:\"#fff\",textColor:\"#000\",textareaColor:\"#fff\",textareaBorderColor:\"#333\",buttonColor:\"#c23531\",buttonTextColor:\"#fff\"}},e}(lz);function Tz(t,e){return z(t,(function(t,n){var i=e&&e[n];if(q(i)&&!Y(i)){q(t)&&!Y(t)||(t={value:t});var r=null!=i.name&&null==t.name;return t=k(t,i),r&&delete t.name,t}return t}))}Mm({type:\"changeDataView\",event:\"dataViewChanged\",update:\"prepareAndUpdate\"},(function(t,e){var n=[];E(t.newOption.series,(function(t){var i=e.getSeriesByName(t.name)[0];if(i){var r=i.get(\"data\");n.push({name:t.name,data:Tz(t.data,r)})}else n.push(A({type:\"scatter\"},t))})),e.mergeOption(k({series:n},t.newOption))}));var Cz=E,Dz=Oo();function Az(t){var e=Dz(t);return e.snapshots||(e.snapshots=[{}]),e.snapshots}var kz=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.onclick=function(t,e){!function(t){Dz(t).snapshots=null}(t),e.dispatchAction({type:\"restore\",from:this.uid})},e.getDefaultOption=function(t){return{show:!0,icon:\"M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5\",title:t.getLocaleModel().get([\"toolbox\",\"restore\",\"title\"])}},e}(lz);Mm({type:\"restore\",event:\"restore\",update:\"prepareAndUpdate\"},(function(t,e){e.resetOption(\"recreate\")}));var Lz=[\"grid\",\"xAxis\",\"yAxis\",\"geo\",\"graph\",\"polar\",\"radiusAxis\",\"angleAxis\",\"bmap\"],Pz=function(){function t(t,e,n){var i=this;this._targetInfoList=[];var r=Rz(e,t);E(Nz,(function(t,e){(!n||!n.include||P(n.include,e)>=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;r<n.length;r++){var o=n[r],a=t.panelId;if(a){if(o.panelId===a)return o}else for(var s=0;s<Ez.length;s++)if(Ez[s](i,o))return o}return!0},t}();function Oz(t){return t[0]>t[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<i;n++)if(t[n]in e)return t[n]}var Qz=Jz([\"transform\",\"webkitTransform\",\"OTransform\",\"MozTransform\",\"msTransform\"]);function tV(t,e){if(!t)return e;e=dp(e,!0);var n=t.indexOf(e);return(t=-1===n?e:\"-\"+t.slice(0,n)+\"-\"+e).toLowerCase()}var eV=tV(Jz([\"webkitTransition\",\"transition\",\"OTransition\",\"MozTransition\",\"msTransition\"]),\"transition\"),nV=tV(Qz,\"transform\"),iV=\"position:absolute;display:block;border-style:solid;white-space:nowrap;z-index:9999999;\"+(r.transform3dSupported?\"will-change:transform;\":\"\");function rV(t,e,n){var i=t.toFixed(0)+\"px\",o=e.toFixed(0)+\"px\";if(!r.transformSupported)return n?\"top:\"+o+\";left:\"+i+\";\":[[\"top\",o],[\"left\",i]];var a=r.transform3dSupported,s=\"translate\"+(a?\"3d\":\"\")+\"(\"+i+\",\"+o+(a?\",0\":\"\")+\")\";return n?\"top:0;left:0;\"+nV+\":\"+s+\";\":[[\"top\",0],[\"left\",0],[Qz,s]]}function oV(t,e,n){var i=[],o=t.get(\"transitionDuration\"),a=t.get(\"backgroundColor\"),s=t.get(\"shadowBlur\"),l=t.get(\"shadowColor\"),u=t.get(\"shadowOffsetX\"),h=t.get(\"shadowOffsetY\"),c=t.getModel(\"textStyle\"),p=pg(t,\"html\"),d=u+\"px \"+h+\"px \"+s+\"px \"+l;return i.push(\"box-shadow:\"+d),e&&o&&i.push(function(t,e){var n=\"cubic-bezier(0.23,1,0.32,1)\",i=\" \"+t/2+\"s \"+n,o=\"opacity\"+i+\",visibility\"+i;return e||(i=\" \"+t+\"s \"+n,o+=r.transformSupported?\",\"+nV+i:\",left\"+i+\",top\"+i),eV+\":\"+o}(o,n)),a&&i.push(\"background-color:\"+a),E([\"width\",\"color\",\"radius\"],(function(e){var n=\"border-\"+e,r=dp(n),o=t.get(r);null!=o&&i.push(n+\":\"+o+(\"color\"===e?\"\":\"px\"))})),i.push(function(t){var e=[],n=t.get(\"fontSize\"),i=t.getTextColor();i&&e.push(\"color:\"+i),e.push(\"font:\"+t.getFont()),n&&e.push(\"line-height:\"+Math.round(3*n/2)+\"px\");var r=t.get(\"textShadowColor\"),o=t.get(\"textShadowBlur\")||0,a=t.get(\"textShadowOffsetX\")||0,s=t.get(\"textShadowOffsetY\")||0;return r&&o&&e.push(\"text-shadow:\"+a+\"px \"+s+\"px \"+o+\"px \"+r),E([\"decoration\",\"align\"],(function(n){var i=t.get(n);i&&e.push(\"text-\"+n+\":\"+i)})),e.join(\";\")}(c)),null!=p&&i.push(\"padding:\"+fp(p).join(\"px \")+\"px\"),i.join(\";\")+\";\"}function aV(t,e,n,i,r){var o=e&&e.painter;if(n){var a=o&&o.getViewportRoot();a&&function(t,e,n,i,r){te(Qt,e,i,r,!0)&&te(t,n,Qt[0],Qt[1])}(t,a,document.body,i,r)}else{t[0]=i,t[1]=r;var s=o&&o.getViewportRootOffset();s&&(t[0]+=s.offsetLeft,t[1]+=s.offsetTop)}t[2]=t[0]/e.getWidth(),t[3]=t[1]/e.getHeight()}var sV=function(){function t(t,e,n){if(this._show=!1,this._styleCoord=[0,0,0,0],this._enterable=!0,this._alwaysShowContent=!1,this._firstShow=!0,this._longHide=!0,r.wxa)return null;var i=document.createElement(\"div\");i.domBelongToZr=!0,this.el=i;var o=this._zr=e.getZr(),a=this._appendToBody=n&&n.appendToBody;aV(this._styleCoord,o,a,e.getWidth()/2,e.getHeight()/2),a?document.body.appendChild(i):t.appendChild(i),this._container=t;var s=this;i.onmouseenter=function(){s._enterable&&(clearTimeout(s._hideTimeout),s._show=!0),s._inContent=!0},i.onmousemove=function(t){if(t=t||window.event,!s._enterable){var e=o.handler;ce(o.painter.getViewportRoot(),t,!0),e.dispatch(\"mousemove\",t)}},i.onmouseleave=function(){s._inContent=!1,s._enterable&&s._show&&s.hideLater(s._hideDelay)}}return t.prototype.update=function(t){var e,n,i,r=this._container,o=(n=\"position\",(i=(e=r).currentStyle||document.defaultView&&document.defaultView.getComputedStyle(e))?n?i[n]:i:null),a=r.style;\"absolute\"!==a.position&&\"absolute\"!==o&&(a.position=\"relative\");var s=t.get(\"alwaysShowContent\");s&&this._moveIfResized(),this._alwaysShowContent=s,this.el.className=t.get(\"className\")||\"\"},t.prototype.show=function(t,e){clearTimeout(this._hideTimeout),clearTimeout(this._longHideTimeout);var n=this.el,i=n.style,r=this._styleCoord;n.innerHTML?i.cssText=iV+oV(t,!this._firstShow,this._longHide)+rV(r[0],r[1],!0)+\"border-color:\"+_p(e)+\";\"+(t.get(\"extraCssText\")||\"\")+\";pointer-events:\"+(this._enterable?\"auto\":\"none\"):i.display=\"none\",this._show=!0,this._firstShow=!1,this._longHide=!1},t.prototype.setContent=function(t,e,n,i,r){var o=this.el;if(null!=t){var a=\"\";if(U(r)&&\"item\"===n.get(\"trigger\")&&!$z(n)&&(a=function(t,e,n){if(!U(n)||\"inside\"===n)return\"\";var i=t.get(\"backgroundColor\"),r=t.get(\"borderWidth\");e=_p(e);var o,a,s=\"left\"===(o=n)?\"right\":\"right\"===o?\"left\":\"top\"===o?\"bottom\":\"top\",l=Math.max(1.5*Math.round(r),6),u=\"\",h=nV+\":\";P([\"left\",\"right\"],s)>-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'<div style=\"'+[\"position:absolute;width:\"+l+\"px;height:\"+l+\"px;z-index:-1;\",(u+=\";\"+s+\":-\"+Math.round(100*((d-Math.SQRT2*r)/2+Math.SQRT2*r-(d-p)/2))/100+\"px\")+\";\"+h+\";\",\"border-bottom:\"+f,\"border-right:\"+f,\"background-color:\"+i+\";\"].join(\"\")+'\"></div>'}(n,i,r)),U(t))o.innerHTML=t+a;else if(t){o.innerHTML=\"\",Y(t)||(t=[t]);for(var s=0;s<t.length;s++)J(t[s])&&t[s].parentNode!==o&&o.appendChild(t[s]);if(a&&o.childNodes.length){var l=document.createElement(\"div\");l.innerHTML=a,o.appendChild(l)}}}else o.innerHTML=\"\"},t.prototype.setEnterable=function(t){this._enterable=t},t.prototype.getSize=function(){var t=this.el;return[t.offsetWidth,t.offsetHeight]},t.prototype.moveTo=function(t,e){var n=this._styleCoord;if(aV(n,this._zr,this._appendToBody,t,e),null!=n[0]&&null!=n[1]){var i=this.el.style;E(rV(n[0],n[1]),(function(t){i[t[0]]=t[1]}))}},t.prototype._moveIfResized=function(){var t=this._styleCoord[2],e=this._styleCoord[3];this.moveTo(t*this._zr.getWidth(),e*this._zr.getHeight())},t.prototype.hide=function(){var t=this,e=this.el.style;e.visibility=\"hidden\",e.opacity=\"0\",r.transform3dSupported&&(e.willChange=\"\"),this._show=!1,this._longHideTimeout=setTimeout((function(){return t._longHide=!0}),500)},t.prototype.hideLater=function(t){!this._show||this._inContent&&this._enterable||this._alwaysShowContent||(t?(this._hideDelay=t,this._show=!1,this._hideTimeout=setTimeout(W(this.hide,this),t)):this.hide())},t.prototype.isShow=function(){return this._show},t.prototype.dispose=function(){this.el.parentNode.removeChild(this.el)},t}(),lV=function(){function t(t){this._show=!1,this._styleCoord=[0,0,0,0],this._alwaysShowContent=!1,this._enterable=!0,this._zr=t.getZr(),cV(this._styleCoord,this._zr,t.getWidth()/2,t.getHeight()/2)}return t.prototype.update=function(t){var e=t.get(\"alwaysShowContent\");e&&this._moveIfResized(),this._alwaysShowContent=e},t.prototype.show=function(){this._hideTimeout&&clearTimeout(this._hideTimeout),this.el.show(),this._show=!0},t.prototype.setContent=function(t,e,n,i,r){var o=this;q(t)&&vo(\"\"),this.el&&this._zr.remove(this.el);var a=n.getModel(\"textStyle\");this.el=new Fs({style:{rich:e.richTextStyles,text:t,lineHeight:22,borderWidth:1,borderColor:i,textShadowColor:a.get(\"textShadowColor\"),fill:n.get([\"textStyle\",\"color\"]),padding:pg(n,\"richText\"),verticalAlign:\"top\",align:\"left\"},z:n.get(\"z\")}),E([\"backgroundColor\",\"borderRadius\",\"shadowColor\",\"shadowBlur\",\"shadowOffsetX\",\"shadowOffsetY\"],(function(t){o.el.style[t]=n.get(t)})),E([\"textShadowBlur\",\"textShadowOffsetX\",\"textShadowOffsetY\"],(function(t){o.el.style[t]=a.get(t)||0})),this._zr.add(this.el);var s=this;this.el.on(\"mouseover\",(function(){s._enterable&&(clearTimeout(s._hideTimeout),s._show=!0),s._inContent=!0})),this.el.on(\"mouseout\",(function(){s._enterable&&s._show&&s.hideLater(s._hideDelay),s._inContent=!1}))},t.prototype.setEnterable=function(t){this._enterable=t},t.prototype.getSize=function(){var t=this.el,e=this.el.getBoundingRect(),n=hV(t.style);return[e.width+n.left+n.right,e.height+n.top+n.bottom]},t.prototype.moveTo=function(t,e){var n=this.el;if(n){var i=this._styleCoord;cV(i,this._zr,t,e),t=i[0],e=i[1];var r=n.style,o=uV(r.borderWidth||0),a=hV(r);n.x=t+o+a.left,n.y=e+o+a.top,n.markRedraw()}},t.prototype._moveIfResized=function(){var t=this._styleCoord[2],e=this._styleCoord[3];this.moveTo(t*this._zr.getWidth(),e*this._zr.getHeight())},t.prototype.hide=function(){this.el&&this.el.hide(),this._show=!1},t.prototype.hideLater=function(t){!this._show||this._inContent&&this._enterable||this._alwaysShowContent||(t?(this._hideDelay=t,this._show=!1,this._hideTimeout=setTimeout(W(this.hide,this),t)):this.hide())},t.prototype.isShow=function(){return this._show},t.prototype.dispose=function(){this._zr.remove(this.el)},t}();function uV(t){return Math.max(0,t)}function hV(t){var e=uV(t.shadowBlur||0),n=uV(t.shadowOffsetX||0),i=uV(t.shadowOffsetY||0);return{left:uV(e-n),right:uV(e+n),top:uV(e-i),bottom:uV(e+i)}}function cV(t,e,n,i){t[0]=n,t[1]=i,t[2]=t[0]/e.getWidth(),t[3]=t[1]/e.getHeight()}var pV=new zs({shape:{x:-1,y:-1,width:2,height:2}}),dV=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){if(!r.node&&e.getDom()){var n,i=t.getComponent(\"tooltip\"),o=this._renderMode=\"auto\"===(n=i.get(\"renderMode\"))?r.domSupported?\"html\":\"richText\":n||\"html\";this._tooltipContent=\"richText\"===o?new lV(e):new sV(e.getDom(),e,{appendToBody:i.get(\"appendToBody\",!0)})}},e.prototype.render=function(t,e,n){if(!r.node&&n.getDom()){this.group.removeAll(),this._tooltipModel=t,this._ecModel=e,this._api=n;var i=this._tooltipContent;i.update(t),i.setEnterable(t.get(\"enterable\")),this._initGlobalListener(),this._keepShow(),\"richText\"!==this._renderMode&&t.get(\"transitionDuration\")?Fg(this,\"_updatePosition\",50,\"fixRate\"):Gg(this,\"_updatePosition\")}},e.prototype._initGlobalListener=function(){var t=this._tooltipModel.get(\"triggerOn\");vN(\"itemTooltip\",this._api,W((function(e,n,i){\"none\"!==t&&(t.indexOf(e)>=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\":\"<br/>\",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]<s[0]&&s.reverse(),IV(s[0],a)||IV(s[1],a)||IV(a[0],s)||IV(a[1],s)}}}}function IV(t,e){return e[0]<=t&&t<=e[1]}var TV=[\"inBrush\",\"outOfBrush\"],CV=\"__ecBrushSelect\",DV=\"__ecInBrushSelectEvent\";function AV(t){t.eachComponent({mainType:\"brush\"},(function(e){(e.brushTargetManager=new Pz(e.option,t)).setInputRanges(e.areas,t)}))}function kV(t,e,n){var i,r,o=[];t.eachComponent({mainType:\"brush\"},(function(t){n&&\"takeGlobalCursor\"===n.type&&t.setBrushOption(\"brush\"===n.key?n.brushOption:{brushType:!1})})),AV(t),t.eachComponent({mainType:\"brush\"},(function(e,n){var a={brushId:e.id,brushIndex:n,brushName:e.name,areas:T(e.areas),selected:[]};o.push(a);var s=e.option,l=s.brushLink,u=[],h=[],c=[],p=!1;n||(i=s.throttleType,r=s.throttleDelay);var d=z(e.areas,(function(t){var e=OV[t.brushType],n=k({boundingRect:e?e(t):void 0},t);return n.selectors=function(t){var e=t.brushType,n={point:function(i){return SV[e].point(i,n,t)},rect:function(i){return SV[e].rect(i,n,t)}};return n}(n),n})),f=bV(e.option,TV,(function(t){t.mappingMethod=\"fixed\"}));function g(t){return\"all\"===l||!!u[t]}function y(t){return!!t.length}Y(l)&&E(l,(function(t){u[t]=1})),t.eachSeries((function(n,i){var r=c[i]=[];\"parallel\"===n.subType?function(t,e){var n=t.coordinateSystem;p=p||n.hasAxisBrushed(),g(e)&&n.eachActiveState(t.getData(),(function(t,e){\"active\"===t&&(h[e]=1)}))}(n,i):function(n,i,r){if(!n.brushSelector||function(t,e){var n=t.option.seriesIndex;return null!=n&&\"all\"!==n&&(Y(n)?P(n,e)<0:e!==n)}(e,i))return;if(E(d,(function(i){e.brushTargetManager.controlSeries(i,n,t)&&r.push(i),p=p||y(r)})),g(i)&&y(r)){var o=n.getData();o.each((function(t){PV(n,r,o,t)&&(h[t]=1)}))}}(n,i,r)})),t.eachSeries((function(t,e){var n={seriesId:t.id,seriesIndex:e,seriesName:t.name,dataIndex:[]};a.selected.push(n);var i=c[e],r=t.getData(),o=g(e)?function(t){return h[t]?(n.dataIndex.push(r.getRawIndex(t)),\"inBrush\"):\"outOfBrush\"}:function(e){return PV(t,i,r,e)?(n.dataIndex.push(r.getRawIndex(e)),\"inBrush\"):\"outOfBrush\"};(g(e)?p:y(i))&&function(t,e,n,i,r,o){var a,s={};function l(t){return Iy(n,a,t)}function u(t,e){Cy(n,a,t,e)}function h(t,h){a=null==o?t:h;var c=n.getRawDataItem(a);if(!c||!1!==c.visualMap)for(var p=i.call(r,t),d=e[p],f=s[p],g=0,y=f.length;g<y;g++){var v=f[g];d[v]&&d[v].applyVisual(t,l,u)}}E(t,(function(t){var n=_D.prepareVisualTypes(e[t]);s[t]=n})),null==o?n.each(h):n.each([o],h)}(TV,f,r,o)}))})),function(t,e,n,i,r){if(!r)return;var o=t.getZr();if(o[DV])return;o[CV]||(o[CV]=LV);var a=Fg(o,CV,n,e);a(t,i)}(e,i,r,o,n)}function LV(t,e){if(!t.isDisposed()){var n=t.getZr();n[DV]=!0,t.dispatchAction({type:\"brushSelect\",batch:e}),n[DV]=!1}}function PV(t,e,n,i){for(var r=0,o=e.length;r<o;r++){var a=e[r];if(t.brushSelector(i,n,a.selectors,a))return!0}}var OV={rect:function(t){return RV(t.range)},polygon:function(t){for(var e,n=t.range,i=0,r=n.length;i<r;i++){e=e||[[1/0,-1/0],[1/0,-1/0]];var o=n[i];o[0]<e[0][0]&&(e[0][0]=o[0]),o[0]>e[0][1]&&(e[0][1]=o[0]),o[1]<e[1][0]&&(e[1][0]=o[1]),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<e.getHeight()/2?\"-\":\"+\":l.x+l.width/2<e.getWidth()/2?\"+\":\"-\":U(a)?{horizontal:{top:\"-\",bottom:\"+\"},vertical:{left:\"-\",right:\"+\"}}[s][a]:a)>=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<i[0]&&(n=i[0]),this._currentPointer.x=n,this._currentPointer.markRedraw();var r=this._progressLine;r&&(r.shape.x2=n,r.dirty());var o=this._findNearestTick(n),a=this.model;(e||o!==a.getCurrentIndex()&&a.get(\"realtime\"))&&this._changeTimeline(o)},e.prototype._doPlayStop=function(){var t=this;this._clearTimer(),this.model.getPlayState()&&(this._timer=setTimeout((function(){var e=t.model;t._changeTimeline(e.getCurrentIndex()+(e.get(\"rewind\",!0)?-1:1))}),this.model.get(\"playInterval\")))},e.prototype._toAxisCoord=function(t){return zh(t,this._mainGroup.getLocalTransform(),!0)},e.prototype._findNearestTick=function(t){var e,n=this.model.getData(),i=1/0,r=this._axis;return n.each([\"value\"],(function(n,o){var a=r.dataToCoord(n),s=Math.abs(a-t);s<i&&(i=s,e=o)})),e},e.prototype._clearTimer=function(){this._timer&&(clearTimeout(this._timer),this._timer=null)},e.prototype._changeTimeline=function(t){var e=this.model.getCurrentIndex();\"+\"===t?t=e+1:\"-\"===t&&(t=e-1),this.api.dispatchAction({type:\"timelineChange\",currentIndex:t,from:this.uid})},e.prototype._updateTicksStatus=function(){var t=this.model.getCurrentIndex(),e=this._tickSymbols,n=this._tickLabels;if(e)for(var i=0;i<e.length;i++)e&&e[i]&&e[i].toggleState(\"progress\",i<t);if(n)for(i=0;i<n.length;i++)n&&n[i]&&n[i].toggleState(\"progress\",ZV(n[i]).dataIndex<=t)},e.type=\"timeline.slider\",e}(YV);function qV(t,e,n,i,r,o){var a=e.get(\"color\");r?(r.setColor(a),n.add(r),o&&o.onUpdate(r)):((r=Wy(t.get(\"symbol\"),-1,-1,2,2,a)).setStyle(\"strokeNoScale\",!0),n.add(r),o&&o.onCreate(r));var s=e.getItemStyle([\"color\"]);r.setStyle(s),i=C({rectHover:!0,z2:100},i,!0);var l=Hy(t.get(\"symbolSize\"));i.scaleX=l[0]/2,i.scaleY=l[1]/2;var u=Yy(t.get(\"symbolOffset\"),l);u&&(i.x=(i.x||0)+u[0],i.y=(i.y||0)+u[1]);var h=t.get(\"symbolRotate\");return i.rotation=(h||0)*Math.PI/180||0,r.attr(i),r.updateTransform(),r}function KV(t,e,n,i,r,o){if(!t.dragging){var a=r.getModel(\"checkpointStyle\"),s=i.dataToCoord(r.getData().get(\"value\",n));if(o||!a.get(\"animation\",!0))t.attr({x:s,y:0}),e&&e.attr({shape:{x2:s}});else{var l={duration:a.get(\"animationDuration\",!0),easing:a.get(\"animationEasing\",!0)};t.stopAnimation(null,!0),t.animateTo({x:s,y:0},l),e&&e.animateTo({shape:{x2:s}},l)}}}function $V(t){var e=t&&t.timeline;Y(e)||(e=e?[e]:[]),E(e,(function(t){t&&function(t){var e=t.type,n={number:\"value\",time:\"time\"};n[e]&&(t.axisType=n[e],delete t.type);if(JV(t),QV(t,\"controlPosition\")){var i=t.controlStyle||(t.controlStyle={});QV(i,\"position\")||(i.position=t.controlPosition),\"none\"!==i.position||QV(i,\"show\")||(i.show=!1,delete i.position),delete t.controlPosition}E(t.data||[],(function(t){q(t)&&!Y(t)&&(!QV(t,\"value\")&&QV(t,\"name\")&&(t.value=t.name),JV(t))}))}(t)}))}function JV(t){var e=t.itemStyle||(t.itemStyle={}),n=e.emphasis||(e.emphasis={}),i=t.label||t.label||{},r=i.normal||(i.normal={}),o={normal:1,emphasis:1};E(i,(function(t,e){o[e]||QV(r,e)||(r[e]=t)})),n.label&&!QV(i,\"emphasis\")&&(i.emphasis=n.label,delete n.label)}function QV(t,e){return t.hasOwnProperty(e)}function tB(t,e){if(!t)return!1;for(var n=Y(t)?t:[t],i=0;i<n.length;i++)if(n[i]&&n[i][e])return!0;return!1}function eB(t){wo(t,\"label\",[\"show\"])}var nB=Oo(),iB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.createdBySelf=!1,n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n),this._mergeOption(t,n,!1,!0)},e.prototype.isAnimationEnabled=function(){if(r.node)return!1;var t=this.__hostSeries;return this.getShallow(\"animation\")&&t&&t.isAnimationEnabled()},e.prototype.mergeOption=function(t,e){this._mergeOption(t,e,!1,!1)},e.prototype._mergeOption=function(t,e,n,i){var r=this.mainType;n||e.eachSeries((function(t){var n=t.get(this.mainType,!0),o=nB(t)[r];n&&n.data?(o?o._mergeOption(n,e,!0):(i&&eB(n),E(n.data,(function(t){t instanceof Array?(eB(t[0]),eB(t[1])):eB(t)})),A(o=this.createMarkerModelFromSeries(n,this,e),{mainType:this.mainType,seriesIndex:t.seriesIndex,name:t.name,createdBySelf:!0}),o.__hostSeries=t),nB(t)[r]=o):nB(t)[r]=null}),this)},e.prototype.formatTooltip=function(t,e,n){var i=this.getData(),r=this.getRawValue(t),o=i.getName(t);return ng(\"section\",{header:this.name,blocks:[ng(\"nameValue\",{name:o,value:r,noName:!o,noValue:null==r})]})},e.prototype.getData=function(){return this._data},e.prototype.setData=function(t){this._data=t},e.getMarkerModelFromSeries=function(t,e){return nB(t)[e]},e.type=\"marker\",e.dependencies=[\"series\",\"grid\",\"polar\",\"geo\"],e}(Rp);R(iB,vf.prototype);var rB=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=\"markPoint\",e.defaultOption={z:5,symbol:\"pin\",symbolSize:50,tooltip:{trigger:\"item\"},label:{show:!0,position:\"inside\"},itemStyle:{borderWidth:2},emphasis:{label:{show:!0}}},e}(iB);function oB(t){return!(isNaN(parseFloat(t.x))&&isNaN(parseFloat(t.y)))}function aB(t,e,n,i,r,o){var a=[],s=gx(e,i)?e.getCalculationInfo(\"stackResultDimension\"):i,l=pB(e,s,t),u=e.indicesOfNearest(s,l)[0];a[r]=e.get(n,u),a[o]=e.get(s,u);var h=e.get(i,u),c=qr(e.get(i,u));return(c=Math.min(c,20))>=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]<c[0]||h[0]>p[1]||h[1]<p[0]);u.setItemLayout(e,{points:n,allClipped:d});var f=u.getItemModel(e).getModel(\"itemStyle\").getItemStyle(),g=Ty(a,\"color\");f.fill||(f.fill=g,U(f.fill)&&(f.fill=ii(f.fill,.4))),f.stroke||(f.stroke=g),u.setItemVisual(e,\"style\",f)})),u.diff(TB(l).data).add((function(t){var e=u.getItemLayout(t);if(!e.allClipped){var n=new Wu({shape:{points:e.points}});u.setItemGraphicEl(t,n),l.group.add(n)}})).update((function(t,n){var i=TB(l).data.getItemGraphicEl(n),r=u.getItemLayout(t);r.allClipped?i&&l.group.remove(i):(i?fh(i,{shape:{points:r.points}},e,t):i=new Wu({shape:{points:r.points}}),u.setItemGraphicEl(t,i),l.group.add(i))})).remove((function(t){var e=TB(l).data.getItemGraphicEl(t);l.group.remove(e)})).execute(),u.eachItemGraphicEl((function(t,n){var i=u.getItemModel(n),r=u.getItemVisual(n,\"style\");t.useStyle(u.getItemVisual(n,\"style\")),tc(t,ec(i),{labelFetcher:e,labelDataIndex:n,defaultText:u.getName(n)||\"\",inheritColor:U(r.fill)?ii(r.fill,1):\"#000\"}),jl(t,i),Yl(t,null,null,i.get([\"emphasis\",\"disabled\"])),Qs(t).dataModel=e})),TB(l).data=u,l.group.silent=e.get(\"silent\")||t.get(\"silent\")},e.type=\"markArea\",e}(fB);var RB=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.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n),t.selected=t.selected||{},this._updateSelector(t)},e.prototype.mergeOption=function(e,n){t.prototype.mergeOption.call(this,e,n),this._updateSelector(e)},e.prototype._updateSelector=function(t){var e=t.selector,n=this.ecModel;!0===e&&(e=t.selector=[\"all\",\"inverse\"]),Y(e)&&E(e,(function(t,i){U(t)&&(t={type:t}),e[i]=C(t,function(t,e){return\"all\"===e?{type:\"all\",title:t.getLocaleModel().get([\"legend\",\"selector\",\"all\"])}:\"inverse\"===e?{type:\"inverse\",title:t.getLocaleModel().get([\"legend\",\"selector\",\"inverse\"])}:void 0}(n,t.type))}))},e.prototype.optionUpdated=function(){this._updateData(this.ecModel);var t=this._data;if(t[0]&&\"single\"===this.get(\"selectedMode\")){for(var e=!1,n=0;n<t.length;n++){var i=t[n].get(\"name\");if(this.isSelected(i)){this.select(i),e=!0;break}}!e&&this.select(t[0].get(\"name\"))}},e.prototype._updateData=function(t){var e=[],n=[];t.eachRawSeries((function(i){var r,o=i.name;if(n.push(o),i.legendVisualProvider){var a=i.legendVisualProvider.getAllNames();t.isSeriesFiltered(i)||(n=n.concat(a)),a.length?e=e.concat(a):r=!0}else r=!0;r&&ko(i)&&e.push(i.name)})),this._availableNames=n;var i=this.get(\"data\")||e,r=yt(),o=z(i,(function(t){return(U(t)||j(t))&&(t={name:t}),r.get(t.name)?null:(r.set(t.name,!0),new Mc(t,this,this.ecModel))}),this);this._data=B(o,(function(t){return!!t}))},e.prototype.getData=function(){return this._data},e.prototype.select=function(t){var e=this.option.selected;\"single\"===this.get(\"selectedMode\")&&E(this._data,(function(t){e[t.get(\"name\")]=!1}));e[t]=!0},e.prototype.unSelect=function(t){\"single\"!==this.get(\"selectedMode\")&&(this.option.selected[t]=!1)},e.prototype.toggleSelected=function(t){var e=this.option.selected;e.hasOwnProperty(t)||(e[t]=!0),this[e[t]?\"unSelect\":\"select\"](t)},e.prototype.allSelect=function(){var t=this._data,e=this.option.selected;E(t,(function(t){e[t.get(\"name\",!0)]=!0}))},e.prototype.inverseSelect=function(){var t=this._data,e=this.option.selected;E(t,(function(t){var n=t.get(\"name\",!0);e.hasOwnProperty(n)||(e[n]=!0),e[n]=!e[n]}))},e.prototype.isSelected=function(t){var e=this.option.selected;return!(e.hasOwnProperty(t)&&!e[t])&&P(this._availableNames,t)>=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;i<r&&!(e=n[i].states.emphasis);)i++;return e&&e.hoverLayer}function GB(t,e,n,i){FB(n)||n.dispatchAction({type:\"highlight\",seriesName:t,name:e,excludeSeriesId:i})}function WB(t,e,n,i){FB(n)||n.dispatchAction({type:\"downplay\",seriesName:t,name:e,excludeSeriesId:i})}function HB(t){var e=t.findComponents({mainType:\"legend\"});e&&e.length&&t.filterSeries((function(t){for(var n=0;n<e.length;n++)if(!e[n].isSelected(t.name))return!1;return!0}))}function YB(t,e,n){var i,r={},o=\"toggleSelected\"===t;return n.eachComponent(\"legend\",(function(n){o&&null!=i?n[i?\"select\":\"unSelect\"](e.name):\"allSelect\"===t||\"inverseSelect\"===t?n[t]():(n[t](e.name),i=n.isSelected(e.name)),E(n.getData(),(function(t){var e=t.get(\"name\");if(\"\\n\"!==e&&\"\"!==e){var i=n.isSelected(e);r.hasOwnProperty(e)?r[e]=r[e]&&i:r[e]=i}}))})),\"allSelect\"===t||\"inverseSelect\"===t?{selected:r}:{name:e.name,selected:r}}function XB(t){t.registerComponentModel(RB),t.registerComponentView(VB),t.registerProcessor(t.PRIORITY.PROCESSOR.SERIES_FILTER,HB),t.registerSubTypeDefaulter(\"legend\",(function(){return\"plain\"})),function(t){t.registerAction(\"legendToggleSelect\",\"legendselectchanged\",H(YB,\"toggleSelected\")),t.registerAction(\"legendAllSelect\",\"legendselectall\",H(YB,\"allSelect\")),t.registerAction(\"legendInverseSelect\",\"legendinverseselect\",H(YB,\"inverseSelect\")),t.registerAction(\"legendSelect\",\"legendselected\",H(YB,\"select\")),t.registerAction(\"legendUnSelect\",\"legendunselected\",H(YB,\"unSelect\"))}(t)}var UB=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.setScrollDataIndex=function(t){this.option.scrollDataIndex=t},e.prototype.init=function(e,n,i){var r=Lp(e);t.prototype.init.call(this,e,n,i),ZB(this,e,r)},e.prototype.mergeOption=function(e,n){t.prototype.mergeOption.call(this,e,n),ZB(this,this.option,e)},e.type=\"legend.scroll\",e.defaultOption=Cc(RB.defaultOption,{scrollDataIndex:0,pageButtonItemGap:5,pageButtonGap:null,pageButtonPosition:\"end\",pageFormatter:\"{current}/{total}\",pageIcons:{horizontal:[\"M0,0L12,-10L12,10z\",\"M0,0L-12,-10L-12,10z\"],vertical:[\"M0,0L20,0L10,-20z\",\"M0,0L20,0L10,20z\"]},pageIconColor:\"#2f4554\",pageIconInactiveColor:\"#aaa\",pageIconSize:15,pageTextStyle:{color:\"#333\"},animationDurationUpdate:800}),e}(RB);function ZB(t,e,n){var i=[1,1];i[t.getOrient().index]=0,kp(e,n,{type:\"box\",ignoreSize:!!i})}var jB=zr,qB=[\"width\",\"height\"],KB=[\"x\",\"y\"],$B=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.newlineDisabled=!0,n._currentIndex=0,n}return n(e,t),e.prototype.init=function(){t.prototype.init.call(this),this.group.add(this._containerGroup=new jB),this._containerGroup.add(this.getContentGroup()),this.group.add(this._controllerGroup=new jB)},e.prototype.resetInner=function(){t.prototype.resetInner.call(this),this._controllerGroup.removeAll(),this._containerGroup.removeClipPath(),this._containerGroup.__rectSize=null},e.prototype.renderInner=function(e,n,i,r,o,a,s){var l=this;t.prototype.renderInner.call(this,e,n,i,r,o,a,s);var u=this._controllerGroup,h=n.get(\"pageIconSize\",!0),c=Y(h)?h:[h,h];d(\"pagePrev\",0);var p=n.getModel(\"pageTextStyle\");function d(t,e){var i=t+\"DataIndex\",o=Hh(n.get(\"pageIcons\",!0)[n.getOrient().name][e],{onclick:W(l._pageGo,l,i,n,r)},{x:-c[0]/2,y:-c[1]/2,width:c[0],height:c[1]});o.name=t,u.add(o)}u.add(new Fs({name:\"pageText\",style:{text:\"xx/xx\",fill:p.getTextColor(),font:p.getFont(),verticalAlign:\"middle\",align:\"center\"},silent:!0})),d(\"pageNext\",1)},e.prototype.layoutInner=function(t,e,n,i,r,o){var a=this.getSelectorGroup(),s=t.getOrient().index,l=qB[s],u=KB[s],h=qB[1-s],c=KB[1-s];r&&Tp(\"horizontal\",a,t.get(\"selectorItemGap\",!0));var p=t.get(\"selectorButtonGap\",!0),d=a.getBoundingRect(),f=[-d.x,-d.y],g=T(n);r&&(g[l]=n[l]-d[l]-p);var y=this._layoutContentAndController(t,i,g,s,l,h,c,u);if(r){if(\"end\"===o)f[s]+=y[l]+p;else{var v=d[l]+p;f[s]-=v,y[u]-=v}y[l]+=d[l]+p,f[1-s]+=y[c]+y[h]/2-d[h]/2,y[h]=Math.max(y[h],d[h]),y[c]=Math.min(y[c],d[c]+f[1-s]),a.x=f[0],a.y=f[1],a.markRedraw()}return y},e.prototype._layoutContentAndController=function(t,e,n,i,r,o,a,s){var l=this.getContentGroup(),u=this._containerGroup,h=this._controllerGroup;Tp(t.get(\"orient\"),l,t.get(\"itemGap\"),i?n.width:null,i?null:n.height),Tp(\"horizontal\",h,t.get(\"pageButtonItemGap\",!0));var c=l.getBoundingRect(),p=h.getBoundingRect(),d=this._showController=c[r]>n[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<y.i)||(y=g,null==p.pagePrevDataIndex&&(p.pagePrevDataIndex=g.i),++p.pageCount,++p.pageIndex),g=v;return p;function m(t){if(t){var e=t.getBoundingRect(),n=e[a]+t[a];return{s:n,e:n+e[o],i:t.__legendDataIndex}}}function x(t,e){return t.e>=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;r<i.length;r++){var o=i[r],a=n.get(o),s=a.dataZoomInfoMap;if(s){var l=e.uid;s.get(l)&&(s.removeKey(l),s.keys().length||nF(n,a))}}}(this.api,this.dataZoomModel),this.range=null},e.type=\"dataZoom.inside\",e}(QE),sF={zoom:function(t,e,n,i){var r=this.range,o=r.slice(),a=t.axisModels[0];if(a){var s=uF[e](null,[i.originX,i.originY],a,n,t),l=(s.signal>0?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;l<a.length;l++){var u=a[l],h=u.getClipPath();h||(h=new zs,u.setClipPath(h)),h.setShape({x:s[l],y:0,width:s[l+1]-s[l],height:r[1]})}this._updateDataInfo(t)},e.prototype._updateDataInfo=function(t){var e=this.dataZoomModel,n=this._displayables,i=n.handleLabels,r=this._orient,o=[\"\",\"\"];if(e.get(\"showDetail\")){var a=e.findRepresentativeAxisProxy();if(a){var s=a.getAxisModel().axis,l=this._range,u=t?a.calculateDataWindow({start:l[0],end:l[1]}).valueWindow:a.getDataValueWindow();o=[this._formatLabel(u[0],s),this._formatLabel(u[1],s)]}}var h=jr(this._handleEnds.slice());function c(t){var e=Eh(n.handles[t].parent,this.group),a=Vh(0===t?\"right\":\"left\",e),s=this._handleWidth/2+5,l=zh([h[t]+(0===t?-s:s),this._size[1]/2],e);i[t].setStyle({x:l[0],y:l[1],verticalAlign:r===dF?\"middle\":a,align:r===dF?a:\"center\",text:o[t]})}c.call(this,0),c.call(this,1)},e.prototype._formatLabel=function(t,e){var n=this.dataZoomModel,i=n.get(\"labelFormatter\"),r=n.get(\"labelPrecision\");null!=r&&\"auto\"!==r||(r=e.getPixelPrecision());var o=null==t||isNaN(t)?\"\":\"category\"===e.type||\"time\"===e.type?e.scale.getLabel({value:Math.round(t)}):t.toFixed(Math.min(r,20));return X(i)?i(t,o):U(i)?i.replace(\"{value}\",o):o},e.prototype._showDataInfo=function(t){t=this._dragging||t;var e=this._displayables,n=e.handleLabels;n[0].attr(\"invisible\",!t),n[1].attr(\"invisible\",!t),e.moveHandle&&this.api[t?\"enterEmphasis\":\"leaveEmphasis\"](e.moveHandle,1)},e.prototype._onDragMove=function(t,e,n,i){this._dragging=!0,de(i.event);var r=zh([e,n],this._displayables.sliderGroup.getLocalTransform(),!0),o=this._updateInterval(t,r[0]),a=this.dataZoomModel.get(\"realtime\");this._updateView(!a),o&&a&&this._dispatchZoomAction(!0)},e.prototype._onDragEnd=function(){this._dragging=!1,this._showDataInfo(!1),!this.dataZoomModel.get(\"realtime\")&&this._dispatchZoomAction(!1)},e.prototype._onClickPanel=function(t){var e=this._size,n=this._displayables.sliderGroup.transformCoordToLocal(t.offsetX,t.offsetY);if(!(n[0]<0||n[0]>e[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]<t[0]&&(e[0]=t[0]),e[1]<t[0]&&(e[1]=t[0]),e},e.prototype.getValueState=function(t){var e=this.option.range,n=this.getExtent();return(e[0]<=n[0]||e[0]<=t)&&(e[1]>=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;a<l&&(!n.length||e[a]<=n[0]);a++)e[a]<n[o]&&r(e[a],\"outOfRange\");for(var u=1;o<s;o++,u=0)u&&i.length&&r(n[o],\"outOfRange\"),r(n[o],\"inRange\");for(u=1;a<l;a++)(!n.length||n[n.length-1]<e[a])&&(u&&(i.length&&r(i[i.length-1].value,\"outOfRange\"),u=0),r(e[a],\"outOfRange\"));var h=i.length;return{stops:i,outerColors:[h?i[0].color:\"transparent\",h?i[h-1].color:\"transparent\"]}},e.type=\"visualMap.continuous\",e.defaultOption=Cc(DF.defaultOption,{align:\"auto\",calculable:!1,hoverLink:!0,realtime:!0,handleIcon:\"path://M-11.39,9.77h0a3.5,3.5,0,0,1-3.5,3.5h-22a3.5,3.5,0,0,1-3.5-3.5h0a3.5,3.5,0,0,1,3.5-3.5h22A3.5,3.5,0,0,1-11.39,9.77Z\",handleSize:\"120%\",handleStyle:{borderColor:\"#fff\",borderWidth:1},indicatorIcon:\"circle\",indicatorSize:\"50%\",indicatorStyle:{borderColor:\"#fff\",borderWidth:2,shadowBlur:2,shadowOffsetX:1,shadowOffsetY:1,shadowColor:\"rgba(0,0,0,0.2)\"}}),e}(DF);function LF(t,e,n){if(n[0]===n[1])return n.slice();for(var i=(n[1]-n[0])/200,r=n[0],o=[],a=0;a<=200&&r<n[1];a++)o.push(r),r+=i;return o.push(n[1]),o}var PF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.autoPositionValues={left:1,right:1,top:1,bottom:1},n}return n(e,t),e.prototype.init=function(t,e){this.ecModel=t,this.api=e},e.prototype.render=function(t,e,n,i){this.visualMapModel=t,!1!==t.get(\"show\")?this.doRender(t,e,n,i):this.group.removeAll()},e.prototype.renderBackground=function(t){var e=this.visualMapModel,n=fp(e.get(\"padding\")||0),i=t.getBoundingRect();t.add(new zs({z2:-1,silent:!0,shape:{x:i.x-n[3],y:i.y-n[0],width:i.width+n[3]+n[1],height:i.height+n[0]+n[2]},style:{fill:e.get(\"backgroundColor\"),stroke:e.get(\"borderColor\"),lineWidth:e.get(\"borderWidth\")}}))},e.prototype.getControllerVisual=function(t,e,n){var i=(n=n||{}).forceState,r=this.visualMapModel,o={};if(\"color\"===e){var a=r.get(\"contentColor\");o.color=a}function s(t){return o[t]}function l(t,e){o[t]=e}var u=r.controllerVisuals[i||r.getValueState(t)];return E(_D.prepareVisualTypes(u),(function(i){var r=u[i];n.convertOpacityToAlpha&&\"opacity\"===i&&(i=\"colorAlpha\",r=u.__alphaForOpacity),_D.dependsOn(i,e)&&r&&r.applyVisual(t,s,l)})),o[e]},e.prototype.positionGroup=function(t){var e=this.visualMapModel,n=this.api;Dp(t,e.getBoxLayoutParams(),{width:n.getWidth(),height:n.getHeight()})},e.prototype.doRender=function(t,e,n,i){},e.type=\"visualMap\",e}(Tg),OF=[[\"left\",\"right\",\"width\"],[\"top\",\"bottom\",\"height\"]];function RF(t,e,n){var i=t.option,r=i.align;if(null!=r&&\"auto\"!==r)return r;for(var o={width:e.getWidth(),height:e.getHeight()},a=\"horizontal\"===i.orient?1:0,s=OF[a],l=[0,null,10],u={},h=0;h<3;h++)u[OF[1-a][h]]=l[h],u[s[h]]=2===h?n[0]:i[s[h]];var c=[[\"x\",\"width\",3],[\"y\",\"height\",0]][a],p=Cp(u,o,i.padding);return s[(p.margin[c[2]]||0)+p[c[0]]+.5*p[c[1]]<.5*o[c[1]]?0:1]}function NF(t,e){return E(t||[],(function(t){null!=t.dataIndex&&(t.dataIndexInside=t.dataIndex,t.dataIndex=null),t.highlightKey=\"visualMap\"+(e?e.componentIndex:\"\")})),t}var EF=Xr,zF=E,VF=Math.min,BF=Math.max,FF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._shapes={},n._dataInterval=[],n._handleEnds=[],n._hoverLinkDataIndices=[],n}return n(e,t),e.prototype.doRender=function(t,e,n,i){this._api=n,i&&\"selectDataRange\"===i.type&&i.from===this.uid||this._buildView()},e.prototype._buildView=function(){this.group.removeAll();var t=this.visualMapModel,e=this.group;this._orient=t.get(\"orient\"),this._useHandle=t.get(\"calculable\"),this._resetInterval(),this._renderBar(e);var n=t.get(\"text\");this._renderEndsText(e,n,0),this._renderEndsText(e,n,1),this._updateView(!0),this.renderBackground(e),this._updateView(),this._enableHoverLinkToSeries(),this._enableHoverLinkFromSeries(),this.positionGroup(e)},e.prototype._renderEndsText=function(t,e,n){if(e){var i=e[1-n];i=null!=i?i+\"\":\"\";var r=this.visualMapModel,o=r.get(\"textGap\"),a=r.itemSize,s=this._shapes.mainGroup,l=this._applyTransform([a[0]/2,0===n?-o:a[1]+o],s),u=this._applyTransform(0===n?\"bottom\":\"top\",s),h=this._orient,c=this.visualMapModel.textStyleModel;this.group.add(new Fs({style:nc(c,{x:l[0],y:l[1],verticalAlign:\"horizontal\"===h?\"middle\":u,align:\"horizontal\"===h?u:\"center\",text:i})}))}},e.prototype._renderBar=function(t){var e=this.visualMapModel,n=this._shapes,i=e.itemSize,r=this._orient,o=this._useHandle,a=RF(e,this.api,i),s=n.mainGroup=this._createBarGroup(a),l=new zr;s.add(l),l.add(n.outOfRange=GF()),l.add(n.inRange=GF(null,o?HF(this._orient):null,W(this._dragHandle,this,\"all\",!1),W(this._dragHandle,this,\"all\",!0))),l.setClipPath(new zs({shape:{x:0,y:0,width:i[0],height:i[1],r:3}}));var u=e.textStyleModel.getTextRect(\"国\"),h=BF(u.width,u.height);o&&(n.handleThumbs=[],n.handleLabels=[],n.handleLabelPoints=[],this._createHandle(e,s,0,i,h,r),this._createHandle(e,s,1,i,h,r)),this._createIndicator(e,s,i,h,r),t.add(s)},e.prototype._createHandle=function(t,e,n,i,r,o){var a=W(this._dragHandle,this,n,!1),s=W(this._dragHandle,this,n,!0),l=Ir(t.get(\"handleSize\"),i[0]),u=Wy(t.get(\"handleIcon\"),-l/2,-l/2,l,l,null,!0),h=HF(this._orient);u.attr({cursor:h,draggable:!0,drift:a,ondragend:s,onmousemove:function(t){de(t.event)}}),u.x=i[0]/2,u.useStyle(t.getModel(\"handleStyle\").getItemStyle()),u.setStyle({strokeNoScale:!0,strokeFirst:!0}),u.style.lineWidth*=2,u.ensureState(\"emphasis\").style=t.getModel([\"emphasis\",\"handleStyle\"]).getItemStyle(),ql(u,!0),e.add(u);var c=this.visualMapModel.textStyleModel,p=new Fs({cursor:h,draggable:!0,drift:a,onmousemove:function(t){de(t.event)},ondragend:s,style:nc(c,{x:0,y:0,text:\"\"})});p.ensureState(\"blur\").style={opacity:.1},p.stateTransition={duration:200},this.group.add(p);var d=[l,0],f=this._shapes;f.handleThumbs[n]=u,f.handleLabelPoints[n]=d,f.handleLabels[n]=p},e.prototype._createIndicator=function(t,e,n,i,r){var o=Ir(t.get(\"indicatorSize\"),n[0]),a=Wy(t.get(\"indicatorIcon\"),-o/2,-o/2,o,o,null,!0);a.attr({cursor:\"move\",invisible:!0,silent:!0,x:n[0]/2});var s=t.getModel(\"indicatorStyle\").getItemStyle();if(a instanceof ks){var l=a.style;a.useStyle(A({image:l.image,x:l.x,y:l.y,width:l.width,height:l.height},s))}else a.useStyle(s);e.add(a);var u=this.visualMapModel.textStyleModel,h=new Fs({silent:!0,invisible:!0,style:nc(u,{x:0,y:0,text:\"\"})});this.group.add(h);var c=[(\"horizontal\"===r?i/2:6)+n[0]/2,0],p=this._shapes;p.indicator=a,p.indicatorLabel=h,p.indicatorLabelPoint=c,this._firstShowIndicator=!0},e.prototype._dragHandle=function(t,e,n,i){if(this._useHandle){if(this._dragging=!e,!e){var r=this._applyTransform([n,i],this._shapes.mainGroup,!0);this._updateInterval(t,r[1]),this._hideIndicator(),this._updateView()}e===!this.visualMapModel.get(\"realtime\")&&this.api.dispatchAction({type:\"selectDataRange\",from:this.uid,visualMapId:this.visualMapModel.id,selected:this._dataInterval.slice()}),e?!this._hovering&&this._clearHoverLinkToSeries():WF(this.visualMapModel)&&this._doHoverLinkToSeries(this._handleEnds[t],!1)}},e.prototype._resetInterval=function(){var t=this.visualMapModel,e=this._dataInterval=t.getSelected(),n=t.getExtent(),i=[0,t.itemSize[1]];this._handleEnds=[EF(e[0],n,i,!0),EF(e[1],n,i,!0)]},e.prototype._updateInterval=function(t,e){e=e||0;var n=this.visualMapModel,i=this._handleEnds,r=[0,n.itemSize[1]];Ck(e,i,r,t,0);var o=n.getExtent();this._dataInterval=[EF(i[0],r,o,!0),EF(i[1],r,o,!0)]},e.prototype._updateView=function(t){var e=this.visualMapModel,n=e.getExtent(),i=this._shapes,r=[0,e.itemSize[1]],o=t?r:this._handleEnds,a=this._createBarVisual(this._dataInterval,n,o,\"inRange\"),s=this._createBarVisual(n,n,r,\"outOfRange\");i.inRange.setStyle({fill:a.barColor}).setShape(\"points\",a.barPoints),i.outOfRange.setStyle({fill:s.barColor}).setShape(\"points\",s.barPoints),this._updateHandle(o,a)},e.prototype._createBarVisual=function(t,e,n,i){var r={forceState:i,convertOpacityToAlpha:!0},o=this._makeColorGradient(t,r),a=[this.getControllerVisual(t[0],\"symbolSize\",r),this.getControllerVisual(t[1],\"symbolSize\",r)],s=this._createBarPoints(n,a);return{barColor:new nh(0,0,0,1,o),barPoints:s,handlesColor:[o[0].color,o[o.length-1].color]}},e.prototype._makeColorGradient=function(t,e){var n=[],i=(t[1]-t[0])/100;n.push({color:this.getControllerVisual(t[0],\"color\",e),offset:0});for(var r=1;r<100;r++){var o=t[0]+i*r;if(o>t[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;S<w.length;S++)this._api.enterBlur(w[S])}},e.prototype._enableHoverLinkToSeries=function(){var t=this;this._shapes.mainGroup.on(\"mousemove\",(function(e){if(t._hovering=!0,!t._dragging){var n=t.visualMapModel.itemSize,i=t._applyTransform([e.offsetX,e.offsetY],t._shapes.mainGroup,!0,!0);i[1]=VF(BF(0,i[1]),n[1]),t._doHoverLinkToSeries(i[1],0<=i[0]&&i[0]<=n[0])}})).on(\"mouseout\",(function(){t._hovering=!1,!t._dragging&&t._clearHoverLinkToSeries()}))},e.prototype._enableHoverLinkFromSeries=function(){var t=this.api.getZr();this.visualMapModel.option.hoverLink?(t.on(\"mouseover\",this._hoverLinkFromSeriesMouseOver,this),t.on(\"mouseout\",this._hideIndicator,this)):this._clearHoverLinkFromSeries()},e.prototype._doHoverLinkToSeries=function(t,e){var n=this.visualMapModel,i=n.itemSize;if(n.option.hoverLink){var r=[0,i[1]],o=n.getExtent();t=VF(BF(r[0],t),r[1]);var a=function(t,e,n){var i=6,r=t.get(\"hoverLinkDataSize\");r&&(i=EF(r,e,n,!0)/2);return i}(n,o,r),s=[t-a,t+a],l=EF(t,r,o,!0),u=[EF(s[0],r,o,!0),EF(s[1],r,o,!0)];s[0]<r[0]&&(u[0]=-1/0),s[1]>r[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<r;i++){var o=Ao(t[i].seriesId,null);if(null==o)return;for(var a=bo(t[i].dataIndex),s=n&&n[o],l=0,u=a.length;l<u;l++){var h=a[l];s&&s[h]?s[h]=null:(e[o]||(e[o]={}))[h]=1}}}function o(t,e){var n=[];for(var i in t)if(t.hasOwnProperty(i)&&null!=t[i])if(e)n.push(+i);else{var r=o(t[i],!0);r.length&&n.push({seriesId:i,dataIndex:r})}return n}}(h,c);this._dispatchHighDown(\"downplay\",NF(p[0],n)),this._dispatchHighDown(\"highlight\",NF(p[1],n))}},e.prototype._hoverLinkFromSeriesMouseOver=function(t){var e;if(ky(t.target,(function(t){var n=Qs(t);if(null!=n.dataIndex)return e=n,!0}),!0),e){var n=this.ecModel.getSeriesByIndex(e.seriesIndex),i=this.visualMapModel;if(i.isTargetSeries(n)){var r=n.getData(e.dataType),o=r.getStore().get(i.getDataDimensionIndex(r),e.dataIndex);isNaN(o)||this._showIndicator(o,o)}}},e.prototype._hideIndicator=function(){var t=this._shapes;t.indicator&&t.indicator.attr(\"invisible\",!0),t.indicatorLabel&&t.indicatorLabel.attr(\"invisible\",!0);var e=this._shapes.handleLabels;if(e)for(var n=0;n<e.length;n++)this._api.leaveBlur(e[n])},e.prototype._clearHoverLinkToSeries=function(){this._hideIndicator();var t=this._hoverLinkDataIndices;this._dispatchHighDown(\"downplay\",NF(t,this.visualMapModel)),t.length=0},e.prototype._clearHoverLinkFromSeries=function(){this._hideIndicator();var t=this.api.getZr();t.off(\"mouseover\",this._hoverLinkFromSeriesMouseOver),t.off(\"mouseout\",this._hideIndicator)},e.prototype._applyTransform=function(t,e,n,i){var r=Eh(e,i?null:this.group);return Y(t)?zh(t,r,n):Vh(t,r,n)},e.prototype._dispatchHighDown=function(t,e){e&&e.length&&this.api.dispatchAction({type:t,batch:e})},e.prototype.dispose=function(){this._clearHoverLinkFromSeries(),this._clearHoverLinkToSeries()},e.prototype.remove=function(){this._clearHoverLinkFromSeries(),this._clearHoverLinkToSeries()},e.type=\"visualMap.continuous\",e}(PF);function GF(t,e,n,i){return new Wu({shape:{points:t},draggable:!!n,cursor:e,drift:n,onmousemove:function(t){de(t.event)},ondragend:i})}function WF(t){var e=t.get(\"hoverLinkOnHandle\");return!!(null==e?t.get(\"realtime\"):e)}function HF(t){return\"vertical\"===t?\"ns-resize\":\"ew-resize\"}var YF={type:\"selectDataRange\",event:\"dataRangeSelected\",update:\"update\"},XF=function(t,e){e.eachComponent({mainType:\"visualMap\",query:t},(function(e){e.setSelected(t.selected)}))},UF=[{createOnAllSeries:!0,reset:function(t,e){var n=[];return e.eachComponent(\"visualMap\",(function(e){var i,r,o,a,s,l=t.pipelineContext;!e.isTargetSeries(t)||l&&l.large||n.push((i=e.stateList,r=e.targetVisuals,o=W(e.getValueState,e),a=e.getDataDimensionIndex(t.getData()),s={},E(i,(function(t){var e=_D.prepareVisualTypes(r[t]);s[t]=e})),{progress:function(t,e){var n,i;function l(t){return Iy(e,i,t)}function u(t,n){Cy(e,i,t,n)}null!=a&&(n=e.getDimensionIndex(a));for(var h=e.getStore();null!=(i=t.next());){var c=e.getRawDataItem(i);if(!c||!1!==c.visualMap)for(var p=null!=a?h.get(n,i):i,d=o(p),f=r[d],g=s[d],y=0,v=g.length;y<v;y++){var m=g[y];f[m]&&f[m].applyVisual(p,l,u)}}}}))})),n}},{createOnAllSeries:!0,reset:function(t,e){var n=t.getData(),i=[];e.eachComponent(\"visualMap\",(function(e){if(e.isTargetSeries(t)){var r=e.getVisualMeta(W(ZF,null,t,e))||{stops:[],outerColors:[]},o=e.getDataDimensionIndex(n);o>=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;s<l;s++){var u=o[s],h=r[\"opacity\"===u?\"__alphaForOpacity\":u];h&&h.applyVisual(n,c,p)}return a.color;function c(t){return a[t]}function p(t,e){a[t]=e}}var jF=E;function qF(t){var e=t&&t.visualMap;Y(e)||(e=e?[e]:[]),jF(e,(function(t){if(t){KF(t,\"splitList\")&&!KF(t,\"pieces\")&&(t.pieces=t.splitList,delete t.splitList);var e=t.pieces;e&&Y(e)&&jF(e,(function(t){q(t)&&(KF(t,\"start\")&&!KF(t,\"min\")&&(t.min=t.start),KF(t,\"end\")&&!KF(t,\"max\")&&(t.max=t.end))}))}}))}function KF(t,e){return t&&t.hasOwnProperty&&t.hasOwnProperty(e)}var $F=!1;function JF(t){$F||($F=!0,t.registerSubTypeDefaulter(\"visualMap\",(function(t){return t.categories||(t.pieces?t.pieces.length>0: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<r;s+=o,a++){var l=a===r-1?i[1]:s+o;t.push({interval:[s,l],close:[1,1]})}e.maxOpen&&t.push({interval:[i[1],1/0],close:[0,0]}),uo(t),E(t,(function(t,e){t.index=e,t.text=this.formatValueText(t.interval)}),this)},categories:function(t){var e=this.option;E(e.categories,(function(e){t.push({text:this.formatValueText(e,!0),value:e})}),this),nG(e,t)},pieces:function(t){var e=this.option;E(e.pieces,(function(e,n){q(e)||(e={value:e});var i={text:\"\",index:n};if(null!=e.label&&(i.text=e.label),e.hasOwnProperty(\"value\")){var r=i.value=e.value;i.interval=[r,r],i.close=[1,1]}else{for(var o=i.interval=[],a=i.close=[0,0],s=[1,0,1],l=[-1/0,1/0],u=[],h=0;h<2;h++){for(var c=[[\"gte\",\"gt\",\"min\"],[\"lte\",\"lt\",\"max\"]][h],p=0;p<3&&null==o[h];p++)o[h]=e[c[p]],a[h]=s[p],u[h]=2===p;null==o[h]&&(o[h]=l[h])}u[0]&&o[1]===1/0&&(a[0]=0),u[1]&&o[0]===-1/0&&(a[1]=0),o[0]===o[1]&&a[0]&&a[1]&&(i.value=o[0])}i.visual=_D.retrieveVisuals(e),t.push(i)}),this),nG(e,t),uo(t),E(t,(function(t){var e=t.close,n=[[\"<\",\"≤\"][e[1]],[\">\",\"≥\"][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(n<c){var i=void 0,a=e.get(\"name\")?\"withName\":\"withoutName\";i=r(i=l>1?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<s.count();g++)if(g<u){var y=s.getName(g),v=s.getValues(g),m=o.get([\"data\",y?\"withName\":\"withoutName\"]);f.push(r(m,{name:y,value:v.join(h)}))}i+=f.join(h)+p,d.push(i)}var x}));var f=o.getModel([\"series\",\"multiple\",\"separator\"]),g=f.get(\"middle\"),y=f.get(\"end\");s+=d.join(g)+y,a.setAttribute(\"aria-label\",s)}()}function r(t,e){if(!U(t))return t;var n=t;return E(e,(function(t,e){n=n.replace(new RegExp(\"\\\\{\\\\s*\"+e+\"\\\\s*\\\\}\",\"g\"),t)})),n}}function uG(t){if(t&&t.aria){var e=t.aria;null!=e.show&&(e.enabled=e.show),e.label=e.label||{},E([\"description\",\"general\",\"series\",\"data\"],(function(t){null!=e[t]&&(e.label[t]=e[t])}))}}var hG={value:\"eq\",\"<\":\"lt\",\"<=\":\"lte\",\">\":\"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;e<t.length;e++)if(!t[e].evaluate())return!1;return!0},t}(),fG=function(){function t(){}return t.prototype.evaluate=function(){for(var t=this.children,e=0;e<t.length;e++)if(t[e].evaluate())return!0;return!1},t}(),gG=function(){function t(){}return t.prototype.evaluate=function(){return!this.child.evaluate()},t}(),yG=function(){function t(){}return t.prototype.evaluate=function(){for(var t=!!this.valueParser,e=(0,this.getValue)(this.valueGetterParam),n=t?this.valueParser(e):null,i=0;i<this.subCondList.length;i++)if(!this.subCondList[i].evaluate(t?n:e))return!1;return!0},t}();function vG(t,e){if(!0===t||!1===t){var n=new pG;return n.value=t,n}var i=\"\";return xG(t)||vo(i),t.and?mG(\"and\",t,e):t.or?mG(\"or\",t,e):t.not?function(t,e){var n=t.not,i=\"\";0;xG(n)||vo(i);var r=new gG;r.child=vG(n,e),r.child||vo(i);return r}(t,e):function(t,e){for(var n=\"\",i=e.prepareGetValue(t),r=[],o=G(t),a=t.parser,s=a?Mf(a):null,l=0;l<o.length;l++){var u=o[l];if(\"parser\"!==u&&!e.valueGetterAttrMap.get(u)){var h=_t(hG,u)?hG[u]:u,c=t[u],p=s?s(c):c,d=Af(h,p)||\"reg\"===h&&new cG(p);d||vo(n),r.push(d)}}r.length||vo(n);var f=new yG;return f.valueGetterParam=i,f.valueParser=s,f.getValue=e.getValue,f.subCondList=r,f}(t,e)}function mG(t,e,n){var i=e[t],r=\"\";Y(i)||vo(r),i.length||vo(r);var o=\"and\"===t?new dG:new fG;return o.children=z(i,(function(t){return vG(t,n)})),o.children.length||vo(r),o}function xG(t){return q(t)&&!N(t)}var _G=function(){function t(t,e){this._cond=vG(t,e)}return t.prototype.evaluate=function(){return this._cond.evaluate()},t}();var bG={type:\"echarts:filter\",transform:function(t){for(var e,n,i,r=t.upstream,o=(n=t.config,i={valueGetterAttrMap:yt({dimension:!0}),prepareGetValue:function(t){var e=\"\",n=t.dimension;_t(t,\"dimension\")||vo(e);var i=r.getDimensionInfo(n);return i||vo(e),{dimIdx:i.index}},getValue:function(t){return r.retrieveValueFromItem(e,t.dimIdx)}},new _G(n,i)),a=[],s=0,l=r.count();s<l;s++)e=r.getRawDataItem(s),o.evaluate()&&a.push(e);return{data:a}}};var wG={type:\"echarts:sort\",transform:function(t){var e=t.upstream,n=t.config,i=\"\",r=bo(n);r.length||vo(i);var o=[];E(r,(function(t){var n=t.dimension,r=t.order,a=t.parser,s=t.incomparable;if(null==n&&vo(i),\"asc\"!==r&&\"desc\"!==r&&vo(i),s&&\"min\"!==s&&\"max\"!==s){var l=\"\";0,vo(l)}if(\"asc\"!==r&&\"desc\"!==r){var u=\"\";0,vo(u)}var h=e.getDimensionInfo(n);h||vo(i);var c=a?Mf(a):null;a&&!c&&vo(i),o.push({dimIdx:h.index,parser:c,comparator:new Cf(r,s)})}));var a=e.sourceFormat;a!==Fp&&a!==Gp&&vo(i);for(var s=[],l=0,u=e.count();l<u;l++)s.push(e.getRawDataItem(l));return s.sort((function(t,n){for(var i=0;i<o.length;i++){var r=o[i],a=e.retrieveValueFromItem(t,r.dimIdx),s=e.retrieveValueFromItem(n,r.dimIdx);r.parser&&(a=r.parser(a),s=r.parser(s));var l=r.comparator.evaluate(a,s);if(0!==l)return l}return 0})),{data:s}}};var SG=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=\"dataset\",e}return n(e,t),e.prototype.init=function(e,n,i){t.prototype.init.call(this,e,n,i),this._sourceManager=new jf(this),qf(this)},e.prototype.mergeOption=function(e,n){t.prototype.mergeOption.call(this,e,n),qf(this)},e.prototype.optionUpdated=function(){this._sourceManager.dirty()},e.prototype.getSourceManager=function(){return this._sourceManager},e.type=\"dataset\",e.defaultOption={seriesLayoutBy:Xp},e}(Rp),MG=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=\"dataset\",e}return n(e,t),e.type=\"dataset\",e}(Tg);var IG=os.CMD;function TG(t,e){return Math.abs(t-e)<1e-5}function CG(t){var e,n,i,r,o,a=t.data,s=t.len(),l=[],u=0,h=0,c=0,p=0;function d(t,n){e&&e.length>2&&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=n<t?-1:1,h=Math.cos(t),c=Math.sin(t),p=Math.cos(n),d=Math.sin(n),f=h*o+i,g=c*a+r,y=p*o+i,v=d*a+r,m=o*l*u,x=a*l*u;e.push(f-m*c,g+x*h,y+m*d,v-x*p,y,v)}for(var y=0;y<s;){var v=a[y++],m=1===y;switch(m&&(c=u=a[y],p=h=a[y+1],v!==IG.L&&v!==IG.C&&v!==IG.Q||(e=[c,p])),v){case IG.M:u=c=a[y++],h=p=a[y++],d(c,p);break;case IG.L:f(u,h,n=a[y++],i=a[y++]),u=n,h=i;break;case IG.C:e.push(a[y++],a[y++],a[y++],a[y++],u=a[y++],h=a[y++]);break;case IG.Q:n=a[y++],i=a[y++],r=a[y++],o=a[y++],e.push(u+2/3*(n-u),h+2/3*(i-h),r+2/3*(n-r),o+2/3*(i-o),r,o),u=r,h=o;break;case IG.A:var x=a[y++],_=a[y++],b=a[y++],w=a[y++],S=a[y++],M=a[y++]+S;y+=1;var I=!a[y++];n=Math.cos(S)*b+x,i=Math.sin(S)*w+_,m?d(c=n,p=i):f(u,h,n,i),u=Math.cos(M)*b+x,h=Math.sin(M)*w+_;for(var T=(I?-1:1)*Math.PI/2,C=S;I?C>M:C<M;C+=T){g(C,I?Math.max(C+T,M):Math.min(C+T,M),x,_,b,w)}break;case IG.R:c=u=a[y++],p=h=a[y++],n=c+a[y++],i=p+a[y++],d(n,p),f(n,p,n,i),f(n,i,c,i),f(c,i,c,p),f(c,p,n,p);break;case IG.Z:e&&f(u,h,c,p),u=c,h=p}}return e&&e.length>2&&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<c&&_<c)l.push(a,s);else{var b=p*g+d*y,w=-p*v-d*m;if(x-b*b<c&&b>=0&&_-w*w<c&&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;u<a;u++)l.push(s);var h=n-a*s;if(h>0)for(u=0;u<h;u++)l[u%a]+=1;return l}function kG(t,e,n){for(var i=t.r0,r=t.r,o=t.startAngle,a=t.endAngle,s=Math.abs(a-o),l=s*r,u=r-i,h=l>Math.abs(u),c=AG([l,u],h?0:1,e),p=(h?s:u)/c.length,d=0;d<c.length;d++)for(var f=(h?u:s)/c[d],g=0;g<c[d];g++){var y={};h?(y.startAngle=o+p*d,y.endAngle=o+p*(d+1),y.r0=i+f*g,y.r=i+f*(g+1)):(y.startAngle=o+f*g,y.endAngle=o+f*(g+1),y.r0=i+p*d,y.r=i+p*(d+1)),y.clockwise=t.clockwise,y.cx=t.cx,y.cy=t.cy,n.push(y)}}function LG(t,e,n,i){return t*i-n*e}function PG(t,e,n,i,r,o,a,s){var l=n-t,u=i-e,h=a-r,c=s-o,p=LG(h,c,l,u);if(Math.abs(p)<1e-6)return null;var d=LG(t-r,e-o,h,c)/p;return d<0||d>1?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;o<i;o++){var a=t[o],s=t[(o+1)%i],l=PG(a[0],a[1],s[0],s[1],e.x,e.y,n.x,n.y);l&&r.push({projPt:OG(l,e,n),pt:l,idx:o})}if(r.length<2)return[{points:t},{points:t}];r.sort((function(t,e){return t.projPt-e.projPt}));var u=r[0],h=r[r.length-1];if(h.idx<u.idx){var c=u;u=h,h=c}var p=[u.pt.x,u.pt.y],d=[h.pt.x,h.pt.y],f=[p],g=[d];for(o=u.idx+1;o<=h.idx;o++)RG(f,t[o].slice());for(RG(f,d),RG(f,p),o=h.idx+1;o<=u.idx+i;o++)RG(g,t[o%i].slice());return RG(g,p),RG(g,d),[{points:f},{points:g}]}(e,u,h)}function EG(t,e,n,i){if(1===n)i.push(e);else{var r=Math.floor(n/2),o=t(e);EG(t,o[0],r,i),EG(t,o[1],n-r,i)}return i}function zG(t,e){e.setStyle(t.style),e.z=t.z,e.z2=t.z2,e.zlevel=t.zlevel}function VG(t,e){var n,i=[],r=t.shape;switch(t.type){case\"rect\":!function(t,e,n){for(var i=t.width,r=t.height,o=i>r,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;p<a.length;p++)for(var d=t[l]/a[p],f=0;f<a[p];f++){var g={};g[u]=p*c,g[h]=f*d,g[s]=c,g[l]=d,g.x+=t.x,g.y+=t.y,n.push(g)}}(r,e,i),n=zs;break;case\"sector\":kG(r,e,i),n=zu;break;case\"circle\":kG({r0:0,r:r.r,startAngle:0,endAngle:2*Math.PI,cx:r.cx,cy:r.cy},e,i),n=zu;break;default:var o=t.getComputedTransform(),a=o?Math.sqrt(Math.max(o[0]*o[0]+o[1]*o[1],o[2]*o[2]+o[3]*o[3])):1,s=z(function(t,e){var n=CG(t),i=[];e=e||1;for(var r=0;r<n.length;r++){var o=n[r],a=[],s=o[0],l=o[1];a.push(s,l);for(var u=2;u<o.length;){var h=o[u++],c=o[u++],p=o[u++],d=o[u++],f=o[u++],g=o[u++];DG(s,l,h,c,p,d,f,g,a,e),s=f,l=g}i.push(a)}return i}(t.getUpdatedPathProxy(),a),(function(t){return function(t){for(var e=[],n=0;n<t.length;)e.push([t[n++],t[n++]]);return e}(t)})),l=s.length;if(0===l)EG(NG,{points:s[0]},e,i);else if(l===e)for(var u=0;u<l;u++)i.push({points:s[u]});else{var h=0,c=z(s,(function(t){var e=[],n=[];Ra(t,e,n);var i=(n[1]-e[1])*(n[0]-e[0]);return h+=i,{poly:t,area:i}}));c.sort((function(t,e){return e.area-t.area}));var p=e;for(u=0;u<l;u++){var d=c[u];if(p<=0)break;var f=u===l-1?p:Math.ceil(d.area/h*e);f<0||(EG(NG,{points:d.poly},f,i),p-=f)}}n=Wu}if(!n)return function(t,e){for(var n=[],i=0;i<e;i++)n.push(mu(t));return n}(t,e);var g=[];for(u=0;u<i.length;u++){var y=new n;y.setShape(i[u]),zG(t,y),g.push(y)}return g}function BG(t,e){var n=t.length,i=e.length;if(n===i)return[t,e];for(var r=[],o=[],a=n<i?t:e,s=Math.min(n,i),l=Math.abs(i-n)/6,u=(s-2)/6,h=Math.ceil(l/u)+1,c=[a[0],a[1]],p=l,d=2;d<s;){var f=a[d-2],g=a[d-1],y=a[d++],v=a[d++],m=a[d++],x=a[d++],_=a[d++],b=a[d++];if(p<=0)c.push(y,v,m,x,_,b);else{for(var w=Math.min(p,h-1)+1,S=1;S<=w;S++){var M=S/w;wn(f,y,m,_,M,r),wn(g,v,x,b,M,o),f=r[3],g=o[3],c.push(r[1],o[1],r[2],o[2],f,g),y=r[5],v=o[5],m=r[6],x=o[6]}p-=w-1}}return a===t?[c,e]:[t,c]}function FG(t,e){for(var n=t.length,i=t[n-2],r=t[n-1],o=[],a=0;a<e.length;)o[a++]=i,o[a++]=r;return o}function GG(t){for(var e=0,n=0,i=0,r=t.length,o=0,a=r-2;o<r;a=o,o+=2){var s=t[a],l=t[a+1],u=t[o],h=t[o+1],c=s*h-u*l;e+=c,n+=(s+u)*c,i+=(l+h)*c}return 0===e?[t[0]||0,t[1]||0]:[n/e/3,i/e/3,e]}function WG(t,e,n,i){for(var r=(t.length-2)/6,o=1/0,a=0,s=t.length,l=s-2,u=0;u<r;u++){for(var h=6*u,c=0,p=0;p<s;p+=2){var d=0===p?h:(h+p-2)%l+2,f=t[d]-n[0],g=t[d+1]-n[1],y=e[p]-i[0]-f,v=e[p+1]-i[1]-g;c+=y*y+v*v}c<o&&(o=c,a=u)}return a}function HG(t){for(var e=[],n=t.length,i=0;i<n;i+=2)e[i]=t[n-i-2],e[i+1]=t[n-i-1];return e}function YG(t){return t.__isCombineMorphing}var XG=\"__mOriginal_\";function UG(t,e,n){var i=XG+e,r=t[i]||t[e];t[i]||(t[i]=t[e]);var o=n.replace,a=n.after,s=n.before;t[e]=function(){var t,e=arguments;return s&&s.apply(this,e),t=o?o.apply(this,e):r.apply(this,e),a&&a.apply(this,e),t}}function ZG(t,e){var n=XG+e;t[n]&&(t[e]=t[n],t[n]=null)}function jG(t,e){for(var n=0;n<t.length;n++)for(var i=t[n],r=0;r<i.length;){var o=i[r],a=i[r+1];i[r++]=e[0]*o+e[2]*a+e[4],i[r++]=e[1]*o+e[3]*a+e[5]}}function qG(t,e){var n=t.getUpdatedPathProxy(),i=e.getUpdatedPathProxy(),r=function(t,e){for(var n,i,r,o=[],a=[],s=0;s<Math.max(t.length,e.length);s++){var l=t[s],u=e[s],h=void 0,c=void 0;l?u?(i=h=(n=BG(l,u))[0],r=c=n[1]):(c=FG(r||l,l),h=l):(h=FG(i||u,u),c=u),o.push(h),a.push(c)}return[o,a]}(CG(n),CG(i)),o=r[0],a=r[1],s=t.getComputedTransform(),l=e.getComputedTransform();s&&jG(o,s),l&&jG(a,l),UG(e,\"updateTransform\",{replace:function(){this.transform=null}}),e.transform=null;var u=function(t,e,n,i){for(var r,o=[],a=0;a<t.length;a++){var s=t[a],l=e[a],u=GG(s),h=GG(l);null==r&&(r=u[2]<0!=h[2]<0);var c=[],p=[],d=0,f=1/0,g=[],y=s.length;r&&(s=HG(s));for(var v=6*WG(s,l,u,h),m=y-2,x=0;x<m;x+=2){var _=(v+x)%m+2;c[x+2]=s[_]-u[0],c[x+3]=s[_+1]-u[1]}if(c[0]=s[v]-u[0],c[1]=s[v+1]-u[1],n>0)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;x<s.length;x+=2){var T=c[x],C=c[x+1],D=l[x]-h[0],A=l[x+1]-h[1],k=D*M-A*S,L=D*S+A*M;g[x]=k,g[x+1]=L;var P=k-T,O=L-C;I+=P*P+O*O}if(I<f){f=I,d=w;for(var R=0;R<g.length;R++)p[R]=g[R]}}else for(var N=0;N<y;N+=2)p[N]=l[N]-h[0],p[N+1]=l[N+1]-h[1];o.push({from:c,to:p,fromCp:u,toCp:h,rotation:-d})}return o}(o,a,10,Math.PI),h=[];UG(e,\"buildPath\",{replace:function(t){for(var n=e.__morphT,i=1-n,r=[],o=0;o<u.length;o++){var a=u[o],s=a.from,l=a.to,c=a.rotation*n,p=a.fromCp,d=a.toCp,f=Math.sin(c),g=Math.cos(c);Gt(r,p,d,n);for(var y=0;y<s.length;y+=2){var v=s[y],m=s[y+1],x=v*i+(S=l[y])*n,_=m*i+(M=l[y+1])*n;h[y]=x*g-_*f+r[0],h[y+1]=x*f+_*g+r[1]}var b=h[0],w=h[1];t.moveTo(b,w);for(y=2;y<s.length;){var S=h[y++],M=h[y++],I=h[y++],T=h[y++],C=h[y++],D=h[y++];b===S&&w===M&&I===C&&T===D?t.lineTo(C,D):t.bezierCurveTo(S,M,I,T,C,D),b=C,w=D}}}})}function KG(t,e,n){if(!t||!e)return e;var i=n.done,r=n.during;return qG(t,e),e.__morphT=0,e.animateTo({__morphT:1},k({during:function(t){e.dirtyShape(),r&&r(t)},done:function(){ZG(e,\"buildPath\"),ZG(e,\"updateTransform\"),e.__morphT=-1,e.createPathProxy(),e.dirtyShape(),i&&i()}},n)),e}function $G(t,e,n,i,r,o){t=r===n?0:Math.round(32767*(t-n)/(r-n)),e=o===i?0:Math.round(32767*(e-i)/(o-i));for(var a,s=0,l=32768;l>0;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<i;r++)n.push({one:t[r],many:[]});for(r=0;r<e.length;r++){var o=e[r].length,a=void 0;for(a=0;a<o;a++)n[a%i].many.push(e[r][a])}var s=0;for(r=i-1;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;i<t.count;i++){var r=mu(t.path);r.setStyle(\"opacity\",n),e.push(r)}return e},split:null};function iW(t,e,n,i,r,o){if(t.length&&e.length){var a=ph(\"update\",i,r);if(a&&a.duration>0){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;f<p.length;f++)d+=p[f].many.length;var g=0;for(f=0;f<p.length;f++)y(p[f],c,g,d),g+=p[f].many.length}}function y(t,e,i,r,a){var s=t.many,l=t.one;if(1!==s.length||a)for(var c=k({dividePath:nW[n],individualDelay:u&&function(t,e,n,o){return u(t+i,r)}},h),p=e?function(t,e,n){var i=[];!function t(e){for(var n=0;n<e.length;n++){var r=e[n];YG(r)?t(r.childrenRef()):r instanceof Is&&i.push(r)}}(t);var r=i.length;if(!r)return{fromIndividuals:[],toIndividuals:[],count:0};var o=(n.dividePath||QG)({path:e,count:r});if(o.length!==r)return console.error(\"Invalid morphing: unmatched splitted path\"),{fromIndividuals:[],toIndividuals:[],count:0};i=JG(i),o=JG(o);for(var a=n.done,s=n.during,l=n.individualDelay,u=new gr,h=0;h<r;h++){var c=i[h],p=o[h];p.parent=e,p.copyTransform(u),l||qG(c,p)}function d(t){for(var e=0;e<o.length;e++)o[e].addSelfToZr(t)}function f(){e.__isCombineMorphing=!1,e.__morphT=-1,e.childrenRef=null,ZG(e,\"addSelfToZr\"),ZG(e,\"removeSelfFromZr\")}e.__isCombineMorphing=!0,e.childrenRef=function(){return o},UG(e,\"addSelfToZr\",{after:function(t){d(t)}}),UG(e,\"removeSelfFromZr\",{after:function(t){for(var e=0;e<o.length;e++)o[e].removeSelfFromZr(t)}});var g=o.length;if(l){var y=g,v=function(){0==--y&&(f(),a&&a())};for(h=0;h<g;h++){var m=l?k({delay:(n.delay||0)+l(h,g,i[h],o[h]),done:v},n):n;KG(i[h],o[h],m)}}else e.__morphT=0,e.animateTo({__morphT:1},k({during:function(t){for(var n=0;n<g;n++){var i=o[n];i.__morphT=e.__morphT,i.dirtyShape()}s&&s(t)},done:function(){f();for(var e=0;e<t.length;e++)ZG(t[e],\"updateTransform\");a&&a()}},n));return e.__zr&&d(e.__zr),{fromIndividuals:i,toIndividuals:o,count:g}}(s,l,c):function(t,e,n){var i=e.length,r=[],o=n.dividePath||QG;if(YG(t)){!function t(e){for(var n=0;n<e.length;n++){var i=e[n];YG(i)?t(i.childrenRef()):i instanceof Is&&r.push(i)}}(t.childrenRef());var a=r.length;if(a<i)for(var s=0,l=a;l<i;l++)r.push(mu(r[s++%a]));r.length=i}else{r=o({path:t,count:i});var u=t.getComputedTransform();for(l=0;l<r.length;l++)r[l].setLocalTransform(u);if(r.length!==i)return console.error(\"Invalid morphing: unmatched splitted path\"),{fromIndividuals:[],toIndividuals:[],count:0}}r=JG(r),e=JG(e);var h=n.individualDelay;for(l=0;l<i;l++){var c=h?k({delay:(n.delay||0)+h(l,i,r[l],e[l])},n):n;KG(r[l],e[l],c)}return{fromIndividuals:r,toIndividuals:e,count:e.length}}(l,s,c),d=p.fromIndividuals,f=p.toIndividuals,g=d.length,v=0;v<g;v++){m=u?k({delay:u(v,g)},h):h;o(d[v],f[v],e?s[v]:t.one,e?t.one:s[v],m)}else{var m,x=e?s[0]:l,_=e?l:s[0];if(YG(x))y({many:[x],one:_},!0,i,r,!0);else KG(x,_,m=u?k({delay:u(i,r)},h):h),o(x,_,x,_,m)}}}function rW(t){if(!t)return[];if(Y(t)){for(var e=[],n=0;n<t.length;n++)e.push(rW(t[n]));return e}var i=[];return t.traverse((function(t){t instanceof Is&&!t.disableMorphing&&!t.invisible&&!t.ignore&&i.push(t)})),i}var oW=Oo();function aW(t){var e=[];return E(t,(function(t){var n=t.data;if(!(n.count()>1e4))for(var i=n.getIndices(),r=function(t){for(var e=t.dimensions,n=0;n<e.length;n++){var i=t.getDimensionInfo(e[n]);if(i&&0===i.otherDims.itemGroupId)return e[n]}}(n),o=0;o<i.length;o++)e.push({dataGroupId:t.dataGroupId,data:n,dim:t.dim||r,divide:t.divide,dataIndex:o})})),e}function sW(t,e,n){t.traverse((function(t){t instanceof Is&&gh(t,{style:{opacity:0}},e,{dataIndex:n,isFrom:!0})}))}function lW(t){if(t.parent){var e=t.getComputedTransform();t.setLocalTransform(e),t.parent.remove(t)}}function uW(t){t.stopAnimation(),t.isGroup&&t.traverse((function(t){t.stopAnimation()}))}function hW(t,e,n){var i=ph(\"update\",n,e);i&&t.traverse((function(t){if(t instanceof Sa){var e=function(t){return ch(t).oldStyle}(t);e&&t.animateFrom({style:e},i)}}))}function cW(t,e,n){var i=aW(t),r=aW(e);function o(t,e,n,i,r){(n||t)&&e.animateFrom({style:n&&n!==t?A(A({},n.style),t.style):t.style},r)}function a(t){for(var e=0;e<t.length;e++)if(t[e].dim)return t[e].dim}var s=a(i),l=a(r),u=!1;function h(t,e){return function(n){var i=n.data,r=n.dataIndex;if(e)return i.getId(r);var o=n.dataGroupId,a=t?s||l:l||s,u=a&&i.getDimensionInfo(a),h=u&&u.ordinalMeta;if(u){var c=i.get(u.name,r);return h&&h.categories[c]||c+\"\"}var p=i.getRawDataItem(r);return p&&p.groupId?p.groupId+\"\":o||i.getId(r)}}var c=function(t,e){var n=t.length;if(n!==e.length)return!1;for(var i=0;i<n;i++){var r=t[i],o=e[i];if(r.data.getId(r.dataIndex)!==o.data.getId(o.dataIndex))return!1}return!0}(i,r),p={};if(!c)for(var d=0;d<r.length;d++){var f=r[d],g=f.data.getItemGraphicEl(f.dataIndex);g&&(p[g.id]=!0)}function y(t,e){var n=i[e],a=r[t],s=a.data.hostModel,l=n.data.getItemGraphicEl(n.dataIndex),h=a.data.getItemGraphicEl(a.dataIndex);l!==h?l&&p[l.id]||h&&(uW(h),l?(uW(l),lW(l),u=!0,iW(rW(l),rW(h),a.divide,s,t,o)):sW(h,s,t)):h&&hW(h,a.dataIndex,s)}new Vm(i,r,h(!0,c),h(!1,c),null,\"multiple\").update(y).updateManyToOne((function(t,e){var n=r[t],a=n.data,s=a.hostModel,l=a.getItemGraphicEl(n.dataIndex),h=B(z(e,(function(t){return i[t].data.getItemGraphicEl(i[t].dataIndex)})),(function(t){return t&&t!==l&&!p[t.id]}));l&&(uW(l),h.length?(E(h,(function(t){uW(t),lW(t)})),u=!0,iW(rW(h),rW(l),n.divide,s,t,o)):sW(l,s,n.dataIndex))})).updateOneToMany((function(t,e){var n=i[e],a=n.data.getItemGraphicEl(n.dataIndex);if(!a||!p[a.id]){var s=B(z(t,(function(t){return r[t].data.getItemGraphicEl(r[t].dataIndex)})),(function(t){return t&&t!==a})),l=r[t[0]].data.hostModel;s.length&&(E(s,(function(t){return uW(t)})),a?(uW(a),lW(a),u=!0,iW(rW(a),rW(s),n.divide,l,t[0],o)):E(s,(function(e){return sW(e,l,t[0])})))}})).updateManyToMany((function(t,e){new Vm(e,t,(function(t){return i[t].data.getId(i[t].dataIndex)}),(function(t){return r[t].data.getId(r[t].dataIndex)})).update((function(n,i){y(t[n],e[i])})).execute()})).execute(),u&&E(e,(function(t){var e=t.data.hostModel,i=e&&n.getViewOfSeriesModel(e),r=ph(\"update\",e,0);i&&e.isAnimationEnabled()&&r&&r.duration>0&&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<t.length;n++){if(null!=e.seriesIndex&&e.seriesIndex===t[n].seriesIndex||null!=e.seriesId&&e.seriesId===t[n].id)return n}}Nm([function(t){t.registerPainter(\"canvas\",eS)}]),Nm([function(t){t.registerPainter(\"svg\",jw)}]),Nm([function(t){t.registerChartView(NS),t.registerSeriesModel(nS),t.registerLayout(ES(\"line\",!0)),t.registerVisual({seriesType:\"line\",reset:function(t){var e=t.getData(),n=t.getModel(\"lineStyle\").getLineStyle();n&&!n.stroke&&(n.stroke=e.getVisual(\"style\").fill),e.setVisual(\"legendLineStyle\",n)}}),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,BS(\"line\"))},function(t){t.registerChartView(qS),t.registerSeriesModel(GS),t.registerLayout(t.PRIORITY.VISUAL.LAYOUT,H(Hx,\"bar\")),t.registerLayout(t.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,Yx(\"bar\")),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,BS(\"bar\")),t.registerAction({type:\"changeAxisOrder\",event:\"changeAxisOrder\",update:\"update\"},(function(t,e){var n=t.componentType||\"series\";e.eachComponent({mainType:n,query:t},(function(e){t.sortInfo&&e.axis.setCategorySortInfo(t.sortInfo)}))}))},function(t){t.registerChartView(SM),t.registerSeriesModel(CM),Dy(\"pie\",t.registerAction),t.registerLayout(H(gM,\"pie\")),t.registerProcessor(yM(\"pie\")),t.registerProcessor(function(t){return{seriesType:t,reset:function(t,e){var n=t.getData();n.filterSelf((function(t){var e=n.mapDimension(\"value\"),i=n.get(e,t);return!(j(i)&&!isNaN(i)&&i<0)}))}}}(\"pie\"))},function(t){Nm(DI),t.registerSeriesModel(DM),t.registerChartView(PM),t.registerLayout(ES(\"scatter\"))},function(t){Nm(WI),t.registerChartView(OI),t.registerSeriesModel(RI),t.registerLayout(AI),t.registerProcessor(yM(\"radar\")),t.registerPreprocessor(PI)},function(t){Nm(vC),t.registerChartView(JT),t.registerSeriesModel(QT),t.registerLayout(eC),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,tC),Dy(\"map\",t.registerAction)},function(t){t.registerChartView(AC),t.registerSeriesModel($C),t.registerLayout(QC),t.registerVisual(tD),function(t){t.registerAction({type:\"treeExpandAndCollapse\",event:\"treeExpandAndCollapse\",update:\"update\"},(function(t,e){e.eachComponent({mainType:\"series\",subType:\"tree\",query:t},(function(e){var n=t.dataIndex,i=e.getData().tree.getNodeByDataIndex(n);i.isExpand=!i.isExpand}))})),t.registerAction({type:\"treeRoam\",event:\"treeRoam\",update:\"none\"},(function(t,e,n){e.eachComponent({mainType:\"series\",subType:\"tree\",query:t},(function(e){var i=fC(e.coordinateSystem,t,void 0,n);e.setCenter&&e.setCenter(i.center),e.setZoom&&e.setZoom(i.zoom)}))}))}(t)},function(t){t.registerSeriesModel(iD),t.registerChartView(yD),t.registerVisual(OD),t.registerLayout(UD),function(t){for(var e=0;e<eD.length;e++)t.registerAction({type:eD[e],update:\"updateView\"},bt);t.registerAction({type:\"treemapRootToNode\",update:\"updateView\"},(function(t,e){e.eachComponent({mainType:\"series\",subType:\"treemap\",query:t},(function(e,n){var i=ZC(t,[\"treemapZoomToNode\",\"treemapRootToNode\"],e);if(i){var r=e.getViewRoot();r&&(t.direction=qC(r,i.node)?\"rollUp\":\"drillDown\"),e.resetViewRoot(i.node)}}))}))}(t)},function(t){t.registerChartView(ZA),t.registerSeriesModel(tk),t.registerProcessor(JD),t.registerVisual(QD),t.registerVisual(eA),t.registerLayout(cA),t.registerLayout(t.PRIORITY.VISUAL.POST_CHART_LAYOUT,xA),t.registerLayout(bA),t.registerCoordinateSystem(\"graphView\",{dimensions:iC.dimensions,create:wA}),t.registerAction({type:\"focusNodeAdjacency\",event:\"focusNodeAdjacency\",update:\"series:focusNodeAdjacency\"},bt),t.registerAction({type:\"unfocusNodeAdjacency\",event:\"unfocusNodeAdjacency\",update:\"series:unfocusNodeAdjacency\"},bt),t.registerAction(ek,(function(t,e,n){e.eachComponent({mainType:\"series\",query:t},(function(e){var i=fC(e.coordinateSystem,t,void 0,n);e.setCenter&&e.setCenter(i.center),e.setZoom&&e.setZoom(i.zoom)}))}))},function(t){t.registerChartView(ok),t.registerSeriesModel(ak)},function(t){t.registerChartView(uk),t.registerSeriesModel(hk),t.registerLayout(ck),t.registerProcessor(yM(\"funnel\"))},function(t){Nm(zL),t.registerChartView(pk),t.registerSeriesModel(vk),t.registerVisual(t.PRIORITY.VISUAL.BRUSH,_k)},function(t){t.registerChartView(FL),t.registerSeriesModel(WL),t.registerLayout(HL),t.registerVisual(eP),t.registerAction({type:\"dragNode\",event:\"dragnode\",update:\"update\"},(function(t,e){e.eachComponent({mainType:\"series\",subType:\"sankey\",query:t},(function(e){e.setNodePosition(t.dataIndex,[t.localX,t.localY])}))}))},function(t){t.registerSeriesModel(iP),t.registerChartView(rP),t.registerLayout(cP),t.registerTransform(pP)},function(t){t.registerChartView(fP),t.registerSeriesModel(IP),t.registerPreprocessor(TP),t.registerVisual(PP),t.registerLayout(OP)},function(t){t.registerChartView(zP),t.registerSeriesModel(VP),t.registerLayout(ES(\"effectScatter\"))},function(t){t.registerChartView(UP),t.registerSeriesModel(KP),t.registerLayout(XP),t.registerVisual(JP)},function(t){t.registerChartView(eO),t.registerSeriesModel(nO)},function(t){t.registerChartView(aO),t.registerSeriesModel(MO),t.registerLayout(t.PRIORITY.VISUAL.LAYOUT,H(Hx,\"pictorialBar\")),t.registerLayout(t.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,Yx(\"pictorialBar\"))},function(t){t.registerChartView(IO),t.registerSeriesModel(TO),t.registerLayout(CO),t.registerProcessor(yM(\"themeRiver\"))},function(t){t.registerChartView(PO),t.registerSeriesModel(OO),t.registerLayout(H(EO,\"sunburst\")),t.registerProcessor(H(yM,\"sunburst\")),t.registerVisual(VO),function(t){t.registerAction({type:kO,update:\"updateView\"},(function(t,e){e.eachComponent({mainType:\"series\",subType:\"sunburst\",query:t},(function(e,n){var i=ZC(t,[kO],e);if(i){var r=e.getViewRoot();r&&(t.direction=qC(r,i.node)?\"rollUp\":\"drillDown\"),e.resetViewRoot(i.node)}}))})),t.registerAction({type:LO,update:\"none\"},(function(t,e,n){t=A({},t),e.eachComponent({mainType:\"series\",subType:\"sunburst\",query:t},(function(e){var n=ZC(t,[LO],e);n&&(t.dataIndex=n.node.dataIndex)})),n.dispatchAction(A(t,{type:\"highlight\"}))})),t.registerAction({type:\"sunburstUnhighlight\",update:\"updateView\"},(function(t,e,n){t=A({},t),n.dispatchAction(A(t,{type:\"downplay\"}))}))}(t)},function(t){t.registerChartView(AR),t.registerSeriesModel(WO)}]),Nm((function(t){Nm(DI),Nm(kN)})),Nm((function(t){Nm(kN),yI.registerAxisPointerClass(\"PolarAxisPointer\",LN),t.registerCoordinateSystem(\"polar\",XN),t.registerComponentModel(ON),t.registerComponentView(sE),FM(t,\"angle\",NN,oE),FM(t,\"radius\",EN,aE),t.registerComponentView(KN),t.registerComponentView(tE),t.registerLayout(H(rE,\"bar\"))})),Nm(vC),Nm((function(t){Nm(kN),yI.registerAxisPointerClass(\"SingleAxisPointer\",bE),t.registerComponentView(IE),t.registerComponentView(cE),t.registerComponentModel(dE),FM(t,\"single\",dE,dE.defaultOption),t.registerCoordinateSystem(\"single\",mE)})),Nm(zL),Nm((function(t){t.registerComponentModel(TE),t.registerComponentView(DE),t.registerCoordinateSystem(\"calendar\",kE)})),Nm((function(t){t.registerComponentModel(EE),t.registerComponentView(BE),t.registerPreprocessor((function(t){var e=t.graphic;Y(e)?e[0]&&e[0].elements?t.graphic=[t.graphic[0]]:t.graphic=[{elements:e}]:e&&!e.elements&&(t.graphic=[{elements:[e]}])}))})),Nm((function(t){t.registerComponentModel(pz),t.registerComponentView(fz),hz(\"saveAsImage\",gz),hz(\"magicType\",mz),hz(\"dataView\",Iz),hz(\"dataZoom\",Zz),hz(\"restore\",kz),Nm(sz)})),Nm((function(t){Nm(kN),t.registerComponentModel(Kz),t.registerComponentView(dV),t.registerAction({type:\"showTip\",event:\"showTip\",update:\"tooltip:manuallyShowTip\"},bt),t.registerAction({type:\"hideTip\",event:\"hideTip\",update:\"tooltip:manuallyHideTip\"},bt)})),Nm(kN),Nm((function(t){t.registerComponentView(NV),t.registerComponentModel(EV),t.registerPreprocessor(mV),t.registerVisual(t.PRIORITY.VISUAL.BRUSH,kV),t.registerAction({type:\"brush\",event:\"brush\",update:\"updateVisual\"},(function(t,e){e.eachComponent({mainType:\"brush\",query:t},(function(e){e.setAreas(t.areas)}))})),t.registerAction({type:\"brushSelect\",event:\"brushSelected\",update:\"none\"},bt),t.registerAction({type:\"brushEnd\",event:\"brushEnd\",update:\"none\"},bt),hz(\"brush\",BV)})),Nm((function(t){t.registerComponentModel(FV),t.registerComponentView(GV)})),Nm((function(t){t.registerComponentModel(HV),t.registerComponentView(jV),t.registerSubTypeDefaulter(\"timeline\",(function(){return\"slider\"})),function(t){t.registerAction({type:\"timelineChange\",event:\"timelineChanged\",update:\"prepareAndUpdate\"},(function(t,e,n){var i=e.getComponent(\"timeline\");return i&&null!=t.currentIndex&&(i.setCurrentIndex(t.currentIndex),!i.get(\"loop\",!0)&&i.isIndexMax()&&i.getPlayState()&&(i.setPlayState(!1),n.dispatchAction({type:\"timelinePlayChange\",playState:!1,from:t.from}))),e.resetOption(\"timeline\",{replaceMerge:i.get(\"replaceMerge\",!0)}),k({currentIndex:i.option.currentIndex},t)})),t.registerAction({type:\"timelinePlayChange\",event:\"timelinePlayChanged\",update:\"update\"},(function(t,e){var n=e.getComponent(\"timeline\");n&&null!=t.playState&&n.setPlayState(t.playState)}))}(t),t.registerPreprocessor($V)})),Nm((function(t){t.registerComponentModel(rB),t.registerComponentView(yB),t.registerPreprocessor((function(t){tB(t.series,\"markPoint\")&&(t.markPoint=t.markPoint||{})}))})),Nm((function(t){t.registerComponentModel(vB),t.registerComponentView(MB),t.registerPreprocessor((function(t){tB(t.series,\"markLine\")&&(t.markLine=t.markLine||{})}))})),Nm((function(t){t.registerComponentModel(IB),t.registerComponentView(OB),t.registerPreprocessor((function(t){tB(t.series,\"markArea\")&&(t.markArea=t.markArea||{})}))})),Nm((function(t){Nm(XB),Nm(JB)})),Nm((function(t){Nm(hF),Nm(xF)})),Nm(hF),Nm(xF),Nm((function(t){Nm(QF),Nm(rG)})),Nm(QF),Nm(rG),Nm((function(t){t.registerPreprocessor(uG),t.registerVisual(t.PRIORITY.VISUAL.ARIA,lG)})),Nm((function(t){t.registerTransform(bG),t.registerTransform(wG)})),Nm((function(t){t.registerComponentModel(SG),t.registerComponentView(MG)})),Nm((function(t){t.registerUpdateLifecycle(\"series:beforeupdate\",(function(t,e,n){E(bo(n.seriesTransition),(function(t){E(bo(t.to),(function(t){for(var e=n.updatedSeries,i=0;i<e.length;i++)(null!=t.seriesIndex&&t.seriesIndex===e[i].seriesIndex||null!=t.seriesId&&t.seriesId===e[i].id)&&(e[i][vg]=!0)}))}))})),t.registerUpdateLifecycle(\"series:transition\",(function(t,e,n){var i=oW(e);if(i.oldSeries&&n.updatedSeries&&n.optionChanged){var r=n.seriesTransition;if(r)E(bo(r),(function(t){!function(t,e,n,i){var r=[],o=[];E(bo(t.from),(function(t){var n=gW(e.oldSeries,t);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<a.length;h++){var c=a[h].getData();c.count()<1e4&&(s.push(a[h]),l.push(a[h].get(\"dataGroupId\")),u.push(c))}}))})),Nm((function(t){t.registerUpdateLifecycle(\"series:beforeupdate\",(function(t,e,n){var i=Gb(e).labelManager;i||(i=Gb(e).labelManager=new Fb),i.clearLabels()})),t.registerUpdateLifecycle(\"series:layoutlabels\",(function(t,e,n){var i=Gb(e).labelManager;n.updatedSeries.forEach((function(t){i.addLabelsOfSeries(e.getViewOfSeriesModel(t))})),i.updateLayoutConfig(e),i.layout(e),i.processLabelsOverall()}))})),t.Axis=nb,t.ChartView=kg,t.ComponentModel=Rp,t.ComponentView=Tg,t.List=lx,t.Model=Mc,t.PRIORITY=Mv,t.SeriesModel=mg,t.color=ai,t.connect=function(t){if(Y(t)){var e=t;t=null,E(e,(function(e){null!=e.group&&(t=e.group)})),t=t||\"g_\"+dm++,E(e,(function(e){e.group=t}))}return cm[t]=!0,t},t.dataTool={},t.dependencies={zrender:\"5.4.4\"},t.disConnect=ym,t.disconnect=gm,t.dispose=function(t){U(t)?t=hm[t]:t instanceof Qv||(t=vm(t)),t instanceof Qv&&!t.isDisposed()&&t.dispose()},t.env=r,t.extendChartView=function(t){var e=kg.extend(t);return kg.registerClass(e),e},t.extendComponentModel=function(t){var e=Rp.extend(t);return Rp.registerClass(e),e},t.extendComponentView=function(t){var e=Tg.extend(t);return Tg.registerClass(e),e},t.extendSeriesModel=function(t){var e=mg.extend(t);return mg.registerClass(e),e},t.format=Y_,t.getCoordinateSystemDimensions=function(t){var e=xd.get(t);if(e)return e.getDimensionsInfo?e.getDimensionsInfo():e.dimensions.slice()},t.getInstanceByDom=vm,t.getInstanceById=function(t){return hm[t]},t.getMap=function(t){var e=bv(\"getMap\");return e&&e(t)},t.graphic=H_,t.helper=C_,t.init=function(t,e,n){var i=!(n&&n.ssr);if(i){0;var r=vm(t);if(r)return r;0}var o=new Qv(t,e,n);return o.id=\"ec_\"+pm++,hm[o.id]=o,i&&Fo(t,fm,o.id),jv(o),xv.trigger(\"afterinit\",o),o},t.innerDrawElementOnCanvas=hv,t.matrix=Ce,t.number=G_,t.parseGeoJSON=F_,t.parseGeoJson=F_,t.registerAction=Mm,t.registerCoordinateSystem=Im,t.registerLayout=Tm,t.registerLoading=km,t.registerLocale=Rc,t.registerMap=Lm,t.registerPostInit=bm,t.registerPostUpdate=wm,t.registerPreprocessor=xm,t.registerProcessor=_m,t.registerTheme=mm,t.registerTransform=Pm,t.registerUpdateLifecycle=Sm,t.registerVisual=Cm,t.setCanvasCreator=function(t){c({createCanvas:t})},t.setPlatformAPI=c,t.throttle=Bg,t.time=W_,t.use=Nm,t.util=X_,t.vector=Xt,t.version=\"5.4.3\",t.zrUtil=St,t.zrender=Hr,Object.defineProperty(t,\"__esModule\",{value:!0})}));"
  },
  {
    "path": "sa-token-doc/static/page-com/github-stars-vs/github-stars-vs.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\"\n    \tcontent=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n    <title>GitHub仓库Star数量对比</title>\n    <script src=\"./echarts.min-5.4.3.js\"></script>\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;\n        }\n        \n        body {\n            background: #ffffff;\n            color: #333333;\n            width: 1000px;\n            height: 700px;\n            display: flex;\n            flex-direction: column;\n            overflow: hidden;\n        }\n        \n        .container {\n            width: 100%;\n            height: 100%;\n            padding: 15px;\n            display: flex;\n            flex-direction: column;\n        }\n        \n        .comparison-text {\n            text-align: center;\n            margin-bottom: 15px;\n            padding: 12px;\n            background: #f8f9fa;\n            font-size: 16px;\n            line-height: 1.5;\n            height: 80px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            border-radius: 0;\n        }\n        \n        .highlight {\n            color: #8A2BE2; /* 紫色 */\n            font-weight: 700; /* 更粗 */\n            font-size: 18px; /* 增大字号 */\n\t\t\tmargin-left: 6px;\n\t\t\tmargin-right: 6px;\n            /* padding: 0 5px; /* 左右添加空格 */ */\n        }\n        \n        .chart-container {\n            background: #ffffff;\n            padding: 10px;\n            border: 1px solid #e0e0e0;\n            flex: 1;\n            min-height: 0;\n            position: relative;\n            border-radius: 0;\n        }\n        \n        #starsChart {\n            width: 100% !important;\n            height: 100% !important;\n        }\n        \n        .loading {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            background: rgba(255, 255, 255, 0.9);\n            z-index: 10;\n        }\n        \n        .spinner {\n            width: 40px;\n            height: 40px;\n            border: 3px solid rgba(74, 144, 226, 0.2);\n            border-radius: 50%;\n            border-top-color: #4a90e2;\n            animation: spin 1s linear infinite;\n            margin-bottom: 15px;\n        }\n        \n        @keyframes spin {\n            to { transform: rotate(360deg); }\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <div class=\"comparison-text\" id=\"comparisonText\">\n            正在加载数据...\n        </div>\n        \n        <div class=\"chart-container\">\n            <div id=\"starsChart\"></div>\n            <div class=\"loading\" id=\"loadingOverlay\">\n                <div class=\"spinner\"></div>\n                <p>正在加载数据...</p>\n            </div>\n        </div>\n    </div>\n    \n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\">\n    \n    <script>\n        // 配置要获取的仓库列表\n        const repos = [\n            { owner: 'dromara', name: 'Sa-Token', desc: 'Sa-Token', color: '#349A34' },\n            { owner: 'spring-projects', name: 'spring-security', desc: 'Spring Security', color: '#5DB1FF' },\n            { owner: 'apache', name: 'shiro', desc: 'Apache Shiro', color: '#ED7C25' }\n        ];\n        \n        // ECharts实例和数据\n        let starsChart = null;\n        let repoData = [];\n        \n        // 初始化ECharts\n        function initChart() {\n            const chartDom = document.getElementById('starsChart');\n            if (!chartDom) {\n                console.error(\"找不到图表容器\");\n                return null;\n            }\n            \n            starsChart = echarts.init(chartDom);\n            return starsChart;\n        }\n        \n        // 页面加载完成后执行\n        window.onload = async function() {\n            console.log(\"页面加载完成，开始初始化...\");\n            \n            // 初始化图表\n            starsChart = initChart();\n            if (!starsChart) {\n                console.error(\"ECharts初始化失败\");\n                return;\n            }\n            \n            // 显示加载状态\n            showLoading(true);\n            \n            // 获取数据\n            await fetchAllStars();\n            \n            // 每30分钟自动刷新数据\n            setInterval(fetchAllStars, 30 * 60 * 1000);\n        };\n        \n        // 获取所有仓库的star数量\n        async function fetchAllStars() {\n            try {\n                const promises = repos.map(repo => fetchRepoStars(repo));\n                const results = await Promise.allSettled(promises);\n                \n                // 处理结果\n                repoData = results.map((result, index) => {\n                    if (result.status === 'fulfilled') {\n                        return {\n                            ...repos[index],\n                            stars: result.value,\n                            error: null\n                        };\n                    } else {\n                        return {\n                            ...repos[index],\n                            stars: 0,\n                            error: result.reason.message\n                        };\n                    }\n                });\n                \n                console.log(\"获取到的数据:\", repoData);\n                \n                // 更新UI\n                updateComparisonText();\n                updateChart();\n                showLoading(false);\n                \n            } catch (error) {\n                console.error('获取数据时发生错误:', error);\n                document.querySelector('.comparison-text').innerHTML = \"数据加载失败，请稍后重试\";\n                showLoading(false);\n            }\n        }\n        \n        // 获取单个仓库的star数量\n        async function fetchRepoStars(repo) {\n            try {\n                // 使用GitHub API\n                const url = `https://api.github.com/repos/${repo.owner}/${repo.name}`;\n                \n                console.log(`正在获取 ${repo.owner}/${repo.name} 的数据...`);\n                \n                const response = await fetch(url, {\n                    headers: {\n                        'Accept': 'application/vnd.github.v3+json',\n                        'User-Agent': 'GitHub-Stars-Chart'\n                    }\n                });\n                \n                if (!response.ok) {\n                    throw new Error(`HTTP错误: ${response.status}`);\n                }\n                \n                const data = await response.json();\n                console.log(`${repo.owner}/${repo.name}: ${data.stargazers_count} stars`);\n                return data.stargazers_count;\n            } catch (error) {\n                console.error(`获取 ${repo.owner}/${repo.name} 数据失败:`, error);\n                \n                // 返回模拟数据用于测试\n                if (repo.name === 'Sa-Token') return 18452;\n                if (repo.name === 'spring-security') return 9398;\n                if (repo.name === 'shiro') return 4419;\n                \n                throw error;\n            }\n        }\n        \n        // 更新比较文本\n        function updateComparisonText() {\n            const saTokenData = repoData.find(r => r.name === 'Sa-Token');\n            const springSecurityData = repoData.find(r => r.name === 'spring-security');\n            const shiroData = repoData.find(r => r.name === 'shiro');\n            \n            if (!saTokenData || !springSecurityData || !shiroData) {\n                document.querySelector('.comparison-text').innerHTML = \"数据加载不完整\";\n                return;\n            }\n            \n            const saTokenStars = saTokenData.stars || 0;\n            const springSecurityStars = springSecurityData.stars || 1;\n            const shiroStars = shiroData.stars || 1;\n            \n            const springSecurityMultiple = (saTokenStars / springSecurityStars).toFixed(2);\n            const shiroMultiple = (saTokenStars / shiroStars).toFixed(2);\n            \n            document.querySelector('.comparison-text').innerHTML = \n                `Sa-Token GitHub 关注量达到 <span class=\"highlight\"> ${saTokenStars.toLocaleString()} </span> Star，是主要竞争框架 Spring Security 的 <span class=\"highlight\"> ${springSecurityMultiple} </span> 倍，Apache Shiro 的 <span class=\"highlight\"> ${shiroMultiple} </span> 倍`;\n        }\n        \n        // 更新柱状图\n        function updateChart() {\n            if (!starsChart) {\n                console.error(\"图表实例未初始化\");\n                return;\n            }\n            \n            // 准备图表数据\n            const labels = repoData.map(repo => repo.desc);\n            const stars = repoData.map(repo => repo.stars);\n            const colors = repoData.map(repo => repo.color);\n            \n            console.log(\"图表数据准备完成:\", { labels, stars, colors });\n            \n            // 配置ECharts选项\n            const option = {\n\t\t\t\ttoolbox: {\n\t\t\t\t\tshow: true,\n\t\t\t\t\ttop: 15,\n\t\t\t\t\tfeature: {\n\t\t\t\t\t\tsaveAsImage: {\n\t\t\t\t\t\t\tshow: true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n                tooltip: {\n                    trigger: 'axis',\n                    axisPointer: {\n                        type: 'shadow'\n                    },\n                    formatter: function(params) {\n                        const data = params[0];\n                        return `<div style=\"color:#fff;font-size:14px;\">\n                            ${data.name}<br/>\n                            Star数量: <span style=\"color:#ffd700;font-weight:bold\">${data.value.toLocaleString()} star</span>\n                        </div>`;\n                    },\n                    backgroundColor: 'rgba(0, 0, 0, 0.85)',\n                    borderColor: '#333',\n                    textStyle: {\n                        color: '#fff',\n                        fontSize: 14\n                    }\n                },\n                grid: {\n                    left: '5%',\n                    right: '5%',\n                    bottom: '8%',\n                    top: '8%',\n                    containLabel: true\n                },\n                xAxis: {\n                    type: 'category',\n                    data: labels,\n                    axisLine: {\n                        lineStyle: {\n                            color: '#333'\n                        }\n                    },\n                    axisLabel: {\n                        color: '#333',\n                        fontSize: 14,\n                        fontWeight: 'bold'\n                    }\n                },\n                yAxis: {\n                    type: 'value',\n                    name: 'Star数量',\n                    nameTextStyle: {\n                        color: '#333',\n                        fontSize: 14,\n                        fontWeight: 'bold',\n                        padding: [0, 0, 0, 10]\n                    },\n                    axisLine: {\n                        lineStyle: {\n                            color: '#f0f0f0'\n                        }\n                    },\n                    axisLabel: {\n                        color: '#666',\n                        fontSize: 12,\n                        formatter: function(value) {\n                            if (value >= 1000) {\n                                return (value / 1000).toFixed(1) + 'k';\n                            }\n                            return value;\n                        },\n                        margin: 10\n                    },\n                    splitLine: {\n                        lineStyle: {\n                            color: '#f0f0f0'\n                        }\n                    }\n                },\n                series: [\n                    {\n                        name: 'Star数量',\n                        type: 'bar',\n                        data: stars.map((value, index) => ({\n                            value: value,\n                            itemStyle: {\n                                color: colors[index]\n                            }\n                        })),\n                        barWidth: '70%',\n                        label: {\n                            show: true,\n                            position: 'top',\n                            formatter: '{c} star',\n                            color: '#333',\n                            fontSize: 14,\n                            fontWeight: 'bold'\n                        },\n                        itemStyle: {\n                            borderRadius: [4, 4, 0, 0]\n                        }\n                    }\n                ]\n            };\n            \n            // 设置图表选项\n            try {\n                starsChart.setOption(option);\n                console.log(\"图表更新成功\");\n                \n                // 响应窗口大小变化\n                window.addEventListener('resize', function() {\n                    starsChart.resize();\n                });\n                \n            } catch (error) {\n                console.error(\"更新图表时发生错误:\", error);\n                \n                // 显示错误信息\n                const chartContainer = document.querySelector('.chart-container');\n                chartContainer.innerHTML = `\n                    <div style=\"text-align: center; padding: 20px; color: #e74c3c;\">\n                        <p>图表更新失败: ${error.message}</p>\n                        <p>数据已加载: ${JSON.stringify(repoData.map(r => ({name: r.name, stars: r.stars})))}</p>\n                    </div>\n                `;\n            }\n        }\n        \n        // 显示/隐藏加载状态\n        function showLoading(show) {\n            const loadingOverlay = document.getElementById('loadingOverlay');\n            if (loadingOverlay) {\n                loadingOverlay.style.display = show ? 'flex' : 'none';\n            }\n        }\n    </script>\n</body>\n</html>"
  },
  {
    "path": "sa-token-doc/static/swiper/index-swiper.css",
    "content": "@charset \"utf-8\";\n/* CSS Document */\n\n/* ry盒子 总区域 */\n.ry-kuai{\n\tpadding-left: 0;\n\tpadding-right: 0;\n}\n/* ry盒子 灰色区域 */\n.ry-box{\n\tpadding-top: 70px;\n\tpadding-bottom: 170px;\n\tbackground-color: #eee;\n\tposition: relative;\n\toverflow: hidden;\n}\n\n/* 轮播图容器 */\n.ry-box .swiper {\n\twidth: 100%;\n\theight: 100%;\n}\n.ry-box .swiper-slide {\n\ttext-align: center;\n\tfont-size: 18px;\n\twidth: 750px;\n\theight: 500px;\n\t/* cursor: pointer; */\n}\n.ry-box .swiper-slide-tx1{\n\twidth: 450px;\n}\n\n.ry-box .swiper-slide img {\n\theight: 100%;\n\tbox-shadow: 0 0 20px #ccc;\n\ttransition: box-shadow 0.2s;\n}\n.ry-box .swiper-slide img:hover{\n\tbox-shadow: 0 0 40px #999;\n}\n.ry-box .swiper-slide p{\n\tdisplay: inline-block;\n\tfont-size: 16px;\n\tmargin-top: 30px;\n\tcolor: #222;\n}\n\n\n/* 分页器样式 */\n.ry-box .swiper-pagination{bottom: -140px;}\n.ry-box .swiper-pagination .swiper-pagination-bullet{width: 18px; height: 18px; line-height: 18px; color: #FFF; font-size: 12px;}\n\n/* 图片放大动画 */\n.ry-box .swiper-slide img{\n\ttransition: 300ms;\n\ttransform: scale(0.8);\n}\n.ry-box .swiper-slide-active img,\n.ry-box .swiper-slide-duplicate-active img{\n  transform: scale(1);\n}\n\n/* 阴影 */\n/* .ry-img-yinying{\n\twidth: 50%; height: 10px; border-radius: 50%; \n\tbackground-color: rgba(0, 0, 0, 0.8);\n\tbox-shadow: 0 0 50px #333;\n\tmargin: auto;\n} */"
  },
  {
    "path": "sa-token-doc/static/swiper/index-swiper.js",
    "content": "function initSwiper () {\n\tif(window.swiper){\n\t\treturn;\n\t}\n\twindow.swiper = new Swiper(\".mySwiper\", {\n\t\t// 最大容纳的slide数量，auto=自动\n\t\tslidesPerView: \"auto\",\n\t\t// 主角 slide 居中 \n\t\tcenteredSlides: true,\n\t\t// 使左右 slide 贴合容器\n\t\t// centeredSlidesBounds: true,\n\t\t// 循环 \n\t\tloop: true,\n\t\t// 自动播放 \n\t\tautoplay: {\n\t\t\t// 3秒切换一次\n\t\t\tdelay: 3000,\n\t\t},\n\t\t// slide 间距 \n\t\tspaceBetween: 80,\n\t\t// 点击 slide 时，过渡到这个 slide \n\t\tslideToClickedSlide: true,\n\t\t// 切换效果 slide=普通位移、fade=淡入、cube=方块、coverflow=3d流、flip=3d翻转、cards=卡片式、creative=创意性\n\t\teffect: 'coverflow',\n\t\t// 抓取时，鼠标变小手 \n        grabCursor: true,\n\t\t// 分页器 \n\t\tpagination: {\n\t\t\tel: \".swiper-pagination\",\n\t\t\t// 点击时切换 slide\n\t\t\tclickable: true,\n\t\t\t// 分页器样式，bullets=原点，fraction=分式，progressbar=进度条，custom=自定义\n\t\t\ttype: \"bullets\",\n\t\t\t// 点击小点，切换 slide \n\t\t\tclickable :true,\n\t\t\t// 将按钮从小点变成数字 \n\t\t\trenderBullet: function (index, className) {\n\t\t\t\treturn '<span class=\"' + className + '\">' + (index + 1) + '</span>';\n\t\t\t},\n\t\t},\n\t\t// 左右切换按钮 \n\t\tnavigation: {\n\t\t\tnextEl: \".swiper-button-next\",\n\t\t\tprevEl: \".swiper-button-prev\",\n\t\t},\n\t});\n}\n\n$(function(){\n\tinitSwiper();\n})\n\n// 滚动到 swiper 时，再加载 \n// $(document).scroll(function(){\n// \t// 页面滚动条高度 > ry盒子到顶部距离 + window 视口高度 时，swiper出现 \n// \tif($(document).scrollTop() > $('.ry-kuai').offset().top - $(window).height()) {\n// \t\tinitSwiper();\n// \t}\n// })"
  },
  {
    "path": "sa-token-doc/static/vue.css",
    "content": "@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}"
  },
  {
    "path": "sa-token-doc/static/water-change-theme/water-change-theme.css",
    "content": "/* 水滴样式 */\n.water-drop {\n\tposition: fixed;\n\t/* 改为fixed避免触发滚动条 */\n\twidth: 20px;\n\theight: 28px;\n\tborder-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;\n\ttransform: rotate(45deg);\n\tz-index: 1550;\n\tborder: 2px solid rgba(0, 0, 0, 0.2);\n\t/* 添加轮廓 */\n\tbox-shadow: 0 0 5px rgba(0, 0, 0, 0.1);\n\t/* 添加阴影增强视觉效果 */\n}\n\n/* 圆形扩散效果 div */\n.color-wave {\n\tposition: fixed;\n\tborder-radius: 50%;\n\ttransform: scale(0);\n\tz-index: -1;\n\t/* 降低z-index确保不遮挡内容 */\n\tpointer-events: none;\n}\n\n/* 将页面主盒子设置为定位，这样就可以让水滴扩散的div 设置 z-index: 保持不覆盖 main-box 里的内容了 */\n.main-box{\n\tposition: relative;\n\t/* z-index: 1; */\n}"
  },
  {
    "path": "sa-token-doc/static/water-change-theme/water-change-theme.js",
    "content": "// 绑定修改背景色的按钮事件\n$('.theme-box span').click(function() {\n\t// 获取主题色 \n\tlet bgColor = this.style.backgroundColor;\n\n\t// 获取点击位置\n\tconst rect = this.getBoundingClientRect();\n\tconst x = rect.left + rect.width / 2;\n\tconst y = rect.top + rect.height / 2;\n\n\t// 创建水滴元素\n\tcreateWaterDrop(x - 7, y + 5, bgColor);\n\n\t// setBg(bgColor);\n\tlocalStorage.setItem('bg-color-value', bgColor)\n})\n\n// 创建水滴动画\nfunction createWaterDrop(x, y, color) {\n\t// 创建水滴元素\n\tconst waterDrop = document.createElement('div');\n\twaterDrop.className = 'water-drop';\n\twaterDrop.style.backgroundColor = color;\n\twaterDrop.style.left = `${x}px`;\n\twaterDrop.style.top = `${y}px`;\n\n\t// 添加到文档中\n\tdocument.body.appendChild(waterDrop);\n\n\t// 获取视口高度\n\tconst viewportHeight = window.innerHeight;\n\n\t// 使用GSAP创建水滴下落动画\n\tgsap.to(waterDrop, {\n\t\ttop: viewportHeight - 30, // 调整为视口底部内，避免触发滚动条\n\t\tduration: 1.5,\n\t\tease: \"power2.in\", // 加速度下落\n\t\tonComplete: function() {\n\t\t\t// 动画完成后移除水滴\n\t\t\tdocument.body.removeChild(waterDrop);\n\n\t\t\t// 创建颜色扩散效果\n\t\t\tcreateColorWave(x, viewportHeight, color);\n\t\t}\n\t});\n}\n\n\n// 创建颜色扩散效果\nfunction createColorWave(x, y, color) {\n\t// 创建颜色波元素\n\tconst colorWave = document.createElement('div');\n\tcolorWave.className = 'color-wave';\n\tcolorWave.style.backgroundColor = color;\n\n\t// 计算所需的最小半径（确保能覆盖整个视口）\n\tconst maxDistance = Math.sqrt(\n\t\tMath.pow(Math.max(x, window.innerWidth - x), 2) +\n\t\tMath.pow(Math.max(y, window.innerHeight - y), 2)\n\t);\n\n\t// 设置颜色波的初始位置和大小\n\tcolorWave.style.width = `${maxDistance * 2}px`;\n\tcolorWave.style.height = `${maxDistance * 2}px`;\n\tcolorWave.style.left = `${x - maxDistance}px`;\n\tcolorWave.style.top = `${y - maxDistance}px`;\n\n\t// 确保 colorWave 在所有内容之下\n\t// const contentElements = document.querySelectorAll('nav, main, footer');\n\t// contentElements.forEach(el => {\n\t// \tif (!el.style.zIndex || parseInt(el.style.zIndex) <= 10) {\n\t// \t\tel.style.zIndex = '20';\n\t// \t}\n\t// });\n\n\t// 添加到文档中\n\tdocument.body.appendChild(colorWave);\n\n\t// 使用 GSAP 创建扩散动画\n\tgsap.to(colorWave, {\n\t\tscale: 1,\n\t\tduration: 1.2,\n\t\tease: \"power2.out\",\n\t\tonComplete: function() {\n\t\t\t// 动画完成后更改背景色\n\t\t\t// document.body.style.backgroundColor = color;\n\t\t\tsetBg(color)\n\n\t\t\t// 延迟移除颜色波\n\t\t\tsetTimeout(() => {\n\t\t\t\tdocument.body.removeChild(colorWave);\n\t\t\t}, 500);\n\t\t}\n\t});\n}\n\n\n// 读取上次记录\nlet bgColor = localStorage.getItem('bg-color-value');\nif (bgColor) {\n\tsetBg(bgColor);\n}\n\n// 设置背景颜色 \nfunction setBg(bgColor) {\n\tconsole.log('---- 背景颜色设定为：', bgColor);\n\n\t// -------- 设置 body 背景\n\tdocument.body.style.backgroundColor = bgColor;\n\n\t// -------- 设置 header 头背景\n\t// 如果是 16 进制，转 rgba\n\tif (bgColor.indexOf('#') == 0) {\n\t\tbgColor = hexToRgba(bgColor, 0.97);\n\t}\n\t// 如果是 rgb，转 rgba\n\telse if (bgColor.match(/\\,/g).length == 2) {\n\t\tbgColor = bgColor.replace(')', ' ,0.97)');\n\t}\n\n\tdocument.querySelector('.doc-header').style.backgroundColor = bgColor;\n}\n\n// 16进制 转 rgba\nfunction hexToRgba(str, a) {\n\ta = a || 1;\n\n\tvar reg = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/\n\tif (!reg.test(str)) {\n\t\treturn;\n\t}\n\tlet newStr = (str.toLowerCase()).replace(/\\#/g, '')\n\tlet len = newStr.length;\n\tif (len == 3) {\n\t\tlet t = ''\n\t\tfor (var i = 0; i < len; i++) {\n\t\t\tt += newStr.slice(i, i + 1).concat(newStr.slice(i, i + 1))\n\t\t}\n\t\tnewStr = t\n\t}\n\tlet arr = []; //将字符串分隔，两个两个的分隔\n\tfor (var i = 0; i < 6; i = i + 2) {\n\t\tlet s = newStr.slice(i, i + 2)\n\t\tarr.push(parseInt(\"0x\" + s))\n\t}\n\treturn 'rgb(' + arr.join(\",\") + ', ' + a + ')';\n}"
  },
  {
    "path": "sa-token-doc/up/basic-auth.md",
    "content": "# Http Basic 认证 \n\nHttp Basic 是 http 协议中最基础的认证方式，其有两个特点：\n- 简单、易集成。\n- 功能支持度低。\n\n在 Sa-Token 中使用 Http Basic 认证非常简单，只需调用几个简单的方法 \n\n--- \n\n### 1、启用 Http Basic 认证 \n\n首先我们在一个接口中，调用 Http Basic 校验：\n``` java\n@RequestMapping(\"test3\")\npublic SaResult test3() {\n    SaHttpBasicUtil.check(\"sa:123456\");\n\t// ... 其它代码\n\treturn SaResult.ok();\n}\n```\n\n全局异常处理：\n``` java\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\t@ExceptionHandler\n\tpublic SaResult handlerException(Exception e) {\n\t\te.printStackTrace(); \n\t\treturn SaResult.error(e.getMessage());\n\t}\n}\n```\n\n然后我们访问这个接口时，浏览器会强制弹出一个表单：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/up/sa-basic.png\" alt=\"sa-basic.png\">\n\n\n当我们输入账号密码后 `（sa / 123456）`，才可以继续访问数据：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/up/sa-basic-ok.png\" alt=\"sa-basic-ok.png\">\n\n\n### 2、其它启用方式 \n``` java\n// 对当前会话进行 Http Basic 校验，账号密码为 yml 配置的值（例如：sa-token.http-basic=sa:123456）\nSaHttpBasicUtil.check();\n\n// 对当前会话进行 Http Basic 校验，账号密码为：`sa / 123456`\nSaHttpBasicUtil.check(\"sa:123456\");\n\n// 以注解方式启用 Http Basic 校验\n@SaCheckHttpBasic(account = \"sa:123456\")\n@RequestMapping(\"test3\")\npublic SaResult test3() {\n\treturn SaResult.ok();\n}\n\n// 在全局拦截器 或 过滤器中启用 Basic 认证 \n@Bean\npublic SaServletFilter getSaServletFilter() {\n\treturn new SaServletFilter()\n\t\t\t.addInclude(\"/**\").addExclude(\"/favicon.ico\")\n\t\t\t.setAuth(obj -> {\n\t\t\t\tSaRouter.match(\"/test/**\", () -> SaHttpBasicUtil.check(\"sa:123456\"));\n\t\t\t});\n}\n```\n\n### 3、URL 认证 \n除了访问后再输入账号密码外，我们还可以在 URL 中直接拼接账号密码通过 Basic 认证，例如：\n``` url\nhttp://sa:123456@127.0.0.1:8081/test/test3\n```\n\n\n### 4、Http Digest 认证 \n\nHttp Digest 认证是 Http Basic 认证的升级版，Http Digest 在提交请求时不会使用明文方式传输认证信息，而是使用一定的规则加密后提交。\n不过对于开发者来讲，开启 Http Digest 认证校验的流程与 Http Basic 认证基本是一致的。\n\n``` java\n// 测试 Http Digest 认证   浏览器访问： http://localhost:8081/test/testDigest\n@RequestMapping(\"testDigest\")\npublic SaResult testDigest() {\n\tSaHttpDigestUtil.check(\"sa\", \"123456\");\n\treturn SaResult.ok();\n}\n\n// 使用注解方式开启 Http Digest 认证\n@SaCheckHttpDigest(\"sa:123456\")\n@RequestMapping(\"testDigest2\")\npublic SaResult testDigest() {\n\treturn SaResult.ok();\n}\n\n\n// 对当前会话进行 Http Digest 校验，账号密码为 yml 配置的值（例如：sa-token.http-digest=sa:123456）\nSaHttpDigestUtil.check();\n```\n\n与上面的 Http Basic 认证一致，在访问这个路由时，浏览器会强制弹出一个表单，客户端输入正确的账号密码后即可通过校验。\n\n同样的，Http Digest 也支持在浏览器访问接口时直接使用 @ 符号拼接账号密码信息，使客户端直接通过校验。\n\n``` url\nhttp://sa:123456@127.0.0.1:8081/test/testDigest\n```\n\n\n\n--- \n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/HttpBasicController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token Http Basic 认证 —— [ HttpBasicController.java ]\n</a>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/up/disable.md",
    "content": "# 账号封禁\n\n之前的章节中，我们学习了 踢人下线 和 强制注销 功能，用于清退违规账号。\n\n在部分场景下，我们还需要将其 **账号封禁**，以防止其再次登录。\n\n--- \n\n### 1、账号封禁\n\n对指定账号进行封禁：\n\n``` java\n// 封禁指定账号 \nStpUtil.disable(10001, 86400); \n```\n\n参数含义：\n- 参数1：要封禁的账号id。\n- 参数2：封禁时间，单位：秒，此为 86400秒 = 1天（此值为 -1 时，代表永久封禁）。\n\n\n注意点：对于正在登录的账号，将其封禁并不会使它立即掉线，如果我们需要它即刻下线，可采用先踢再封禁的策略，例如：<br>\n``` java\n// 先踢下线\nStpUtil.kickout(10001); \n// 再封禁账号\nStpUtil.disable(10001, 86400); \n```\n\n待到下次登录时，我们先校验一下这个账号是否已被封禁：\n``` java\n// 校验指定账号是否已被封禁，如果被封禁则抛出异常 `DisableServiceException`\nStpUtil.checkDisable(10001); \n\n// 通过校验后，再进行登录：\nStpUtil.login(10001); \n```\n\n> [!ATTENTION| label:升级注意：] \n> 旧版本在 `StpUtil.login()` 时会自动校验账号是否被封禁，v1.31.0 之后将 校验封禁 和 登录 两个动作分离成两个方法，不再自动校验，请注意其中的逻辑更改。\n\n此模块所有方法：\n``` java\n// 封禁指定账号 \nStpUtil.disable(10001, 86400); \n\n// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁) \nStpUtil.isDisable(10001); \n\n// 校验指定账号是否已被封禁，如果被封禁则抛出异常 `DisableServiceException`\nStpUtil.checkDisable(10001); \n\n// 获取指定账号剩余封禁时间，单位：秒，如果该账号未被封禁，则返回-2 \nStpUtil.getDisableTime(10001); \n\n// 解除封禁\nStpUtil.untieDisable(10001); \n```\n\n\n### 2、分类封禁\n\n有的时候，我们并不需要将整个账号禁掉，而是只禁止其访问部分服务。\n\n假设我们在开发一个电商系统，对于违规账号的处罚，我们设定三种分类封禁：\n\n- 1、封禁评价能力：账号A 因为多次虚假好评，被限制订单评价功能。\n- 2、封禁下单能力：账号B 因为多次薅羊毛，被限制下单功能。\n- 3、封禁开店能力：账号C 因为店铺销售假货，被限制开店功能。\n\n相比于封禁账号的一刀切处罚，这里的关键点在于：每一项能力封禁的同时，都不会对其它能力造成影响。\n\n也就是说我们需要一种只对部分服务进行限制的能力，对应到代码层面，就是只禁止部分接口的调用。\n\n``` java\n// 封禁指定用户评论能力，期限为 1天\nStpUtil.disable(10001, \"comment\", 86400);\n```\n参数释义：\n- 参数1：要封禁的账号id。\n- 参数2：针对这个账号，要封禁的服务标识（可以是任意的自定义字符串）。\n- 参数3：要封禁的时间，单位：秒，此为 86400秒 = 1天（此值为 -1 时，代表永久封禁）。\n\n分类封禁模块所有可用API：\n\n``` java\n/*\n * 以下示例中：\"comment\"=评论服务标识、\"place-order\"=下单服务标识、\"open-shop\"=开店服务标识\n */\n\n// 封禁指定用户评论能力，期限为 1天\nStpUtil.disable(10001, \"comment\", 86400);\n\n// 在评论接口，校验一下，会抛出异常：`DisableServiceException`，使用 e.getService() 可获取业务标识 `comment` \nStpUtil.checkDisable(10001, \"comment\");\n\n// 在下单时，我们校验一下 下单能力，并不会抛出异常，因为我们没有限制其下单功能\nStpUtil.checkDisable(10001, \"place-order\");\n\n// 现在我们再将其下单能力封禁一下，期限为 7天 \nStpUtil.disable(10001, \"place-order\", 86400 * 7);\n\n// 然后在下单接口，我们添加上校验代码，此时用户便会因为下单能力被封禁而无法下单（代码抛出异常）\nStpUtil.checkDisable(10001, \"place-order\");\n\n// 但是此时，用户如果调用开店功能的话，还是可以通过，因为我们没有限制其开店能力 （除非我们再调用了封禁开店的代码）\nStpUtil.checkDisable(10001, \"open-shop\");\n```\n\n通过以上示例，你应该大致可以理解 `业务封禁 -> 业务校验` 的处理步骤。\n\n有关分类封禁的所有方法：\n``` java\n// 封禁：指定账号的指定服务 \nStpUtil.disable(10001, \"<业务标识>\", 86400); \n\n// 判断：指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) \nStpUtil.isDisable(10001, \"<业务标识>\"); \n\n// 校验：指定账号的指定服务 是否已被封禁，如果被封禁则抛出异常 `DisableServiceException`\nStpUtil.checkDisable(10001, \"<业务标识>\"); \n\n// 获取：指定账号的指定服务 剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）\nStpUtil.getDisableTime(10001, \"<业务标识>\"); \n\n// 解封：指定账号的指定服务\nStpUtil.untieDisable(10001, \"<业务标识>\"); \n```\n\n\n### 3、阶梯封禁\n\n对于多次违规的用户，我们常常采取阶梯处罚的策略，这种 “阶梯” 一般有两种形式：\n\n- 处罚时间阶梯：首次违规封禁 1 天，第二次封禁 7 天，第三次封禁 30 天，依次顺延……\n- 处罚力度阶梯：首次违规消息提醒、第二次禁言禁评论、第三次禁止账号登录，等等……\n\n基于处罚时间的阶梯，我们只需在封禁时 `StpUtil.disable(10001, 86400)` 传入不同的封禁时间即可，下面我们着重探讨一下基于处罚力度的阶梯形式。\n\n假设我们在开发一个论坛系统，对于违规账号的处罚，我们设定三种力度：\n\n- 1、轻度违规：封禁其发帖、评论能力，但允许其点赞、关注等操作。\n- 2、中度违规：封禁其发帖、评论、点赞、关注等一切与别人互动的能力，但允许其浏览帖子、浏览评论。\n- 3、重度违规：封禁其登录功能，限制一切能力。\n\n解决这种需求的关键在于，我们需要把不同处罚力度，量化成不同的处罚等级，比如上述的 `轻度`、`中度`、`重度` 3 个力度，\n我们将其量化为`一级封禁`、`二级封禁`、`三级封禁` 3个等级，数字越大代表封禁力度越高。\n\n然后我们就可以使用阶梯封禁的API，进行鉴权了：\n\n``` java\n// 阶梯封禁，参数：封禁账号、封禁级别、封禁时间 \nStpUtil.disableLevel(10001, 3, 10000);\n\n// 获取：指定账号封禁的级别 （如果此账号未被封禁则返回 -2）\nStpUtil.getDisableLevel(10001);\n\n// 判断：指定账号是否已被封禁到指定级别，返回 true 或 false\nStpUtil.isDisableLevel(10001, 3);\n\n// 校验：指定账号是否已被封禁到指定级别，如果已达到此级别（例如已被3级封禁，这里校验是否达到2级），则抛出异常 `DisableServiceException`\nStpUtil.checkDisableLevel(10001, 2);\n```\n\n注意点：`DisableServiceException` 异常代表当前账号未通过封禁校验，可以：\n- 通过 `e.getLevel()` 获取这个账号实际被封禁的等级。\n- 通过 `e.getLimitLevel()` 获取这个账号在校验时要求低于的等级。当 `Level >= LimitLevel` 时，框架就会抛出异常。\n\n如果业务足够复杂，我们还可能将 分类封禁 和 阶梯封禁 组合使用：\n\n``` java\n// 分类阶梯封禁，参数：封禁账号、封禁服务、封禁级别、封禁时间 \nStpUtil.disableLevel(10001, \"comment\", 3, 10000);\n\n// 获取：指定账号的指定服务 封禁的级别 （如果此账号未被封禁则返回 -2）\nStpUtil.getDisableLevel(10001, \"comment\");\n\n// 判断：指定账号的指定服务 是否已被封禁到指定级别，返回 true 或 false\nStpUtil.isDisableLevel(10001, \"comment\", 3);\n\n// 校验：指定账号的指定服务 是否已被封禁到指定级别（例如 comment服务 已被3级封禁，这里校验是否达到2级），如果已达到此级别，则抛出异常 \nStpUtil.checkDisableLevel(10001, \"comment\", 2);\n```\n\n\n\n### 4、使用注解完成封禁校验\n首先我们需要注册 Sa-Token 全局拦截器（可参考 [注解鉴权](/use/at-check) 章节），然后我们就可以使用以下注解校验账号是否封禁\n\n``` java\n// 校验当前账号是否被封禁，如果已被封禁会抛出异常，无法进入方法 \n@SaCheckDisable\n@PostMapping(\"send\")\npublic SaResult send() {\n\t// ... \n\treturn SaResult.ok(); \n}\n\n// 校验当前账号是否被封禁 comment 服务，如果已被封禁会抛出异常，无法进入方法 \n@SaCheckDisable(\"comment\")\n@PostMapping(\"send\")\npublic SaResult send() {\n\t// ... \n\treturn SaResult.ok(); \n}\n\n// 校验当前账号是否被封禁 comment、place-order、open-shop 等服务，指定多个值，只要有一个已被封禁，就无法进入方法 \n@SaCheckDisable({\"comment\", \"place-order\", \"open-shop\"})\n@PostMapping(\"send\")\npublic SaResult send() {\n\t// ... \n\treturn SaResult.ok(); \n}\n\n// 阶梯封禁，校验当前账号封禁等级是否达到5级，如果达到则抛出异常 \n@SaCheckDisable(level = 5)\n@PostMapping(\"send\")\npublic SaResult send() {\n\t// ... \n\treturn SaResult.ok(); \n}\n\n// 分类封禁 + 阶梯封禁 校验：校验当前账号的 comment 服务，封禁等级是否达到5级，如果达到则抛出异常 \n@SaCheckDisable(value = \"comment\", level = 5)\n@PostMapping(\"send\")\npublic SaResult send() {\n\t// ... \n\treturn SaResult.ok(); \n}\n```\n\n\n### 5、封禁信息持久化\n\nSa-Token 默认将封禁信息储存在缓存中，缓存中的数据是“临时性的”、“易丢失的”，而在大多数系统的设计中，需要将封禁数据持久化到数据库中。\n\n要使封禁信息持久化，你只需要在调用 Sa-Token 的封禁 API 后，再继续调用插入数据库的代码即可，形如：\n\n``` java\n// 在 Sa-Token 框架中封禁指定账号\nStpUtil.disable(10001, 86400); \n\n// 更改数据库中此人信息 (举例代码)\nuserMapper.disableUser(10001);\n```\n\n这样即可保证封禁数据同步插入到缓存和数据库中，但是还有一个问题，如果我们的程序或缓存中间件重启了，导致缓存数据丢失，\n那再调用 `StpUtil.checkDisable(10001)` 代码将没有效果，无法约束到此用户。\n\n比较次的解决方案是在程序启动时，读取数据库中所有封禁信息同步到缓存中去，但是如果封禁记录较多这样将会严重拖慢程序启动时间。\n\nSa-Token 提供一种方案，可以在你调用 `StpUtil.checkDisable(10001)` 校验封禁时才会触发查询数据库 10001 账号到底有没有被封禁。\n你只需要实现 `StpInterface` 的 `isDisabled` 方法即可，例：\n\n``` java\n@Component\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回指定账号 id 是否被封禁\n\t *\n\t * @param loginId  账号id\n\t * @param service 业务标识符\n\t * @return 描述该账号是否封禁的包装信息对象\n\t */\n\tpublic SaDisableWrapperInfo isDisabled(Object loginId, String service) {\n\t\t// 查库操作 ...  (此处仅做示例代码)\n\t\treturn SaDisableWrapperInfo.createDisabled(86400, 1);\n\t}\n\n}\n```\n\n该方法返回一个 `SaDisableWrapperInfo` 实例对象，用来描述指定账号是否已被封禁，一般有以下几种写法：\n``` java\n// 标准写法：new 对象返回，参数为：是否被封禁、封禁时间(秒)、封禁等级\npublic SaDisableWrapperInfo isDisabled(Object loginId, String service) {\n\treturn new SaDisableWrapperInfo(true, 86400, 1);\n}\n\n// 快捷写法：被封禁，解封倒计时86400秒，封禁等级1\npublic SaDisableWrapperInfo isDisabled(Object loginId, String service) {\n\treturn SaDisableWrapperInfo.createDisabled(86400, 1);\n}\n\n// 快捷写法：未被封禁 \npublic SaDisableWrapperInfo isDisabled(Object loginId, String service) {\n\treturn SaDisableWrapperInfo.createNotDisabled();\n}\n\n// 快捷写法：未被封禁，且将查询结果保存到缓存中，ttl为86400，改时间内不再重复进入 isDisabled 方法 \npublic SaDisableWrapperInfo isDisabled(Object loginId, String service) {\n\treturn SaDisableWrapperInfo.createNotDisabled(86400);\n}\n```\n\n\n--- \n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/DisableController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 账号禁用  —— [ DisableController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/global-filter.md",
    "content": "# 全局过滤器\n--- \n\n### 组件简述\n\n之前的章节中，我们学习了“根据拦截器实现路由拦截鉴权”，其实在大多数web框架中，使用过滤器可以实现同样的功能，本章我们就利用Sa-Token全局过滤器来实现路由拦截器鉴权。\n\n首先我们先梳理清楚一个问题，既然拦截器已经可以实现路由鉴权，为什么还要用过滤器再实现一遍呢？简而言之：\n1. 相比于拦截器，过滤器更加底层，执行时机更靠前，有利于防渗透扫描。\n2. 过滤器可以拦截静态资源，方便我们做一些权限控制。\n3. 部分Web框架根本就没有提供拦截器功能，但几乎所有的Web框架都会提供过滤器机制。\n\n但是过滤器也有一些缺点，比如：\n1. 由于太过底层，导致无法率先拿到`HandlerMethod`对象，无法据此添加一些额外功能。\n2. 由于拦截的太全面了，导致我们需要对很多特殊路由(如`/favicon.ico`)做一些额外处理。\n3. 在Spring中，过滤器中抛出的异常无法进入全局`@ExceptionHandler`，我们必须额外编写代码进行异常处理。\n\nSa-Token同时提供过滤器和拦截器机制，不是为了让谁替代谁，而是为了让大家根据自己的实际业务合理选择，拥有更多的发挥空间。\n\n\n### 在 SpringBoot 中注册过滤器\n同拦截器一样，为了避免不必要的性能浪费，Sa-Token全局过滤器默认处于关闭状态，若要使用过滤器组件，首先你需要注册它到项目中：\n``` java\n/**\n * [Sa-Token 权限认证] 配置类 \n */\n@Configuration\npublic class SaTokenConfigure {\n\t\n\t/**\n\t * 注册 [Sa-Token全局过滤器] \n\t */\n\t@Bean\n\tpublic SaServletFilter getSaServletFilter() {\n        return new SaServletFilter()\n\t\t\n        \t\t// 指定 拦截路由 与 放行路由\n        \t\t.addInclude(\"/**\").addExclude(\"/favicon.ico\")    /* 排除掉 /favicon.ico */\n\t\t\t\t\n        \t\t// 认证函数: 每次请求执行 \n        \t\t.setAuth(obj -> {\n\t\t\t\t\tSystem.out.println(\"---------- 进入Sa-Token全局认证 -----------\");\n\t\t\t\t\t\n\t\t\t\t\t// 登录认证 -- 拦截所有路由，并排除/user/doLogin 用于开放登录 \n\t\t\t\t\tSaRouter.match(\"/**\", \"/user/doLogin\", () -> StpUtil.checkLogin());\n\t\t\t\t\t\n\t\t\t\t\t// 更多拦截处理方式，请参考“路由拦截式鉴权”章节 */\n        \t\t})\n\t\t\t\t\n        \t\t// 异常处理函数：每次认证函数发生异常时执行此函数 \n        \t\t.setError(e -> {\n\t\t\t\t\tSystem.out.println(\"---------- 进入Sa-Token异常处理 -----------\");\n        \t\t\treturn SaResult.error(e.getMessage());\n        \t\t})\n\t\t\t\t\n        \t\t// 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）\n        \t\t.setBeforeAuth(r -> {\n        \t\t\t// ---------- 设置一些安全响应头 ----------\n        \t\t\tSaHolder.getResponse()\n        \t\t\t// 服务器名称 \n        \t\t\t.setServer(\"sa-server\")\n        \t\t\t// 是否可以在iframe显示视图： DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 \n        \t\t\t.setHeader(\"X-Frame-Options\", \"SAMEORIGIN\")\n        \t\t\t// 是否启用浏览器默认XSS防护： 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时，停止渲染页面 \n        \t\t\t.setHeader(\"X-XSS-Protection\", \"1; mode=block\")\n        \t\t\t// 禁用浏览器内容嗅探 \n        \t\t\t.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n        \t\t\t;\n        \t\t})\n        \t\t;\n\t}\n\t\n}\n```\n\n> [!WARNING| label:注意事项：] \n> - 在`[认证函数]`里，你可以写和拦截器里一致的代码，进行路由匹配鉴权，参考：[路由拦截鉴权](/use/route-check)。\n> - 由于过滤器中抛出的异常不进入全局异常处理，所以你必须提供`[异常处理函数]`来处理`[认证函数]`里抛出的异常。\n> - 在`[异常处理函数]`里的返回值，将作为字符串输出到前端，如果需要定制化返回数据，请注意其中的格式转换。\n\n改写 `setError` 函数的响应格式示例：\n``` java\n.setError(e -> {\n\t// 设置响应头\n\tSaHolder.getResponse().setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n\t// 使用封装的 JSON 工具类转换数据格式 \n\treturn JSONUtil.toJsonStr( SaResult.error(e.getMessage()) );\n})\n```\nJSON 工具类可参考：[Hutool-Json](https://hutool.cn/docs/#/json/JSONUtil)\n\n\n### 自定义过滤器执行顺序\n\nSaServletFilter 默认执行顺序为 `-100`，如果你要自定义过滤器的执行顺序，可以使用 `FilterRegistrationBean` 注册，参考：\n\n``` java\n/**\n * 注册 [Sa-Token 全局过滤器]\n */\n@Bean\npublic FilterRegistrationBean<SaServletFilter> getSaServletFilter() {\n\tFilterRegistrationBean<SaServletFilter> frBean = new FilterRegistrationBean<>();\n\tfrBean.setFilter(\n\t\t\tnew SaServletFilter()\n\t\t\t\t.addInclude(\"/**\")\n\t\t\t\t.setAuth(obj -> {\n\t\t\t\t\t// ....\n\t\t\t\t})\n\t\t\t\t// 等等，其它代码 ... \n\t);\n\tfrBean.setOrder(-101);  // 更改顺序为 -101\n\treturn frBean;\n}\n```\n\n在 SpringBoot 中， Order 值越小，执行时机越靠前。\n\n\n### 在 WebFlux 中注册过滤器\n`Spring WebFlux`中不提供拦截器机制，因此若你的项目需要路由鉴权功能，过滤器是你唯一的选择，在`Spring WebFlux`注册过滤器的流程与上述流程几乎完全一致，\n除了您需要将过滤器名称由`SaServletFilter`更换为`SaReactorFilter`以外，其它所有步骤均可参考以上示例。\n``` java\n/**\n * [Sa-Token 权限认证] 配置类 \n */\n@Configuration\npublic class SaTokenConfigure {\n\t\t\n\t/**\n\t * 注册 [Sa-Token全局过滤器] \n\t */\n\t@Bean\n\tpublic SaReactorFilter getSaReactorFilter() {\n\t\treturn new SaReactorFilter()\n\t\t\t// 其它代码... \n\t\t;\n\t}\n\t\n}\n```\n\t\t\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 全局过滤器 —— [ SaTokenConfigure.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/global-listener.md",
    "content": "# 全局侦听器\n\n--- \n\n### 1、工作原理\n\nSa-Token 提供一种侦听器机制，通过注册侦听器，你可以订阅框架的一些关键性事件，例如：用户登录、退出、被踢下线等。 \n\n事件触发流程大致如下：\n\n<img class=\"s-w\" src=\"/big-file/doc/up/sa-token-listener.svg\" alt=\"sa-token-listener\">\n\n框架默认内置了侦听器 `SaTokenListenerForLog` 实现：[代码参考](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/listener/SaTokenListenerForLog.java)\n，功能是控制台 log 打印输出，你可以通过配置`sa-token.is-log=true`开启。\n\n要注册自定义的侦听器也非常简单：\n1. 新建类实现 `SaTokenListener` 接口。\n2. 将实现类注册到 `SaTokenEventCenter` 事件发布中心。\n\n\n### 2、自定义侦听器实现\n\n##### 2.1、新建实现类：\n\n新建`MySaTokenListener.java`，实现`SaTokenListener`接口，并添加上注解`@Component`，保证此类被`SpringBoot`扫描到：\n\n``` java\n/**\n * 自定义侦听器的实现 \n */\n@Component\npublic class MySaTokenListener implements SaTokenListener {\n\n\t/** 每次登录时触发 */\n\t@Override\n\tpublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doLogin\");\n\t}\n\n\t/** 每次注销时触发 */\n\t@Override\n\tpublic void doLogout(String loginType, Object loginId, String tokenValue) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doLogout\");\n\t}\n\n\t/** 每次被踢下线时触发 */\n\t@Override\n\tpublic void doKickout(String loginType, Object loginId, String tokenValue) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doKickout\");\n\t}\n\n\t/** 每次被顶下线时触发 */\n\t@Override\n\tpublic void doReplaced(String loginType, Object loginId, String tokenValue) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doReplaced\");\n\t}\n\n\t/** 每次被封禁时触发 */\n\t@Override\n\tpublic void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doDisable\");\n\t}\n\n\t/** 每次被解封时触发 */\n\t@Override\n\tpublic void doUntieDisable(String loginType, Object loginId, String service) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doUntieDisable\");\n\t}\n\n\t/** 每次二级认证时触发 */\n\t@Override\n\tpublic void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doOpenSafe\");\n\t}\n\n\t/** 每次退出二级认证时触发 */\n\t@Override\n\tpublic void doCloseSafe(String loginType, String tokenValue, String service) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doCloseSafe\");\n\t}\n\n\t/** 每次创建Session时触发 */\n\t@Override\n\tpublic void doCreateSession(String id) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doCreateSession\");\n\t}\n\n\t/** 每次注销Session时触发 */\n\t@Override\n\tpublic void doLogoutSession(String id) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doLogoutSession\");\n\t}\n\t\n\t/** 每次Token续期时触发 */\n    @Override\n\tpublic void doRenewTimeout(String tokenValue, Object loginId, long timeout) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doRenewTimeout\");\n\t}\n\t\n    /** 每次Token续期时触发 */\n    @Override\n    public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {\n        System.out.println(\"---------- 自定义侦听器实现 doRenewTimeout\");\n    }\n\n}\n```\n\n##### 2.2、将侦听器注册到事件中心：\n\n以上代码由于添加了 `@Component` 注解，会被 SpringBoot 扫描并自动注册到事件中心，此时我们无需手动注册。\n\n如果我们没有添加 `@Component` 注解或者项目属于非 IOC 自动注入环境，则需要我们手动将这个侦听器注册到事件中心：\n\n``` java\n// 将侦听器注册到事件发布中心\nSaTokenEventCenter.registerListener(new MySaTokenListener());\n```\n\n事件中心的其它一些常用方法：\n\n``` java\n// 获取已注册的所有侦听器 \nSaTokenEventCenter.getListenerList(); \n\n// 重置侦听器集合 \nSaTokenEventCenter.setListenerList(listenerList); \n\n// 注册一个侦听器 \nSaTokenEventCenter.registerListener(listener); \n\n// 注册一组侦听器 \nSaTokenEventCenter.registerListenerList(listenerList); \n\n// 移除一个侦听器 \nSaTokenEventCenter.removeListener(listener); \n\n// 移除指定类型的所有侦听器 \nSaTokenEventCenter.removeListener(cls); \n\n// 清空所有已注册的侦听器 \nSaTokenEventCenter.clearListener(); \n\n// 判断是否已经注册了指定侦听器  \nSaTokenEventCenter.hasListener(listener); \n\n// 判断是否已经注册了指定类型的侦听器   \nSaTokenEventCenter.hasListener(cls); \n```\n\n##### 2.3、启动测试：\n在 `TestController` 中添加登录测试代码：\n``` java\n// 测试登录接口 \n@RequestMapping(\"login\")\npublic SaResult login() {\n\tSystem.out.println(\"登录前\");\n\tStpUtil.login(10001);\t\t\n\tSystem.out.println(\"登录后\");\n\treturn SaResult.ok();\n}\n```\n\n启动项目，访问登录接口，观察控制台输出：\n\n<img class=\"s-w-sh\" src=\"/big-file/doc/up/sa-token-listener-println.png\" alt=\"sa-token-listener-println\">\n\n\n### 3、其它注意点\n\n##### 3.1、你可以通过继承 `SaTokenListenerForSimple` 快速实现一个侦听器：\n\n``` java\n@Component\npublic class MySaTokenListener extends SaTokenListenerForSimple {\n\t/*\n\t * SaTokenListenerForSimple 对所有事件提供了空实现，通过继承此类，你只需重写一部分方法即可实现一个可用的侦听器。\n\t */\n\t/** 每次登录时触发 */\n\t@Override\n\tpublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n\t\tSystem.out.println(\"---------- 自定义侦听器实现 doLogin\");\n\t}\n}\n```\n\n##### 3.2、使用匿名内部类的方式注册：\n``` java\n// 登录时触发 \nSaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {\n\t@Override\n\tpublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n\t\tSystem.out.println(\"---------------- doLogin\");\n\t}\n});\n```\n\n##### 3.3、使用 try-catch 包裹不安全的代码：\n如果你认为你的事件处理代码是不安全的（代码可能在运行时抛出异常），则需要使用 `try-catch` 包裹代码，以防因为抛出异常导致 Sa-Token 的整个登录流程被强制中断。\n\n``` java\n// 登录时触发 \nSaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {\n\t@Override\n\tpublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {\n\t\ttry {\n\t\t\t// 不安全代码需要写在 try-catch 里 \n\t\t\t// ......  \n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n});\n```\n\n##### 3.4、疑问：一个项目可以注册多个侦听器吗？\n可以，多个侦听器间彼此独立，互不影响，按照注册顺序依次接受到事件通知。\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/MySaTokenListener.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 自定义侦听器  —— [ MySaTokenListener.java ]\n</a>\n\n"
  },
  {
    "path": "sa-token-doc/up/integ-redis.md",
    "content": "# Sa-Token 集成 Redis \n--- \n\nSa-Token 默认将数据保存在内存中，此模式读写速度最快，且避免了序列化与反序列化带来的性能消耗，但是此模式也有一些缺点，比如：\n\n1. 重启后数据会丢失。\n2. 无法在分布式环境中共享数据。\n\n为此，Sa-Token 提供了扩展接口，你可以轻松将会话数据存储在一些专业的缓存中间件上（比如 Redis），\n做到重启数据不丢失，而且保证分布式环境下多节点的会话一致性。\n\n---\n\n### 1、Sa-Token 整合 RedisTemplate \n\nRedisTemplate 是 SpringBoot 官方推荐的 Redis 客户端，Sa-Token 提供基于 RedisTemplate 的 Redis 整合方案：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n\n<!-- 提供 Redis 连接池 -->\n<dependency>\n\t<groupId>org.apache.commons</groupId>\n\t<artifactId>commons-pool2</artifactId>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// Sa-Token 整合 RedisTemplate\nimplementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'\n\n// 提供 Redis 连接池\nimplementation 'org.apache.commons:commons-pool2'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n\nRedis 的集成有多种方式，缓存的方案也不止 Redis 一种，Sa-Token 为缓存方案提供多种扩展实现。\n\n如果你对 Sa-Token 还不太熟悉，或者只想“省心省事”，我们推荐你直接使用上述的 RedisTemplate 集成方案，而不必进行过多研究。到此为止，你可以跳转到下一章节了。\n\n如果你想对缓存方案再进行一下深入探究，那么你可以参考：[缓存层扩展](/plugin/dao-extend) \n\n\n### 2、自定义序列化方案\n\n如果你按照上述 RedisTemplate 方案进行集成测试，会发现框架在 Redis 中是以 json 格式存储数据的。可以自定义数据序列化格式吗？当然是可以的。\n\n框架的默认序列化层调用为 `String 序列化` -> `JSON 序列化`。要自定义数据序列化方式你可以从这两方面入手：\n\n\n#### 2.1、自定义 JSON 序列化方案：\n\n先说较为底层的 `JSON 序列化`，如果你引入的是 sa-token-spring-boot-starter 集成包 (含SpringBoot3) ，那么框架将会自动引入 Jackson 框架作为 JSON 序列化方案。\n\n如果你想更换为其它 JSON 解析框架，可以引入相关依赖：\n\n\n<!------------------------------ tabs:start ------------------------------>\n\n<!------------- tab:Fastjson ------------->\n``` xml\n<!-- Sa-Token 整合 Fastjson -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-fastjson</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-fastjson:${sa.top.version}'`\n\n<!------------- tab:Fastjson2 ------------->\n``` xml\n<!-- Sa-Token 整合 Fastjson2 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-fastjson2</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-fastjson2:${sa.top.version}'`\n\n<!------------- tab:Snack3 ------------->\n``` xml\n<!-- Sa-Token 整合 Snack3 -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-snack3</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\nGradle 参考：`implementation 'cn.dev33:sa-token-snack3:${sa.top.version}'`\n\n<!---------------------------- tabs:end ------------------------------>\n\n完整插件列表请参考：[JSON 序列化扩展](/plugin/json-extend)\n\n#### 2.2、自定义 String 序列化方案：\n\n或者你想更直接点，不使用 json 序列化方案，也是可以的。你可以直接自定义数据的 String 序列化方案：\n\n<!------------------------------ tabs:start ------------------------------>\n\n<!------------- tab:jdk序列化 (base64编码) ------------->\n``` java\n// 设置序列化方案: jdk序列化 (base64编码)\n@PostConstruct\npublic void rewriteComponent() {\n\tSaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseBase64());\n}\n```\n\n<!------------- tab:jdk序列化 (16进制编码) ------------->\n``` java\n// 设置序列化方案: jdk序列化 (16进制编码)\n@PostConstruct\npublic void rewriteComponent() {\n\tSaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseHex());\n}\n```\n\n<!------------- tab:jdk序列化 (ISO-8859-1编码) ------------->\n``` java\n// 设置序列化方案: jdk序列化 (ISO-8859-1编码)\n@PostConstruct\npublic void rewriteComponent() {\n\tSaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseISO_8859_1());\n}\n```\n<!---------------------------- tabs:end ------------------------------>\n\n除了以上的几种序列化方案，我们还提供了序列化扩展包，详细可参考：[序列化插件扩展包](/plugin/custom-serializer)\n\n\n### 3、集成 Redis 请注意：\n\n**1. 引入了依赖，我还需要为 Redis 配置连接信息吗？** <br>\n需要！只有项目初始化了正确的 Redis 实例，`Sa-Token`才可以使用 Redis 进行数据持久化，参考以下`yml配置`：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:yaml 风格 -------->\n``` yaml\nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        # password: \n        # 连接超时时间\n        timeout: 10s\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n```\n<!-------- tab:properties 风格 -------->\n``` properties\n# Redis数据库索引（默认为0）\nspring.redis.database=1\n# Redis服务器地址\nspring.redis.host=127.0.0.1\n# Redis服务器连接端口\nspring.redis.port=6379\n# Redis服务器连接密码（默认为空）\n# spring.redis.password=\n# 连接超时时间\nspring.redis.timeout=10s\n# 连接池最大连接数\nspring.redis.lettuce.pool.max-active=200\n# 连接池最大阻塞等待时间（使用负值表示没有限制）\nspring.redis.lettuce.pool.max-wait=-1ms\n# 连接池中的最大空闲连接\nspring.redis.lettuce.pool.max-idle=10\n# 连接池中的最小空闲连接\nspring.redis.lettuce.pool.min-idle=0\n```\n<!---------------------------- tabs:end ------------------------------>\n\n> [!WARNING| label:小提示 ] \n> 如果你使用的是 SpringBoot3.x 版本，则需要将前缀 `spring.redis` 改为 `spring.data.redis`。\n\n\n**2. 集成 Redis 后，是我额外手动保存数据，还是框架自动保存？** <br>\n框架自动保存。集成 `Redis` 只需要引入对应的 `pom依赖` 即可，框架所有上层 API 保持不变。\n\n**3. 集成包版本问题** <br>\nSa-Token-Redis 集成包的版本尽量与 Sa-Token-Starter 集成包的版本一致，否则可能出现兼容性问题。\n\n\n\n### 4、扩展：集成 MongoDB \n\n- [集成 MongoDB 参考一](/up/integ-spring-mongod-1)\n- [集成 MongoDB 参考二](/up/integ-spring-mongod-2)\n"
  },
  {
    "path": "sa-token-doc/up/integ-spring-mongod-1.md",
    "content": "# Sa-Token 集成 MongoDB \n--- \n\n此章介绍如何通过扩展 `SaTokenDao` 接口来实现 MongodDB 的集成。\n\n[示例代码：sa-token-mongodb-demo](https://gitee.com/lilihao/sa-token-mongodb-demo)\n\n先决条件：\n1. Spring Boot 3\n2. Spring Data Mongodb\n\n以下是依赖的引入：\n\n---\n\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- 引入 spring data mongodb -->\n<dependency>\n\t<groupId>org.springframework.boot</groupId>\n\t<artifactId>spring-boot-starter-data-mongodb</artifactId>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// 引入 spring data mongodb\nimplementation 'org.springframework.boot:spring-boot-starter-data-mongodb'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n优点：少量改造即可完成集成 MongodDB\n\n\n\n\n### 集成代码：\n\n\n**1. 创建一个类来包装`Sa—Token`的数据**\n```java\n@Document(\"saTokenMongo\") // 你也可以自定义集合名称\npublic class SaTokenMongoData {\n\n    @Id\n    private String id;\n\n    // token\n    @Indexed(unique = true)\n    private String key;\n\n    // sa-token 的 session\n    private SaSession session;\n\n    // sa-token 的 token string\n    private String string;\n\n    //使用 @SuppressWarnings(\"removal\") 的目的是，防止IDEA报错，因为 expireAfterSeconds是不在支持的属性。\n    @SuppressWarnings(\"removal\")\n    // 给 expireAt 添加 `@Indexed(expireAfterSeconds = 0)` 注解，当过期时MongoDB会自动帮我删除过期的数据\n    @Indexed(expireAfterSeconds = 0)\n    private LocalDateTime expireAt; // 你也可以使用 Date 类型，对应的在`SaTokenMongoDao`中，需要将LocalDateTime替换成Date\n\n    // 忽略 getter setter\n}\n```\n\n**2.实现 SaTokenDao**\n\n这个 SaTokenMongoDao 是仿照官方的 redis 集成实现的\n```java\npackage com.xx.xx.security;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport org.springframework.data.mongodb.core.MongoTemplate;\nimport org.springframework.data.mongodb.core.query.Criteria;\nimport org.springframework.data.mongodb.core.query.Query;\nimport org.springframework.data.mongodb.core.query.Update;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\n\nimport java.time.Duration;\nimport java.time.LocalDateTime;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\n\n@Component\npublic class SaTokenMongoDao implements SaTokenDao {\n\n    private final MongoTemplate mongoTemplate;\n\n    public SaTokenMongoDao(MongoTemplate mongoTemplate) {\n        this.mongoTemplate = mongoTemplate;\n    }\n\n\n    private Query keyQuery(String key) {\n        return Query.query(Criteria.where(\"key\").is(key));\n    }\n\n    /**\n     * 获取 value，如无返空\n     *\n     * @param key 键名称\n     * @return value\n     */\n    @Override\n    public String get(String key) {\n\n        return Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getString).orElse(null);\n    }\n\n\n    LocalDateTime getExpireAtFromTimeout(long timeout) {\n        // 当接受到的值是`SaTokenDao.NEVER_EXPIRE`时，说明永不过期，对应的我们需要把 expireAt 设置为null mongodb就不会删除这个记录\n        return timeout == SaTokenDao.NEVER_EXPIRE ? null : LocalDateTime.now().plusSeconds(timeout);\n    }\n\n    /**\n     * 写入 value，并设定存活时间（单位: 秒）\n     *\n     * @param key     键名称\n     * @param value   值\n     * @param timeout 数据有效期（值大于0时限时存储，值=-1时永久存储，值=0或小于等于-2时不存储）\n     */\n    @Override\n    public void set(String key, String value, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n\n        // 判断是否为永不过期\n        mongoTemplate.upsert(\n                keyQuery(key),\n                Update.update(\"string\", value).set(\"expireAt\", getExpireAtFromTimeout(timeout)),\n                SaTokenMongoData.class\n        );\n    }\n\n    /**\n     * 更新 value （过期时间不变）\n     *\n     * @param key   键名称\n     * @param value 值\n     */\n    @Override\n    public void update(String key, String value) {\n        long expire = getTimeout(key);\n        // -2 = 无此键\n        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        this.set(key, value, expire);\n    }\n\n    /**\n     * 删除 value\n     *\n     * @param key 键名称\n     */\n    @Override\n    public void delete(String key) {\n        mongoTemplate.remove(keyQuery(key), SaTokenMongoData.class);\n    }\n\n    /**\n     * 获取 value 的剩余存活时间（单位: 秒）\n     *\n     * @param key 指定 key\n     * @return 这个 key 的剩余存活时间\n     */\n    @Override\n    public long getTimeout(String key) {\n\n        LocalDateTime localDateTime = Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getExpireAt).orElse(LocalDateTime.MIN);\n\n        long seconds = Duration.between(LocalDateTime.now(), localDateTime).getSeconds();\n        if (seconds < 0) {\n            return 0;\n        }\n        return seconds;\n    }\n\n    /**\n     * 修改 value 的剩余存活时间（单位: 秒）\n     *\n     * @param key     指定 key\n     * @param timeout 过期时间（单位: 秒）\n     */\n    @Override\n    public void updateTimeout(String key, long timeout) {\n        // 判断是否想要设置为永久\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            long expire = getTimeout(key);\n            //noinspection StatementWithEmptyBody\n            if (expire == SaTokenDao.NEVER_EXPIRE) {\n                // 如果其已经被设置为永久，则不作任何处理\n            } else {\n                // 如果尚未被设置为永久，那么再次set一次\n                this.set(key, this.get(key), timeout);\n            }\n            return;\n        }\n\n        mongoTemplate.upsert(\n                keyQuery(key),\n                Update.update(\"expireAt\", getExpireAtFromTimeout(timeout)),\n                SaTokenMongoData.class\n        );\n    }\n\n    /**\n     * 获取 Object，如无返空\n     *\n     * @param key 键名称\n     * @return object\n     */\n    @Override\n    public Object getObject(String key) {\n        return Optional.ofNullable(mongoTemplate.findOne(keyQuery(key), SaTokenMongoData.class)).map(SaTokenMongoData::getSession).orElse(null);\n    }\n\n    /**\n     * 写入 Object，并设定存活时间 （单位: 秒）\n     *\n     * @param key     键名称\n     * @param object  值\n     * @param timeout 存活时间（值大于0时限时存储，值=-1时永久存储，值=0或小于等于-2时不存储）\n     */\n    @Override\n    public void setObject(String key, Object object, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        // 判断是否为永不过期\n        mongoTemplate.upsert(\n                keyQuery(key),\n                Update.update(\"session\", object).set(\"expireAt\", getExpireAtFromTimeout(timeout)),\n                SaTokenMongoData.class\n        );\n    }\n\n    /**\n     * 更新 Object （过期时间不变）\n     *\n     * @param key    键名称\n     * @param object 值\n     */\n    @Override\n    public void updateObject(String key, Object object) {\n        long expire = getObjectTimeout(key);\n        // -2 = 无此键\n        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        this.setObject(key, object, expire);\n    }\n\n    /**\n     * 删除 Object\n     *\n     * @param key 键名称\n     */\n    @Override\n    public void deleteObject(String key) {\n        delete(key);\n    }\n\n    /**\n     * 获取 Object 的剩余存活时间 （单位: 秒）\n     *\n     * @param key 指定 key\n     * @return 这个 key 的剩余存活时间\n     */\n    @Override\n    public long getObjectTimeout(String key) {\n        return getTimeout(key);\n    }\n\n    /**\n     * 修改 Object 的剩余存活时间（单位: 秒）\n     *\n     * @param key     指定 key\n     * @param timeout 剩余存活时间\n     */\n    @Override\n    public void updateObjectTimeout(String key, long timeout) {\n        // 判断是否想要设置为永久\n        updateTimeout(key, timeout);\n    }\n\n    /**\n     * 搜索数据\n     *\n     * @param prefix   前缀\n     * @param keyword  关键字\n     * @param start    开始处索引\n     * @param size     获取数量  (-1代表从 start 处一直取到末尾)\n     * @param sortType 排序类型（true=正序，false=反序）\n     * @return 查询到的数据集合\n     */\n    @Override\n    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\n        List<Criteria> criteriaList = new ArrayList<>();\n\n        if (StringUtils.hasText(prefix)) {\n            criteriaList.add(Criteria.where(\"key\").regex(Pattern.compile(\"^\" + Pattern.quote(prefix))));\n        }\n        if (StringUtils.hasText(keyword)) {\n            Pattern keywordPattern = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE);\n            criteriaList.add(Criteria.where(\"key\").regex(keywordPattern));\n        }\n\n\n        Criteria criteria = new Criteria();\n\n        if (!criteriaList.isEmpty()) {\n            criteria.andOperator(criteriaList);\n        }\n\n        long skip = (long) Math.max(start, 0) * Math.max(size, 1);\n\n        Query query = Query.query(criteria).skip(skip).limit(size);\n\n        query.fields().include(\"key\");\n\n        return mongoTemplate.find(query, SaTokenMongoData.class).stream().map(SaTokenMongoData::getKey).toList();\n    }\n}\n```\n\n\n\n"
  },
  {
    "path": "sa-token-doc/up/integ-spring-mongod-2.md",
    "content": "# Sa-Token 集成 MongoDB \n--- \n\n在 Spring Boot 下集成 MongoDB：\n\n<!---------------------------- tabs:start ------------------------------>\n<!-------- tab:Maven 方式 -------->\n``` xml \n<!-- 提供MongoDB依赖 -->\n<dependency>\n\t<groupId>org.springframework.boot</groupId>\n\t<artifactId>spring-boot-starter-data-mongodb</artifactId>\n</dependency>\n```\n<!-------- tab:Gradle 方式 -------->\n``` gradle\n// 提供MongoDB依赖\nimplementation 'org.springframework.boot:spring-boot-starter-data-mongodb'\n```\n<!---------------------------- tabs:end ------------------------------>\n\n1. 创建一个 `MySaSession` 并继承 `SaSession`\n```java\npublic class MySaSession extends SaSession {\n    public MySaSession(String id) {\n        super(id);\n    }\n\n    public void setDataMap(Map<String, Object> dataMap) {\n        refreshDataMap(dataMap);\n    }\n}\n```\n原因：由于 `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()` 错误\n\n2. 在 `SpringBoot` 启动方法中重写 `SaStrategy.instance.createSession` 方法，使我们自定义的 `MySaSession` 生效\n```java\n@SpringBootApplication\npublic class SpringApplication {\n\n    public static void main(String[] args) {\n\n\t// 重写 SaStrategy.instance.createSession 方法\n        SaStrategy.instance.createSession = (sessionId) -> {\n            return new MySaSession(sessionId);\n        };\n\n        SpringApplication.run(SpringApplication.class, args);\n    }\n}\n```\n\n3. 实现 SaTokenDao 接口\n```java\n//定义一个类用于保存SaSession\n@Document\npublic class SaTokenWrap {\n    private String id;\n    private String value;\n    private Object object;\n    // 这里利用MongoDB的TTL索引，当过期时MongoDB会自动删除过期的数据，同时如果timeout如果为null那么视为永不删除\n    @Indexed(expireAfterSeconds = 1, background = true)\n    private Date timeout;\n\n\n    public boolean live() {\n        return getTimeout() == null || getTimeout().after(new Date());\n    }\n}\n```\n```java\n// SaTokenDao 实现\n@Component\npublic class SaTokenDaoMongo implements SaTokenDao {\n\n\n    private final MongoTemplate mongoTemplate;\n\n    public SaTokenDaoMongo(MongoTemplate mongoTemplate) {\n        this.mongoTemplate = mongoTemplate;\n    }\n\n\n    Optional<SaTokenWrap> getByKey(String key) {\n        SaTokenWrap tokenWrap = mongoTemplate.findById(key, SaTokenWrap.class);\n\n        return Optional.ofNullable(tokenWrap).filter(SaTokenWrap::live);\n    }\n\n    Date timeoutToDate(long timeout) {\n\n        return new Date(timeout * 1000 + System.currentTimeMillis());\n    }\n\n    void upsertByPath(String key, String path, Object value, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        Update update = Update.update(path, value);\n\n        if (timeout != SaTokenDao.NEVER_EXPIRE) {\n            update.set(\"timeout\", timeoutToDate(timeout));\n        } else {\n            update.unset(\"timeout\");\n        }\n\n        mongoTemplate.upsert(\n                Query.query(Criteria.where(\"id\").is(key)),\n                update,\n                SaTokenWrap.class\n        );\n    }\n\n    void updateByPath(String key, String path, Object value) {\n        mongoTemplate.updateFirst(\n                Query.query(Criteria.where(\"id\").is(key).and(\"timeout\").gte(new Date())),\n                Update.update(path, value),\n                SaTokenWrap.class\n        );\n    }\n\n    // ------------------------ String 读写操作\n\n    @Override\n    public String get(String key) {\n\n        return getByKey(key).map(SaTokenWrap::getValue).orElse(null);\n    }\n\n    @Override\n    public void set(String key, String value, long timeout) {\n        upsertByPath(key, \"value\", value, timeout);\n    }\n\n    @Override\n    public void update(String key, String value) {\n        updateByPath(key, \"value\", value);\n    }\n\n    @Override\n    public void delete(String key) {\n        mongoTemplate.remove(Query.query(Criteria.where(\"id\").is(key)));\n    }\n\n    @Override\n    public long getTimeout(String key) {\n\n        SaTokenWrap tokenWrap = mongoTemplate.findById(key, SaTokenWrap.class);\n\n        if (tokenWrap == null) {\n            return SaTokenDao.NOT_VALUE_EXPIRE;\n        }\n\n        if (tokenWrap.getTimeout() == null) {\n            return SaTokenDao.NEVER_EXPIRE;\n        }\n\n        long expire = tokenWrap.getTimeout().getTime();\n        long timeout = (expire - System.currentTimeMillis()) / 1000;\n\n        // 小于零时，视为不存在\n        if (timeout < 0) {\n            mongoTemplate.remove(Query.query(Criteria.where(\"id\").is(key)));\n            return SaTokenDao.NOT_VALUE_EXPIRE;\n        }\n        return timeout;\n    }\n\n    @Override\n    public void updateTimeout(String key, long timeout) {\n\n        Update update = new Update();\n\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            update.unset(\"timeout\");\n        } else {\n            update.set(\"timeout\", timeoutToDate(timeout));\n        }\n\n        mongoTemplate.upsert(\n                Query.query(Criteria.where(\"id\").is(key)),\n                update,\n                SaTokenWrap.class\n        );\n    }\n\n\n    // ------------------------ Object 读写操作\n\n    @Override\n    public Object getObject(String key) {\n        return getByKey(key).map(SaTokenWrap::getObject).orElse(null);\n    }\n\n    @Override\n    public void setObject(String key, Object object, long timeout) {\n        upsertByPath(key, \"object\", object, timeout);\n    }\n\n    @Override\n    public void updateObject(String key, Object object) {\n        updateByPath(key, \"object\", object);\n    }\n\n    @Override\n    public void deleteObject(String key) {\n        delete(key);\n    }\n\n    @Override\n    public long getObjectTimeout(String key) {\n        return getTimeout(key);\n    }\n\n    @Override\n    public void updateObjectTimeout(String key, long timeout) {\n        updateTimeout(key, timeout);\n    }\n\n\n    // ------------------------ Session 读写操作\n    // 使用接口默认实现\n\n\n    // --------- 会话管理\n\n    @Override\n    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\n        List<SaTokenWrap> wrapList = mongoTemplate.find(\n                Query.query(Criteria.where(\"id\").regex(prefix + \"*\" + keyword + \"*\").and(\"timeout\").gte(new Date())),\n                SaTokenWrap.class\n        );\n\n        List<String> list = wrapList.stream().map(SaTokenWrap::getValue).filter(StringUtils::hasText).collect(Collectors.toList());\n\n        return SaFoxUtil.searchList(list, start, size, sortType);\n    }\n\n\n}\n```\n\n"
  },
  {
    "path": "sa-token-doc/up/login-parameter.md",
    "content": "# 登录参数\n\n### 1、登录参数\n\n在之前的章节我们提到，通过 `StpUtil.login(xxx)` 可以完成指定账号登录，同时你可以指定第二个参数来扩展登录信息，比如：\n\n``` java\n// 指定`账号id`和`设备类型`进行登录\nStpUtil.login(10001, \"PC\");    \n\n// 设置登录账号 id 为 10001，并指定是否为 “记住我” 模式\nStpUtil.login(10001, false);\n```\n\n除了以上内容，第二个参数你还可以指定一个 `SaLoginParameter` 对象，来详细控制登录的多个细节，例如：\n\n``` java\nStpUtil.login(10001, new SaLoginParameter()\n\t\t.setDeviceType(\"PC\")             // 此次登录的客户端设备类型, 一般用于完成 [同端互斥登录] 功能\n\t\t.setDeviceId(\"xxxxxxxxx\")        // 此次登录的客户端设备ID, 登录成功后该设备将标记为可信任设备\n\t\t.setIsLastingCookie(true)        // 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t\t.setTimeout(60 * 60 * 24 * 7)    // 指定此次登录 token 的有效期, 单位:秒，-1=永久有效\n\t\t.setActiveTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的最低活跃频率, 单位:秒，-1=不进行活跃检查\n\t\t.setIsConcurrent(true)           // 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\t\t.setIsShare(false)                // 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token）\n\t\t.setMaxLoginCount(12)            // 同一账号最大登录数量，-1代表不限 （只有在 isConcurrent=true, isShare=false 时此配置项才有意义）\n\t\t.setMaxTryTimes(12)              // 在每次创建 token 时的最高循环次数，用于保证 token 唯一性（-1=不循环尝试，直接使用）\n\t\t.setExtra(\"key\", \"value\")        // 记录在 Token 上的扩展参数（只在 jwt 模式下生效）\n\t\t.setToken(\"xxxx-xxxx-xxxx-xxxx\") // 预定此次登录的生成的Token \n\t\t.setIsWriteHeader(false)         // 是否在登录后将 Token 写入到响应头\n\t\t.setTerminalExtra(\"key\", \"value\")// 本次登录挂载到 SaTerminalInfo 的自定义扩展数据\n\t\t.setReplacedRange(SaReplacedRange.CURR_DEVICE_TYPE) // 顶人下线的范围: CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端\n\t\t.setOverflowLogoutMode(SaLogoutMode.LOGOUT)         // 溢出 maxLoginCount 的客户端，将以何种方式注销下线: LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线\n\t\t.setRightNowCreateTokenSession(true)                // 是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\n\t\t.setupCookieConfig(cookie->{     // 设置 Cookie 配置项 \n\t\t\tcookie.setDomain(\"sa-token.cc\");  // 设置：作用域\n\t\t\tcookie.setPath(\"/shop\");          // 设置：路径 （一般只有当你在一个域名下部署多个项目时才会用到此值。）\n\t\t\tcookie.setSecure(true);           // 设置：是否只在 https 协议下有效\n\t\t\tcookie.setHttpOnly(true);         // 设置：是否禁止 js 操作 Cookie \n\t\t\tcookie.setSameSite(\"Lax\");        // 设置：第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\n\t\t\tcookie.addExtraAttr(\"aa\", \"bb\");  // 设置：额外扩展属性\n\t\t}\n);\n```\n\n以上大部分参数在未指定时将使用全局配置作为默认值。\n\n\n\n### 2、注销参数\n\n同样的，在调用注销时，也可以指定一些参数决定注销的细节行为：\n\n``` java\n// 当前客户端注销 \nStpUtil.logout(new SaLogoutParameter()\n\t\t// 注销范围： TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话\n\t\t// 此参数只在调用 StpUtil.logout() 时有效\n\t\t.setRange(SaLogoutRange.TOKEN)   \n);\n\n// 指定 token 注销\nStpUtil.logoutByTokenValue(\"xxxxxxxxxxxxxxxxxxxxxxx\", new SaLogoutParameter()\n\t\t// 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)（默认 false）\n\t\t// 此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue(\"token\") 时有效\n\t\t.setIsKeepFreezeOps(false)  \n\t\t// 是否保留此 token 的 Token-Session 对象（默认 false）\n\t\t.setIsKeepTokenSession(true)  \n);\n\n// 指定 loginId 注销\nStpUtil.logout(10001, new SaLogoutParameter()\n\t\t.setDeviceType(\"PC\")  // 设置注销的设备类型 (如果不指定，则默认注销所有客户端)\n\t\t.setIsKeepTokenSession(true)  // 是否保留对应 token 的 Token-Session 对象（默认 false）\n\t\t.setMode(SaLogoutMode.REPLACED)  // 设置注销模式：LOGOUT=注销登录、KICKOUT=踢人下线，REPLACED=顶人下线（默认LOGOUT）\n);\n```\n\n以上大部分参数在未指定时将使用全局配置作为默认值。\n\n\n### 3、遍历登录终端详细操作\n\n如果你的 登录策略 或 注销策略 非常复杂，凭借上述参数无法组合出你的业务场景，你可以手动遍历一个账号的已登录终端信息列表，手动决定某个设备是否下线，例如：\n\n``` java\n// 测试 \n@RequestMapping(\"logout\")\npublic SaResult logout() {\n\t\n\t// 遍历账号 10001 已登录终端列表，进行详细操作\n\tStpUtil.forEachTerminalList(10001, (session, ter) -> {\n\t\t// 根据登录顺序，奇数的保留，偶数的下线\n\t\tif(ter.getIndex() % 2 == 0) {\n\t\t\tStpUtil.removeTerminalByLogout(session, ter);   // 注销下线方式 移除这个登录客户端\n\t\t\t// StpUtil.removeTerminalByKickout(session, ter);  // 踢人下线方式 移除这个登录客户端\n\t\t\t// StpUtil.removeTerminalByReplaced(session, ter);  // 顶人下线方式 移除这个登录客户端\n\t\t}\n\t});\n\t\n\treturn SaResult.ok();\n}\n```\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/up/many-account.md",
    "content": "# 多账号认证\n--- \n\n### 1、需求场景\n有的时候，我们会在一个项目中设计两套账号体系，比如一个电商系统的 `user表` 和 `admin表`，\n在这种场景下，如果两套账号我们都使用 `StpUtil` 类的API进行登录鉴权，那么势必会发生逻辑冲突。\n\n在Sa-Token中，这个问题的模型叫做：多账号体系认证。\n\n要解决这个问题，我们必须有一个合理的机制将这两套账号的授权给区分开，让它们互不干扰才行。\n\n\n### 2、演进思路\n假如说我们的 user表 和 admin表 都有一个 id=10001 的账号，它们对应的登录代码：`StpUtil.login(10001)` 是一样的，\n那么问题来了：在`StpUtil.getLoginId()`获取到的账号id如何区分它是User用户，还是Admin用户？\n\n你可能会想到为他们加一个固定前缀，比如`StpUtil.login(\"User_\" + 10001)`、`StpUtil.login(\"Admin_\" + 10001)`，这样确实是可以解决问题的，\n但是同样的：你需要在`StpUtil.getLoginId()`时再裁剪掉相应的前缀才能获取真正的账号id，这样一增一减就让我们的代码变得无比啰嗦。\n\n那么，有没有从框架层面支持的，更优雅的解决方案呢？\n\n\n### 3、解决方案\n\n前面几篇介绍的api调用，都是经过 StpUtil 类的各种静态方法进行授权认证，\n而如果我们深入它的源码，[点此阅览](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpUtil.java) <br/>\n就会发现，此类并没有任何代码逻辑，唯一做的事就是对成员变量`stpLogic`的各个API包装一下进行转发。\n\n这样做有两个好处: \n- StpLogic 类的所有函数都可以被重写，按需扩展。\n- 在构造方法时随意传入一个不同的 `loginType`，就可以再造一套账号登录体系。\n\n\n### 4、操作示例\n\n比如说，对于原生`StpUtil`类，我们只做`admin账号`权限认证，而对于`user账号`，我们则：\n1. 新建一个新的权限认证类，比如： `StpUserUtil.java`。\n2. 将`StpUtil.java`类的全部代码复制粘贴到 `StpUserUtil.java`里。\n3. 更改一下其 `LoginType`， 比如：\n\n``` java\npublic class StpUserUtil {\n\t\n\t/**\n\t * 账号体系标识 \n\t */\n\tpublic static final String TYPE = \"user\";\t// 将 LoginType 从`login`改为`user` \n\n\t// 其它代码 ... \n\n}\n```\n\n成品样例参考：[码云 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)\n\n4、接下来就可以像调用`StpUtil.java`一样调用 `StpUserUtil.java`了，这两套账号认证的逻辑是完全隔离的。例如：\n\n``` java\n// 凡是在 StpUtil 上有的方法，都可以在 StpUserUtil 上调用 \nStpUserUtil.login(10001);    // 在当前会话以10001账号进行登录 \nStpUserUtil.checkLogin();    // 校验当前账号是否以 User 身份进行登录 \nStpUserUtil.getSession();    // 获取当前 User 账号的 Access-Session 对象 \nStpUserUtil.checkPermission('xx');    // 校验当前登录的 user 账号是否具有 xx 权限 \n// ...\n```\n\n\n### 5、Kit模式 \n如果你觉得 “复制代码” 的方式繁琐不够优雅，这里还有另一种方案：建立一个 `StpKit.java` 门面类，声明所有的 `StpLogic` 引用：\n``` java\n/**\n * StpLogic 门面类，管理项目中所有的 StpLogic 账号体系\n */\npublic class StpKit {\n\n    /**\n     * 默认原生会话对象\n     */\n    public static final StpLogic DEFAULT = StpUtil.stpLogic;\n\n    /**\n     * Admin 会话对象，管理 Admin 表所有账号的登录、权限认证\n     */\n    public static final StpLogic ADMIN = new StpLogic(\"admin\");\n\n    /**\n     * User 会话对象，管理 User 表所有账号的登录、权限认证\n     */\n    public static final StpLogic USER = new StpLogic(\"user\");\n\n    /**\n     * XX 会话对象，（项目中有多少套账号表，就声明几个 StpLogic 会话对象）\n     */\n    public static final StpLogic XXX = new StpLogic(\"xx\");\n\n}\n```\n\n在需要登录、权限认证的地方：\n``` java\n// 在当前会话进行 Admin 账号登录\nStpKit.ADMIN.login(10001);\n\n// 在当前会话进行 User 账号登录\nStpKit.USER.login(10001);\n\n// 检测当前会话是否以 Admin 账号登录，并具有 article:add 权限\nStpKit.ADMIN.checkPermission(\"article:add\");\n\n// 检测当前会话是否以 User 账号登录，并通过了二级认证\nStpKit.USER.checkSafe();\n\n// 获取当前 User 会话的 Session 对象，并进行写值操作 \nStpKit.USER.getSession().set(\"name\", \"zhang\");\n```\n\n\n### 6、在多账户模式下使用注解鉴权\n框架默认的注解鉴权 如`@SaCheckLogin` 只针对原生`StpUtil`进行鉴权。\n\n例如，我们在一个方法上加上`@SaCheckLogin`注解，这个注解只会放行通过`StpUtil.login(id)`进行登录的会话，\n而对于通过`StpUserUtil.login(id)`进行登录的会话，则始终不会通过校验。\n\n那么如何告诉`@SaCheckLogin`要鉴别的是哪套账号的登录会话呢？很简单，你只需要指定一下注解的type属性即可：\n\n``` java\n// 通过type属性指定此注解校验的是我们自定义的`StpUserUtil`，而不是原生`StpUtil`\n@SaCheckLogin(type = StpUserUtil.TYPE)\n@RequestMapping(\"info\")\npublic String info() {\n    return \"查询用户信息\";\n}\n```\n\n注：`@SaCheckRole(\"xxx\")`、`@SaCheckPermission(\"xxx\")`同理，亦可根据type属性指定其校验的账号体系，此属性默认为`\"\"`，代表使用原生`StpUtil`账号体系。\n\n\n### 7、使用注解合并简化代码\n交流群里有同学反应，虽然可以根据 `@SaCheckLogin(type = \"user\")` 指定账号类型，但几十上百个注解都加上这个的话，还是有些繁琐，代码也不够优雅，有么有更简单的解决方案？\n\n我们期待一种`[注解继承/合并]`的能力，即：自定义一个注解，标注上`@SaCheckLogin(type = \"user\")`，\n然后在方法上标注这个自定义注解，效果等同于标注`@SaCheckLogin(type = \"user\")`。\n\n很遗憾，JDK默认的注解处理器并没有提供这种`[注解继承/合并]`的能力，不过好在我们可以利用 Spring 的注解处理器，达到同样的目的。\n\n1. 重写Sa-Token默认的注解处理器：\n\n``` java\n@Configuration\npublic class SaTokenConfigure {\n    @PostConstruct\n    public void rewriteSaStrategy() {\n    \t// 重写Sa-Token的注解处理器，增加注解合并功能 \n\t\tSaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {\n\t\t\treturn AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); \n\t\t};\n    }\n}\n```\n\n2. 自定义一个注解：\n\n``` java\n/**\n * 登录认证(User版)：只有登录之后才能进入该方法 \n * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上） \n */\n@SaCheckLogin(type = \"user\")\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE})\npublic @interface SaUserCheckLogin {\n\t\n}\n```\n\n3. 接下来就可以使用我们的自定义注解了：\n\n``` java\n// 使用 @SaUserCheckLogin 的效果等同于使用：@SaCheckLogin(type = \"user\")\n@SaUserCheckLogin\n@RequestMapping(\"info\")\npublic String info() {\n    return \"查询用户信息\";\n}\n```\n\n注：其它注解 `@SaCheckRole(\"xxx\")`、`@SaCheckPermission(\"xxx\")`同理， 完整示例参考 Gitee 代码：\n[注解合并](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/merge_annotation)。\n\n> [!TIP| label:自定义注解方案] \n> 除了注解合并方案，这里还有一份自定义注解方案，参考：[自定义注解](/fun/custom-annotations)\n\n\n\n### 8、同端多登陆 \n假设我们不仅需要在后台同时集成两套账号，我们还需要在一个客户端同时登陆两套账号（业务场景举例：一个APP中可以同时登陆商家账号和用户账号）。\n\n如果我们不做任何特殊处理的话，在客户端会发生`token覆盖`，新登录的 token 会覆盖掉旧登录的 token 从而导致旧登录失效。\n\n具体表现大致为：在一个浏览器登录商家账号后，再登录用户账号，然后商家账号的登录态就会自动失效。\n\n那么如何解决这个问题？很简单，我们只要更改一下 `StpUserUtil` 的 `TokenName` 即可，参考示例如下：\n\n``` java\npublic class StpUserUtil {\n\t\n\t// 使用匿名子类 重写`stpLogic对象`的一些方法 \n\tpublic static StpLogic stpLogic = new StpLogic(\"user\") {\n\t\t// 重写 StpLogic 类下的 `splicingKeyTokenName` 函数，返回一个与 `StpUtil` 不同的token名称, 防止冲突 \n\t\t@Override\n\t\tpublic String splicingKeyTokenName() {\n\t\t\treturn super.splicingKeyTokenName() + \"-user\";\n\t\t}\n\t\t// 同理你可以按需重写一些其它方法 ... \n\t}; \n\t\n\t// ... \n\t\n}\n```\n\n再次调用 `StpUserUtil.login(10001)` 进行登录授权时，token的名称将不再是 `satoken`，而是我们重写后的 `satoken-user`，这样就不会再客户端发生 token 的相互覆盖了。\n\n\n### 9、不同体系不同 SaTokenConfig 配置\n如果自定义的 StpUserUtil 需要使用不同 SaTokenConfig 对象, 也很简单，参考示例如下：\n\n``` java\n@Configuration\npublic class SaTokenConfigure {\n\t\n\t@PostConstruct\n\tpublic void setSaTokenConfig() {\n\t\t// 设定 StpUtil 使用的 SaTokenConfig 配置参数对象\n\t\tSaTokenConfig config1 = new SaTokenConfig();\n\t\tconfig1.setTokenName(\"satoken1\");\n\t\tconfig1.setTimeout(1000);\n\t\tconfig1.setTokenStyle(\"random-64\");\n\t\t// 更多设置 ... \n\t\tStpUtil.stpLogic.setConfig(config1);\n\n\t\t// 设定 StpUserUtil 使用的 SaTokenConfig 配置参数对象\n\t\tSaTokenConfig config2 = new SaTokenConfig();\n\t\tconfig2.setTokenName(\"satoken2\");\n\t\tconfig2.setTimeout(2000);\n\t\tconfig2.setTokenStyle(\"tik\");\n\t\t// 更多设置 ... \n\t\tStpUserUtil.stpLogic.setConfig(config2);\n\t}\n\n}\n\n```\n\n\n### 10、多账号体系混合鉴权\nQQ群中经常有小伙伴提问：在多账号体系下，怎么在 SaInterceptor 拦截器中给一个接口登录鉴权？\n\n其实这个问题，主要是靠你的业务需求来决定，以后台 Admin 账号和前台 User 账号为例：\n\n``` java\n// 注册 Sa-Token 拦截器\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\t\n\t\t// 如果这个接口，要求客户端登录了后台 Admin 账号才能访问：\n\t\tSaRouter.match(\"/art/getInfo\").check(r -> StpUtil.checkLogin());\n\n\t\t// 如果这个接口，要求客户端登录了前台 User 账号才能访问：\n\t\tSaRouter.match(\"/art/getInfo\").check(r -> StpUserUtil.checkLogin());\n\t\t\n\t\t// 如果这个接口，要求客户端同时登录 Admin 和 User 账号，才能访问：\n\t\tSaRouter.match(\"/art/getInfo\").check(r -> {\n\t\t\tStpUtil.checkLogin();\n\t\t\tStpUserUtil.checkLogin();\n\t\t});\n\n\t\t// 如果这个接口，要求客户端登录 Admin 和 User 账号任意一个，就能访问：\n\t\tSaRouter.match(\"/art/getInfo\").check(r -> {\n\t\t\tif(StpUtil.isLogin() == false && StpUserUtil.isLogin() == false) {\n\t\t\t\tthrow new SaTokenException(\"请登录后再访问接口\");\n\t\t\t}\n\t\t});\n\t\t\n\t})).addPathPatterns(\"/**\");\n}\n```\n\n\n### 11、在一个接口里获取是哪个体系的账号正在登录\n\n可以分别用两个体系的 isLogin() 方法去判断，哪个返回 true 就代表正在登录哪个体系\n\n``` java\n@RequestMapping(\"test\")\npublic SaResult test2() {\n\t\n\tString loginType = \"\";\n\t\n\tif(StpUtil.isLogin()) {\n\t\tloginType = StpUtil.getLoginType();\n\t}\n\tif(StpUserUtil.isLogin()) {\n\t\tloginType = StpUserUtil.getLoginType();\n\t}\n\t\n\tSystem.out.println(\"当前登录的 loginType：\" + loginType);\n\n\treturn SaResult.ok();\n}\n```\n\n请注意此处可能出现的两种边际情况：\n- 两个 if 均返回 false：代表客户端在两个账号体系都没有登录。\n- 两个 if 均返回 true：代表客户端在两个账号体系都登录了。\n\n\n### 12、注意点：运行时不可更改 LoginType\n在 Q群 解决问题时，发现有些同学会写出类似下列形式的代码：\n\n``` java\nStpUtil.login(10001);\nStpUtil.getStpLogic().setLoginType(\"user\");\nStpUtil.getSession().set(\"name\", \"zhangsan\");\n```\n\n这是一种错误写法：LoginType 不可在运行时更改，只能在项目启动时指定。一旦项目启动成功后再修改 LoginType ，就会造成线程安全问题和严重的逻辑问题。\n\n\n\n\n\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 多账号体系认证 —— [ StpUserUtil.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/mock-person.md",
    "content": "# 模拟他人\n--- \n\n\n以上介绍的 API 都是操作当前账号，对当前账号进行各种鉴权操作，你可能会问，我能不能对别的账号进行一些操作？<br>\n比如：查看账号 10001 有无某个权限码、获取 账号 id=10002 的 `Account-Session`，等等...\n\nSa-Token 在 API 设计时充分考虑了这一点，暴露出多个api进行此类操作：\n\n\n## 有关操作其它账号的api\n\n``` java\n// 获取指定账号10001的`tokenValue`值 \nStpUtil.getTokenValueByLoginId(10001);\n\n// 将账号10001的会话注销登录\nStpUtil.logout(10001);\n\n// 获取账号10001的Session对象, 如果session尚未创建, 则新建并返回\nStpUtil.getSessionByLoginId(10001);\n\n// 获取账号10001的Session对象, 如果session尚未创建, 则返回null \nStpUtil.getSessionByLoginId(10001, false);\n\n// 获取账号10001是否含有指定角色标识 \nStpUtil.hasRole(10001, \"super-admin\");\n\n// 获取账号10001是否含有指定权限码\nStpUtil.hasPermission(10001, \"user:add\");\n```\n\n\n\n## 临时身份切换\n\n有时候，我们需要直接将当前会话的身份切换为其它账号，比如：\n``` java\n// 将当前会话[身份临时切换]为其它账号（本次请求内有效）\nStpUtil.switchTo(10044);\n\n// 此时再调用此方法会返回 10044 (我们临时切换到的账号id)\nStpUtil.getLoginId();\n\n// 结束 [身份临时切换]\nStpUtil.endSwitch();\n```\n\n你还可以：直接在一个代码段里方法内，临时切换身份为指定loginId（此方式无需手动调用`StpUtil.endSwitch()`关闭身份切换）\n``` java\nSystem.out.println(\"------- [身份临时切换]调用开始...\");\nStpUtil.switchTo(10044, () -> {\n\tSystem.out.println(\"是否正在身份临时切换中: \" + StpUtil.isSwitch());  // 输出 true\n\tSystem.out.println(\"获取当前登录账号id: \" + StpUtil.getLoginId());   // 输出 10044\n});\nSystem.out.println(\"------- [身份临时切换]调用结束...\");\n```\n\n\n\n--- \n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SwitchToController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 身份切换 —— [ SwitchToController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/mutex-login.md",
    "content": "# 同端互斥登录\n\n如果你经常使用腾讯QQ，就会发现它的登录有如下特点：它可以手机电脑同时在线，但是不能在两个手机上同时登录一个账号。 <br/>\n同端互斥登录，指的就是：像腾讯QQ一样，在同一类型设备上只允许单地点登录，在不同类型设备上允许同时在线。\n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/up/g3--mutex-login.gif\">加载动态演示图</button>\n\n--- \n\n## 具体API\n\n在 Sa-Token 中如何做到同端互斥登录? <br/>\n首先在配置文件中，将 `isConcurrent` 配置为false，然后调用登录等相关接口时声明设备类型即可：\n\n\n#### 指定设备类型登录\n``` java\n// 指定`账号id`和`设备类型`进行登录\nStpUtil.login(10001, \"PC\");\t\n```\n调用此方法登录后，同设备的会被顶下线（不同设备不受影响），再次访问系统时会抛出 `NotLoginException` 异常，场景值=`-4`\n\n\n#### 指定设备类型强制注销\n``` java\n// 指定`账号id`和`设备类型`进行强制注销 \nStpUtil.logout(10001, \"PC\");\t\n```\n如果第二个参数填写null或不填，代表将这个账号id所有在线端强制注销，被踢出者再次访问系统时会抛出 `NotLoginException` 异常，场景值=`-2`\n\n\n#### 查询当前登录的设备类型\n``` java\n// 返回当前token的登录设备类型\nStpUtil.getLoginDevice();\t\n```\n\n\n#### Id 反查 Token\n``` java\n// 获取指定loginId指定设备类型端的tokenValue \nStpUtil.getTokenValueByLoginId(10001, \"APP\");\t\n```\n\n\n--- \n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 同端互斥登录  —— [ MutexLoginController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/not-cookie.md",
    "content": "# 前后端分离（无Cookie模式）\n--- \n\n### 何为无 Cookie 模式? \n\n无 Cookie 模式：特指不支持 Cookie 功能的终端，通俗来讲就是我们常说的 —— **前后端分离模式**。\n\n常规 Web 端鉴权方法，一般由 `Cookie模式` 完成，而 Cookie 有两个特性：\n1. 可由后端控制写入。\n2. 每次请求自动提交。\n\n这就使得我们在前端代码中，无需任何特殊操作，就能完成鉴权的全部流程（因为整个流程都是后端控制完成的）<br/>\n而在app、小程序等前后端分离场景中，一般是没有 Cookie 这一功能的，此时大多数人都会一脸懵逼，咋进行鉴权啊？\n\n见招拆招，其实答案很简单：\n- 不能后端控制写入了，就前端自己写入。（难点在**后端如何将 Token 传递到前端**）\n- 每次请求不能自动提交了，那就手动提交。（难点在**前端如何将 Token 传递到后端**，同时**后端将其读取出来**）\n\n\n\n### 1、后端将 token 返回到前端\n\n1. 首先调用 `StpUtil.login(id)` 进行登录。\n2. 调用 `StpUtil.getTokenInfo()` 返回当前会话的 token 详细参数。\n\t- 此方法返回一个对象，其有两个关键属性：`tokenName`和`tokenValue`（token 的名称和 token 的值）。\n\t- 将此对象传递到前台，让前端人员将这两个值保存到本地。\n\n代码示例：\n``` java\n// 登录接口\n@RequestMapping(\"doLogin\")\npublic SaResult doLogin() {\n\t// 第1步，先登录上 \n\tStpUtil.login(10001);\n\t// 第2步，获取 Token  相关参数 \n\tSaTokenInfo tokenInfo = StpUtil.getTokenInfo();\n\t// 第3步，返回给前端 \n\treturn SaResult.data(tokenInfo);\n}\n```\n\n\n### 2、前端将 token 提交到后端\n1. 无论是app还是小程序，其传递方式都大同小异。\n2. 那就是，将 token 塞到请求`header`里 ，格式为：`{tokenName: tokenValue}`。\n3. 以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例： \n\n**方式1，简单粗暴**\n\n``` js \n// 1、首先在登录时，将 tokenValue 存储在本地，例如：\nuni.setStorageSync('tokenValue', tokenValue);\n\n// 2、在发起ajax请求的地方，获取这个值，并塞到header里 \nuni.request({\n\turl: 'https://www.example.com/request', // 仅为示例，并非真实接口地址。\n\theader: {\n\t\t\"content-type\": \"application/x-www-form-urlencoded\",\n\t\t\"satoken\": uni.getStorageSync('tokenValue')\t\t// ⚠️ 关键代码, 注意参数名字是 satoken \n\t},\n\tsuccess: (res) => {\n\t\tconsole.log(res.data);\t\n\t}\n});\n```\n\n**方式2，更加灵活**\n\t\n``` js\n// 1、首先在登录时，将tokenName和tokenValue一起存储在本地，例如：\nuni.setStorageSync('tokenName', tokenName); \nuni.setStorageSync('tokenValue', tokenValue); \n\n// 2、在发起ajax的地方，获取这两个值, 并组织到head里 \nvar tokenName = uni.getStorageSync('tokenName');\t// 从本地缓存读取tokenName值\nvar tokenValue = uni.getStorageSync('tokenValue');\t// 从本地缓存读取tokenValue值\nvar header = {\n\t\"content-type\": \"application/x-www-form-urlencoded\"\n};\nif (tokenName != undefined && tokenName != '') {\n\theader[tokenName] = tokenValue;\n}\n\n// 3、后续在发起请求时将 header 对象塞到请求头部 \nuni.request({\n\turl: 'https://www.example.com/request', // 仅为示例，并非真实接口地址。\n\theader: header,\n\tsuccess: (res) => {\n\t\tconsole.log(res.data);\t\n\t}\n});\n```\n\n4. 只要按照如此方法将`token`值传递到后端，Sa-Token 就能像传统PC端一样自动读取到 token 值，进行鉴权。\n5. 你可能会有疑问，难道我每个`ajax`都要写这么一坨？岂不是麻烦死了？\n\t- 你当然不能每个 ajax 都写这么一坨，因为这种重复性代码都是要封装在一个函数里统一调用的。\n\n\n### 其它解决方案？\n如果你对 Cookie 非常了解，那你就会明白，所谓 Cookie ，本质上就是一个特殊的`header`参数而已，\n而既然它只是一个 header 参数，我们就能手动模拟实现它，从而完成鉴权操作。\n\n这其实是对`无Cookie模式`的另一种解决方案，有兴趣的同学可以百度了解一下，在此暂不赘述。\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/NotCookieController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 前后端分离样例 —— [ NotCookieController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/password-secure.md",
    "content": "# 密码加密\n\n严格来讲，密码加密不属于 [权限认证] 的范畴，但是对于大多数系统来讲，密码加密又是安全认证不可或缺的部分，\n所以，应大家要求，`Sa-Token`在 v1.14 版本添加密码加密模块，该模块非常简单，仅仅封装了一些常见的加密算法。\n\n\n\n### 摘要加密\nmd5、sha1、sha256\n``` java\n// md5加密 \nSaSecureUtil.md5(\"123456\");\n\n// sha1加密 \nSaSecureUtil.sha1(\"123456\");\n\n// sha256加密 \nSaSecureUtil.sha256(\"123456\");\n```\n\n\n### 对称加密\nAES加密\n``` java\n// 定义秘钥和明文\nString key = \"123456\";\nString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n// 加密 \nString ciphertext = SaSecureUtil.aesEncrypt(key, text);\nSystem.out.println(\"AES加密后：\" + ciphertext);\n\n// 解密 \nString text2 = SaSecureUtil.aesDecrypt(key, ciphertext);\nSystem.out.println(\"AES解密后：\" + text2);\n```\n\n附：内部密钥生成策略，方便其他开发语言对接\n```java\n    private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException {\n        KeyGenerator kg = KeyGenerator.getInstance(\"AES\");\n        //获取SHA1PRNG伪随机数生成器\n        SecureRandom random = SecureRandom.getInstance(\"SHA1PRNG\");\n        //将实际密码作为伪随机数生成器的种子\n        random.setSeed(password.getBytes());\n        //利用伪随机数生成器生成128位的密钥，能确保解密时生成的密钥的一致性\n        kg.init(128, random);\n        SecretKey secretKey = kg.generateKey();\n        return new SecretKeySpec(secretKey.getEncoded(), \"AES\");\n    }\n```\n\n\n### 非对称加密\n~~RSA加密(已过时)~~\n``` java\n// 定义私钥和公钥 \nString 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==\";\nString publicKey = \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB\";\n\n// 文本\nString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n// 使用公钥加密\nString ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);\nSystem.out.println(\"公钥加密后：\" + ciphertext);\n\n// 使用私钥解密\nString text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);\nSystem.out.println(\"私钥解密后：\" + text2); \n```\n\n你可能会有疑问，私钥和公钥这么长的一大串，我怎么弄出来，手写吗？当然不是，调用以下方法生成即可\n``` java\n// 生成一对公钥和私钥，其中Map对象 (private=私钥, public=公钥)\nSystem.out.println(SaSecureUtil.rsaGenerateKeyPair());\n```\n\n\n### Base64编码与解码\n``` java\n// 文本\nString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n// 使用Base64编码\nString base64Text = SaBase64Util.encode(text);\nSystem.out.println(\"Base64编码后：\" + base64Text);\n\n// 使用Base64解码\nString text2 = SaBase64Util.decode(base64Text);\nSystem.out.println(\"Base64解码后：\" + text2); \n```\n\n\n### Base32编码与解码\n``` java\n// 文本\nString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n// 使用Base32编码\nString base32Text = SaBase32Util.encode(text);\nSystem.out.println(\"Base32编码后：\" + base32Text);\n\n// 使用Base32解码\nString text2 = SaBase32Util.decode(base32Text);\nSystem.out.println(\"Base32解码后：\" + text2); \n```\n\n\n### TOTP 验证器\n\n``` java\n// 1、生成密钥\nString secretKey = SaTotpUtil.generateSecretKey();\nSystem.out.println(\"TOTP 秘钥: \" + secretKey);\n\n// 2、生成扫码字符串\nString qeString = SaTotpUtil.generateGoogleSecretKey(\"zhangsan\", secretKey);\nSystem.out.println(\"扫码字符串: \" + qeString);\n\n// 3、计算当前 TOTP 码\nString code = SaTotpUtil.generateTOTP(secretKey);\nSystem.out.println(\"当前时间戳对应的 TOTP 码: \" + code);\n\n// 4、验证用户输入\nboolean isValid = SaTotpUtil.validateTOTP(secretKey, code, 1);\nSystem.out.println(\"验证结果: \" + isValid);\n```\n\n在线 TOTP 管理器推荐： [TOTP 密码生成管理 - 工具哇](https://toolwa.com/totp/)\n\n\n### BCrypt加密\n由它加密的文件可在所有支持的操作系统和处理器上进行转移\n\n它的口令必须是8至56个字符，并将在内部被转化为448位的密钥\n\n> 此类来自于https://github.com/jeremyh/jBCrypt/\n``` java\n// 使用方法\nString pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); \n\n// 使用checkpw方法检查被加密的字符串是否与原始字符串匹配：\nBCrypt.checkpw(candidate_password, stored_hash); \n\n// gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少，也决定了加密的复杂度:\nString strong_salt = BCrypt.gensalt(10);\nString stronger_salt = BCrypt.gensalt(12); \n```\n\n\n<br>\n\n如需更多加密算法，可参考 [Hutool-crypto: 加密](https://hutool.cn/docs/#/crypto/%E6%A6%82%E8%BF%B0)\n\n\n--- \n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SecureController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 密码加密 —— [ SecureController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/remember-me.md",
    "content": "# [记住我] 模式\n--- \n\n如图所示，一般网站的登录界面都会有一个 **`[记住我]`** 按钮，当你勾选它登录后，即使你关闭浏览器再次打开网站，也依然会处于登录状态，无须重复验证密码：\n\n<img src=\"/big-file/doc/up/login-view.png\" alt=\"../static/login-view.png\">\n\n那么在Sa-Token中，如何做到 [ 记住我 ] 功能呢？\n\n\n### 在 Sa-Token 中实现记住我功能\n\nSa-Token的登录授权，**默认就是`[记住我]`模式**，为了实现`[非记住我]`模式，你需要在登录时如下设置：\n\n``` java\n// 设置登录账号id为10001，第二个参数指定是否为[记住我]，当此值为false后，关闭浏览器后再次打开需要重新登录\nStpUtil.login(10001, false);\n```\n\n那么，Sa-Token实现`[记住我]`的具体原理是？\n\n\n### 实现原理\nCookie作为浏览器提供的默认会话跟踪机制，其生命周期有两种形式，分别是：\n- 临时Cookie：有效期为本次会话，只要关闭浏览器窗口，Cookie就会消失。\n- 持久Cookie：有效期为一个具体的时间，在时间未到期之前，即使用户关闭了浏览器Cookie也不会消失。\n\n利用Cookie的此特性，我们便可以轻松实现 [记住我] 模式：\n- 勾选 [记住我] 按钮时：调用`StpUtil.login(10001, true)`，在浏览器写入一个`持久Cookie`储存 Token，此时用户即使重启浏览器 Token 依然有效。\n- 不勾选 [记住我] 按钮时：调用`StpUtil.login(10001, false)`，在浏览器写入一个`临时Cookie`储存 Token，此时用户在重启浏览器后 Token 便会消失，导致会话失效。\n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/up/g3--remember-me.gif\">加载动态演示图</button>\n\n\n### 前后端分离模式下如何实现[记住我]?\n\n此时机智的你😏很快发现一个问题，Cookie虽好，却无法在前后端分离环境下使用，那是不是代表上述方案在APP、小程序等环境中无效？\n\n准确的讲，答案是肯定的，任何基于Cookie的认证方案在前后端分离环境下都会失效（原因在于这些客户端默认没有实现Cookie功能），不过好在，这些客户端一般都提供了替代方案，\n唯一遗憾的是，此场景中token的生命周期需要我们在前端手动控制：\n\n以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例，我们可以使用如下方式达到同样的效果：\n``` js\n// 使用本地存储保存token，达到 [持久Cookie] 的效果\nuni.setStorageSync(\"satoken\", \"xxxx-xxxx-xxxx-xxxx-xxx\");\n\n// 使用globalData保存token，达到 [临时Cookie] 的效果\ngetApp().globalData.satoken = \"xxxx-xxxx-xxxx-xxxx-xxx\";\n```\n\n如果你决定在PC浏览器环境下进行前后端分离模式开发，那么更加简单：\n``` js\n// 使用 localStorage 保存token，达到 [持久Cookie] 的效果\nlocalStorage.setItem(\"satoken\", \"xxxx-xxxx-xxxx-xxxx-xxx\");\n\n// 使用 sessionStorage 保存token，达到 [临时Cookie] 的效果\nsessionStorage.setItem(\"satoken\", \"xxxx-xxxx-xxxx-xxxx-xxx\");\n```\n\nRemember me, it's too easy!\n\n\n\n### 登录时指定 Token 有效期\n登录时不仅可以指定是否为`[记住我]`模式，还可以指定一个特定的时间作为 Token 有效时长，如下示例：\n``` java\n// 示例1：\n// 指定token有效期(单位: 秒)，如下所示token七天有效\nStpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));\n\n// ----------------------- 示例2：所有参数\n// `SaLoginParameter`为登录参数Model，其有诸多参数决定登录时的各种逻辑，例如：\nStpUtil.login(10001, new SaLoginParameter()\n\t\t\t.setDevice(\"PC\")\t\t\t\t// 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型\n\t\t\t.setIsLastingCookie(true)\t\t// 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\n\t\t\t.setTimeout(60 * 60 * 24 * 7)\t// 指定此次登录token的有效期, 单位:秒 （如未指定，自动取全局配置的 timeout 值）\n\t        .setToken(\"xxxx-xxxx-xxxx-xxxx\") // 预定此次登录的生成的Token \n            .setIsWriteHeader(false)         // 是否在登录后将 Token 写入到响应头\n\t\t\t);\n```\n\n\n\n\n--- \n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/RememberMeController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 记住我登录 —— [ RememberMeController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/safe-auth.md",
    "content": "# 二级认证\n\n在某些敏感操作下，我们需要对已登录的会话进行二次验证。\n\n比如代码托管平台的仓库删除操作，尽管我们已经登录了账号，当我们点击 **[删除]** 按钮时，还是需要再次输入一遍密码，这么做主要为了两点：\n\n1. 保证操作者是当前账号本人。\n2. 增加操作步骤，防止误删除重要数据。\n\n这就是我们本篇要讲的 —— 二级认证，即：在已登录会话的基础上，进行再次验证，提高会话的安全性。\n\n\n--- \n\n### 具体API\n\n在`Sa-Token`中进行二级认证非常简单，只需要使用以下API：\n\n``` java\n// 在当前会话 开启二级认证，时间为120秒\nStpUtil.openSafe(120); \n\n// 获取：当前会话是否处于二级认证时间内\nStpUtil.isSafe(); \n\n// 检查当前会话是否已通过二级认证，如未通过则抛出异常\nStpUtil.checkSafe(); \n\n// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)\nStpUtil.getSafeTime(); \n\n// 在当前会话 结束二级认证\nStpUtil.closeSafe(); \n```\n\n\n### 一个小示例\n\n一个完整的二级认证业务流程，应该大致如下：\n``` java\n// 删除仓库\n@RequestMapping(\"deleteProject\")\npublic SaResult deleteProject(String projectId) {\n\t// 第1步，先检查当前会话是否已完成二级认证 \n\tif(!StpUtil.isSafe()) {\n\t\treturn SaResult.error(\"仓库删除失败，请完成二级认证后再次访问接口\");\n\t}\n\n\t// 第2步，如果已完成二级认证，则开始执行业务逻辑\n\t// ... \n\n\t// 第3步，返回结果 \n\treturn SaResult.ok(\"仓库删除成功\"); \n}\n\n// 提供密码进行二级认证 \n@RequestMapping(\"openSafe\")\npublic SaResult openSafe(String password) {\n\t// 比对密码（此处只是举例，真实项目时可拿其它参数进行校验）\n\tif(\"123456\".equals(password)) {\n\t\t\n\t\t// 比对成功，为当前会话打开二级认证，有效期为120秒 \n\t\tStpUtil.openSafe(120);\n\t\treturn SaResult.ok(\"二级认证成功\");\n\t}\n\t\n\t// 如果密码校验失败，则二级认证也会失败\n\treturn SaResult.error(\"二级认证失败\"); \n}\n```\n\n> [!NOTE| label:调用步骤：] \n> 1. 前端调用 `deleteProject` 接口，尝试删除仓库。\n> 2. 后端校验会话尚未完成二级认证，返回： `仓库删除失败，请完成二级认证后再次访问接口`。\n> 3. 前端将信息提示给用户，用户输入密码，调用 `openSafe` 接口。\n> 4. 后端比对用户输入的密码，完成二级认证，有效期为：120秒。\n> 5. 前端在 120 秒内再次调用 `deleteProject` 接口，尝试删除仓库。\n> 6. 后端校验会话已完成二级认证，返回：`仓库删除成功`。\n\n\n### 指定业务标识进行二级认证\n\n如果项目有多条业务线都需要敏感操作验证，则 `StpUtil.openSafe()` 无法提供细粒度的认证操作，\n此时我们可以指定一个业务标识来分辨不同的业务线：\n\n``` java\n// 在当前会话 开启二级认证，业务标识为client，时间为600秒\nStpUtil.openSafe(\"client\", 600); \n\n// 获取：当前会话是否已完成指定业务的二级认证 \nStpUtil.isSafe(\"client\"); \n\n// 校验：当前会话是否已完成指定业务的二级认证 ，如未认证则抛出异常\nStpUtil.checkSafe(\"client\"); \n\n// 获取当前会话指定业务二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)\nStpUtil.getSafeTime(\"client\"); \n\n// 在当前会话 结束指定业务标识的二级认证\nStpUtil.closeSafe(\"client\"); \n```\n\n业务标识可以填写任意字符串，不同业务标识之间的认证互不影响，比如：\n``` java\n// 打开了业务标识为 client 的二级认证 \nStpUtil.openSafe(\"client\"); \n\n// 判断是否处于 shop 的二级认证，会返回 false \nStpUtil.isSafe(\"shop\");  // 返回 false \n\n// 也不会通过校验，会抛出异常 \nStpUtil.checkSafe(\"shop\"); \n```\n\n\n\n### 使用注解进行二级认证\n在一个方法上使用 `@SaCheckSafe` 注解，可以在代码进入此方法之前进行一次二级认证校验\n``` java\n// 二级认证：必须二级认证之后才能进入该方法 \n@SaCheckSafe      \n@RequestMapping(\"add\")\npublic String add() {\n    return \"用户增加\";\n}\n\n// 指定业务类型，进行二级认证校验\n@SaCheckSafe(\"art\")\n@RequestMapping(\"add2\")\npublic String add2() {\n    return \"文章增加\";\n}\n```\n\n详细使用方法可参考：[注解鉴权](/use/at-check)，此处不再赘述\n\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SafeAuthController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 二级认证 —— [ SafeAuthController.java ]\n</a>"
  },
  {
    "path": "sa-token-doc/up/search-session.md",
    "content": "# 会话查询\n\n--- \n\n### 1、单账号会话查询\n\n使用 `StpUtil.getTerminalListByLoginId( loginId )` 可获取指定账号已登录终端列表信息，例如：\n\n``` java\npublic static void main(String[] args) {\n\tSystem.out.println(\"账号 10001 登录设备信息：\");\n\tList<SaTerminalInfo> terminalList = StpUtil.getTerminalListByLoginId(10001);\n\tfor (SaTerminalInfo ter : terminalList) {\n\t\tSystem.out.println(\"登录index=\" + ter.getIndex() + \", 设备type=\" + ter.getDeviceType() + \", token=\" + ter.getTokenValue() + \", 登录time=\" + ter.getCreateTime());\n\t}\n}\n```\n\n控制台打印结果：\n\n``` txt\n账号 10001 登录设备信息：\n登录index=1, 设备type=PC, token=a8fbb46f-e043-459a-a875-0a2874911be8, 登录time=1742354951192\n登录index=2, 设备type=APP, token=882b6c9c-bdf9-4e8f-a42b-6e17d2fe0e34, 登录time=1742354960950\n登录index=3, 设备type=WEB, token=dacac78c-0983-4819-ab8b-07e7603597fc, 登录time=1742354962848\n```\n\n一个 `SaTerminalInfo` 对象代表一个终端信息，其有如下字段：\n\n``` java\nterminal.getIndex();   // 登录会话索引值 (该账号第几个登录的设备)\nterminal.getDeviceType();   // 所属设备类型，例如：PC、WEB、HD、MOBILE、APP\nterminal.getTokenValue();   // 此次登录的token值\nterminal.getCreateTime();   // 登录时间, 13位时间戳\nterminal.getDeviceId();   // 设备id, 设备唯一标识\nterminal.getExtra(\"key\");  // 此次登录的额外自定义参数 \n```\n\n`Extra` 自定义参数可以在登录时通过如下方式指定: \n``` java\nStpUtil.login(10001, new SaLoginParameter().setTerminalExtra(\"key\", \"value\"));\n```\n\n\n\n### 2、全部会话检索 \n\n``` java\n// 查询所有已登录的 Token\nStpUtil.searchTokenValue(String keyword, int start, int size, boolean sortType);\n\n// 查询所有 Account-Session 会话\nStpUtil.searchSessionId(String keyword, int start, int size, boolean sortType);\n\n// 查询所有 Token-Session 会话\nStpUtil.searchTokenSessionId(String keyword, int start, int size, boolean sortType);\n```\n\n\n#### 参数详解：\n- `keyword`: 查询关键字，只有包括这个字符串的 token 值才会被查询出来。\n- `start`: 数据开始处索引。\n- `size`: 要获取的数据条数 （值为-1代表一直获取到末尾）。 \n- `sortType`: 排序方式（true=正序：先登录的在前，false=反序：后登录的在前）。\n\n简单样例：\n``` java\n// 查询 value 包括 1000 的所有 token，结果集从第 0 条开始，返回 10 条\nList<String> tokenList = StpUtil.searchTokenValue(\"1000\", 0, 10, true);\t\nfor (String token : tokenList) {\n\tSystem.out.println(token);\n}\n```\n\n#### 深入：`StpUtil.searchTokenValue` 和 `StpUtil.searchSessionId` 的区别？\n\n- StpUtil.searchTokenValue 查询的是登录产生的所有 Token。 \n- StpUtil.searchSessionId 查询的是所有已登录账号会话id。 \n\n举个例子，项目配置如下：\n``` yml\nsa-token: \n\t# 允许同一账号在多个设备一起登录\n\tis-concurrent: true\n\t# 同一账号每次登录产生不同的token\n\tis-share: false\n```\n\n假设此时账号A在 电脑、手机、平板 依次登录（共3次登录），账号B在 电脑、手机 依次登录（共2次登录），那么：\n\n- `StpUtil.searchTokenValue` 将返回一共 5 个Token。\n- `StpUtil.searchSessionId` 将返回一共 2 个 SessionId。\n\n综上，若要遍历系统所有已登录的会话，代码将大致如下：\n``` java\n// 获取所有已登录的会话id\nList<String> sessionIdList = StpUtil.searchSessionId(\"\", 0, -1, false);\n\nfor (String sessionId : sessionIdList) {\n\t\n\t// 根据会话id，查询对应的 SaSession 对象，此处一个 SaSession 对象即代表一个登录的账号 \n\tSaSession session = StpUtil.getSessionBySessionId(sessionId);\n\t\n\t// 查询这个账号都在哪些设备登录了，依据上面的示例，账号A 的 SaTerminalInfo 数量是 3，账号B 的 SaTerminalInfo 数量是 2 \n\tList<SaTerminalInfo> terminalList = session.terminalListCopy();\n\tSystem.out.println(\"会话id：\" + sessionId + \"，共在 \" + terminalList.size() + \" 设备登录\");\n}\n```\n\n\n\n<br/>\n\n#### 注意事项：\n由于会话查询底层采用了遍历方式获取数据，当数据量过大时此操作将会比较耗时，有多耗时呢？这里提供一份参考数据：\n- 单机模式下：百万会话取出10条 Token 平均耗时 `0.255s`。\n- Redis模式下：百万会话取出10条 Token 平均耗时 `3.322s`。\n\n请根据业务实际水平合理调用API。\n\n\n> [!WARNING| label:注意] \n> 基于活跃 Token 的统计方式会比实际情况略有延迟，如果需要精确统计实时在线用户信息需要采用 WebSocket。\n\n\n--- \n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/SearchSessionController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 会话查询  —— [ SearchSessionController.java ]\n</a>\n\n\n"
  },
  {
    "path": "sa-token-doc/up/token-prefix.md",
    "content": "# Token 提交前缀\n\n### 需求场景\n\n在某些系统中，前端提交token时会在前面加个固定的前缀，例如：\n\n``` js\n{\n\t\"satoken\": \"Bearer xxxx-xxxx-xxxx-xxxx\"\n}\n```\n\n此时后端如果不做任何特殊处理，框架将会把`Bearer `视为token的一部分，无法正常读取token信息，导致鉴权失败。\n\n为此，我们需要在yml中添加如下配置：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\t# 指定 token 提交时的前缀\n\ttoken-prefix: Bearer\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# token前缀\nsa-token.token-prefix=Bearer\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n此时 Sa-Token 便可在读取 Token 时裁剪掉 `Bearer`，成功获取`xxxx-xxxx-xxxx-xxxx`。\n\n注：**Token前缀  与 Token值 之间必须有一个空格**\n\n\n### Cookie 模式自动填充前缀\n\n由于`Cookie`中无法存储空格字符，所以配置 Token 前缀后，Cookie 模式将会失效，无法成功提交带有前缀的 token。\n\n如果需要在这种场景下仍然使用 Cookie 模式验证 token，可以使用 `cookieAutoFillPrefix` 配置项打开 Cookie 模式自动填充前缀：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\nsa-token: \n\t# 指定 Cookie 模式下自动填充 token 提交前缀\n\tcookie-auto-fill-prefix: true\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 指定 Cookie 模式下自动填充 token 提交前缀\nsa-token.cookie-auto-fill-prefix=true\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n"
  },
  {
    "path": "sa-token-doc/up/token-style.md",
    "content": "# 自定义 Token 风格\n\n本篇介绍token生成的各种风格，以及自定义token生成策略。\n\n--- \n\n\n## 内置风格\n\nSa-Token 默认的 token 生成策略是 uuid 风格，其模样类似于：`623368f0-ae5e-4475-a53f-93e4225f16ae`。<br>\n如果你对这种风格不太感冒，还可以将 token 生成设置为其他风格。\n\n怎么设置呢？只需要在yml配置文件里设置 `sa-token.token-style=风格类型` 即可，其有多种取值： \n\n``` java\n// 1. token-style=uuid    —— uuid风格 (默认风格)\n\"623368f0-ae5e-4475-a53f-93e4225f16ae\"\n\n// 2. token-style=simple-uuid    —— 同上，uuid风格, 只不过去掉了中划线\n\"6fd4221395024b5f87edd34bc3258ee8\"\n\n// 3. token-style=random-32    —— 随机32位字符串\n\"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W\"\n\n// 4. token-style=random-64    —— 随机64位字符串\n\"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc\"\n\n// 5. token-style=random-128    —— 随机128位字符串\n\"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj\"\n\n// 6. token-style=tik    —— tik风格\n\"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__\"\n```\n\n\n## 自定义 Token 生成策略\n\n如果你觉着以上风格都不是你喜欢的类型，那么你还可以**自定义token生成策略**，来定制化token生成风格。 <br>\n\n怎么做呢？只需要重写 `SaStrategy` 策略类的 `createToken` 算法即可：\n\n\n#### 参考步骤如下：\n1、在`SaTokenConfigure`配置类中添加代码：\n``` java \n@Configuration\npublic class SaTokenConfigure {\n    /**\n     * 重写 Sa-Token 框架内部算法策略 \n     */\n    @PostConstruct\n    public void rewriteSaStrategy() {\n    \t// 重写 Token 生成策略 \n    \tSaStrategy.instance.createToken = (loginId, loginType) -> {\n    \t\treturn SaFoxUtil.getRandomString(60);\t// 随机60位长度字符串\n    \t};\n    }\n}\n```\n\n2、再次调用 `StpUtil.login(10001)`方法进行登录，观察其生成的token样式:\n``` java\ngfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH\n```\n\n> [!WARNING| label:更改了 token 生成策略但是不生效？] \n> 把 Redis 中的旧数据清除掉再试试\n\n"
  },
  {
    "path": "sa-token-doc/use/at-check.md",
    "content": "# 注解鉴权\n\n\n### 注解鉴权\n\n有同学表示：尽管使用代码鉴权非常方便，但是我仍希望把鉴权逻辑和业务逻辑分离开来，我可以使用注解鉴权吗？当然可以！<br>\n\n注解鉴权 —— 优雅的将鉴权与业务代码分离！\n\n- `@SaCheckLogin`: 登录校验 —— 只有登录之后才能进入该方法。\n- `@SaCheckRole(\"admin\")`: 角色校验 —— 必须具有指定角色标识才能进入该方法。\n- `@SaCheckPermission(\"user:add\")`: 权限校验 —— 必须具有指定权限才能进入该方法。\n- `@SaCheckSafe`: 二级认证校验 —— 必须二级认证之后才能进入该方法。\n- `@SaCheckHttpBasic`: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。\n- `@SaCheckHttpDigest`: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。\n- `@SaCheckDisable(\"comment\")`：账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。\n- `@SaCheckSign`：API 签名校验 —— 用于跨系统的 API 签名参数校验。\n- `@SaIgnore`：忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。\n\nSa-Token 使用全局拦截器完成注解鉴权功能，为了不为项目带来不必要的性能负担，拦截器默认处于关闭状态。\n因此，为了使用注解鉴权，<green>**你必须手动将 Sa-Token 的全局拦截器注册到你项目中**</green>。\n\n\n### 1、注册拦截器\n以 SpringBoot2 项目为例，新建配置类`SaTokenConfigure.java`\n\n``` java\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t// 注册 Sa-Token 拦截器，打开注解式鉴权功能 \n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器，打开注解式鉴权功能 \n\t\tregistry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\t\n\t}\n}\n```\n保证此类被`springboot`启动类扫描到即可\n\n<!-- !> 注意：如果在高版本 `SpringBoot (≥2.6.x)` 下注册拦截器失效，则需要额外添加 `@EnableWebMvc` 注解才可以使用。 -->\n\n\n### 2、使用注解鉴权\n然后我们就可以愉快的使用注解鉴权了：\n\n``` java \n// 登录校验：只有登录之后才能进入该方法 \n@SaCheckLogin\t\t\t\t\t\t\n@RequestMapping(\"info\")\npublic String info() {\n\treturn \"查询用户信息\";\n}\n\n// 角色校验：必须具有指定角色才能进入该方法 \n@SaCheckRole(\"super-admin\")\t\t\n@RequestMapping(\"add\")\npublic String add() {\n\treturn \"用户增加\";\n}\n\n// 权限校验：必须具有指定权限才能进入该方法 \n@SaCheckPermission(\"user-add\")\t\t\n@RequestMapping(\"add\")\npublic String add() {\n\treturn \"用户增加\";\n}\n\n// 二级认证校验：必须二级认证之后才能进入该方法 \n@SaCheckSafe()\t\t\n@RequestMapping(\"add\")\npublic String add() {\n\treturn \"用户增加\";\n}\n\n// Http Basic 校验：只有通过 Http Basic 认证后才能进入该方法 \n@SaCheckHttpBasic(account = \"sa:123456\")\n@RequestMapping(\"add\")\npublic String add() {\n\treturn \"用户增加\";\n}\n\n// Http Digest 校验：只有通过 Http Digest 认证后才能进入该方法 \n@SaCheckHttpDigest(value = \"sa:123456\")\n@RequestMapping(\"add\")\npublic String add() {\n\treturn \"用户增加\";\n}\n\n// 校验当前账号是否被封禁 comment 服务，如果已被封禁会抛出异常，无法进入方法 \n@SaCheckDisable(\"comment\")\t\t\t\t\n@RequestMapping(\"send\")\npublic String send() {\n\treturn \"查询用户信息\";\n}\n```\n\n注：以上注解都可以加在类上，代表为这个类所有方法进行鉴权\n\n\n### 3、设定校验模式\n`@SaCheckRole`与`@SaCheckPermission`注解可设置校验模式，例如：\n``` java\n// 注解式鉴权：只要具有其中一个权限即可通过校验 \n@RequestMapping(\"atJurOr\")\n@SaCheckPermission(value = {\"user-add\", \"user-all\", \"user-delete\"}, mode = SaMode.OR)\t\t\npublic SaResult atJurOr() {\n\treturn SaResult.data(\"用户信息\");\n}\n```\n\nmode有两种取值：\n- `SaMode.AND`，标注一组权限，会话必须全部具有才可通过校验。\n- `SaMode.OR`，标注一组权限，会话只要具有其一即可通过校验。\n\n\n### 4、角色权限双重 “or校验”\n假设有以下业务场景：一个接口在具有权限 `user.add` 或角色 `admin` 时可以调通。怎么写？\n\n``` java\n// 角色权限双重 “or校验”：具备指定权限或者指定角色即可通过校验\n@RequestMapping(\"userAdd\")\n@SaCheckPermission(value = \"user.add\", orRole = \"admin\")\t\t\npublic SaResult userAdd() {\n\treturn SaResult.data(\"用户信息\");\n}\n```\n\norRole 字段代表权限校验未通过时的次要选择，两者只要其一校验成功即可进入请求方法，其有三种写法：\n- 写法一：`orRole = \"admin\"`，代表需要拥有角色 admin 。\n- 写法二：`orRole = {\"admin\", \"manager\", \"staff\"}`，代表具有三个角色其一即可。\n- 写法三：`orRole = {\"admin, manager, staff\"}`，代表必须同时具有三个角色。\n\n\n### 5、忽略认证\n\n使用 `@SaIgnore` 可表示一个接口忽略认证：\n\n``` java\n@SaCheckLogin\n@RestController\npublic class TestController {\n\t\n\t// ... 其它方法 \n\t\n\t// 此接口加上了 @SaIgnore 可以游客访问 \n\t@SaIgnore\n\t@RequestMapping(\"getList\")\n\tpublic SaResult getList() {\n\t\t// ... \n\t\treturn SaResult.ok(); \n\t}\n}\n```\n\n如上代码表示：`TestController` 中的所有方法都需要登录后才可以访问，但是 `getList` 接口可以匿名游客访问。\n\n- @SaIgnore 修饰方法时代表这个方法可以被游客访问，修饰类时代表这个类中的所有接口都可以游客访问。\n- @SaIgnore 具有最高优先级，当 @SaIgnore 和其它鉴权注解一起出现时，其它鉴权注解都将被忽略。\n- @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权，在下面的 [路由拦截鉴权] 章节中我们会讲到。\n\n\n\n### 6、批量注解鉴权\n\n使用 `@SaCheckOr` 表示批量注解鉴权：\n\n``` java\n// 在 `@SaCheckOr` 中可以指定多个注解，只要当前会话满足其中一个注解即可通过验证，进入方法。\n@SaCheckOr(\n\t\tlogin = @SaCheckLogin,\n\t\trole = @SaCheckRole(\"admin\"),\n\t\tpermission = @SaCheckPermission(\"user.add\"),\n\t\tsafe = @SaCheckSafe(\"update-password\"),\n\t\thttpBasic = @SaCheckHttpBasic(account = \"sa:123456\"),\n\t\tdisable = @SaCheckDisable(\"submit-orders\")\n)\n@RequestMapping(\"test\")\npublic SaResult test() {\n\t// ... \n\treturn SaResult.ok(); \n}\n```\n\n每一项属性都可以写成数组形式，例如：\n\n``` java\n// 当前客户端只要有 [ login 账号登录] 或者 [user 账号登录] 其一，就可以通过验证进入方法。\n// \t\t注意：`type = \"login\"` 和 `type = \"user\"` 是多账号模式章节的扩展属性，此处你可以先略过这个知识点。\n@SaCheckOr(\n\tlogin = { @SaCheckLogin(type = \"login\"), @SaCheckLogin(type = \"user\") }\n)\n@RequestMapping(\"test\")\npublic SaResult test() {\n\t// ... \n\treturn SaResult.ok(); \n}\n```\n\n疑问：既然有了 `@SaCheckOr`，为什么没有与之对应的 `@SaCheckAnd` 呢？\n\n因为当你写多个注解时，其天然就是 `and` 校验关系，例如：\n \n``` java\n// 当你在一个方法上写多个注解鉴权时，其默认就是要满足所有注解规则后，才可以进入方法，只要有一个不满足，就会抛出异常\n@SaCheckLogin\n@SaCheckRole(\"admin\")\n@SaCheckPermission(\"user.add\")\n@RequestMapping(\"test\")\npublic SaResult test() {\n\t// ... \n\treturn SaResult.ok(); \n}\n```\n\n\n使用 append 字段追加抓取扩展包里的注解，例如：\n``` java\n// 测试：只有通过登录校验，或者提供了正确的 ApiKey，才可以进入方法\n@RequestMapping(\"/test\")\n@SaCheckOr(login = @SaCheckLogin, append = { SaCheckApiKey.class })\n@SaCheckApiKey\npublic SaResult test() {\n\t// ...\n\treturn SaResult.ok();\n}\n```\n\n\n### 7、扩展阅读\n\n- 在业务逻辑层使用鉴权注解：[AOP注解鉴权](/plugin/aop-at)\n\n- 制作自定义鉴权注解注入到框架：[自定义注解](/fun/custom-annotations)\n\n\n<!-- 疑问：我能否将注解写在其它架构层呢，比如业务逻辑层？\n\n使用拦截器模式，只能在`Controller层`进行注解鉴权，如需在任意层级使用注解鉴权，请参考：[AOP注解鉴权](/plugin/aop-at) -->\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/AtCheckController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 注解鉴权 —— [ AtCheckController.java ]\n</a>\n<a class=\"dt-btn\" href=\"https://www.wenjuan.ltd/s/ARJvIbA/\" target=\"_blank\">本章小练习：Sa-Token 基础 - 注解鉴权，章节测试</a>\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/use/config.md",
    "content": "# 框架配置\n\n你可以**零配置启动框架**，但同时你也可以通过一定的参数配置，定制性使用框架，`Sa-Token`支持多种方式配置框架信息\n\n--- \n\n### 1、配置方式\n\n##### 方式1、在 application.yml 配置\n\n<!---------------------------- tabs:start ---------------------------->\n\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\nsa-token: \n\t# token 名称（同时也是 cookie 名称）\n\ttoken-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n\ttimeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\tactive-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n\tis-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n\tis-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n\ttoken-style: uuid\n    # 是否输出操作日志 \n\tis-log: true\n```\n\n<!------------- tab:properties 风格  ------------->\n``` properties\n############## Sa-Token 配置 (文档: https://sa-token.cc) ##############\n\n# token 名称（同时也是 cookie 名称）\nsa-token.token-name=satoken\n# token 有效期（单位：秒） 默认30天，-1 代表永久有效\nsa-token.timeout=2592000\n# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\nsa-token.active-timeout=-1\n# 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\nsa-token.is-concurrent=true\n# 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\nsa-token.is-share=false\n# token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\nsa-token.token-style=uuid\n# 是否输出操作日志 \nsa-token.is-log=true\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n##### 方式2、通过代码配置\n\n<!------------------------------ tabs:start ------------------------------>\n<!------------- tab:模式 1 ------------->\n``` java \n/**\n * Sa-Token 配置类\n */\n@Configuration\npublic class SaTokenConfigure {\n\t// Sa-Token 参数配置，参考文档：https://sa-token.cc\n\t// 此配置会覆盖 application.yml 中的配置\n    @Bean\n    @Primary\n    public SaTokenConfig getSaTokenConfigPrimary() {\n\t\tSaTokenConfig config = new SaTokenConfig();\n\t\tconfig.setTokenName(\"satoken\");             // token 名称（同时也是 cookie 名称）\n\t\tconfig.setTimeout(30 * 24 * 60 * 60);       // token 有效期（单位：秒），默认30天，-1代表永不过期 \n\t\tconfig.setActiveTimeout(-1);              // token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\t\tconfig.setIsConcurrent(true);               // 是否允许同一账号多地同时登录（为 true 时允许一起登录，为 false 时新登录挤掉旧登录）\n\t\tconfig.setIsShare(false);                    // 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token，为 false 时每次登录新建一个 token）\n\t\tconfig.setTokenStyle(\"uuid\");               // token 风格\n\t\tconfig.setIsLog(false);                     // 是否输出操作日志 \n\t\treturn config;\n\t}\n}\n```\n<!------------- tab:模式 2 ------------->\n``` java\n/**\n * Sa-Token 配置类\n */\n@Configuration\npublic class SaTokenConfigure {\n\t// Sa-Token 参数配置，参考文档：https://sa-token.cc\n\t// 此配置会与 application.yml 中的配置合并 （代码配置优先）\n\t@Autowired\n\tpublic void configSaToken(SaTokenConfig config) {\n\t\tconfig.setTokenName(\"satoken\");             // token 名称（同时也是 cookie 名称）\n\t\tconfig.setTimeout(30 * 24 * 60 * 60);       // token 有效期（单位：秒），默认30天，-1代表永不过期 \n\t\tconfig.setActiveTimeout(-1);              // token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n\t\tconfig.setIsConcurrent(true);               // 是否允许同一账号多地同时登录（为 true 时允许一起登录，为 false 时新登录挤掉旧登录）\n\t\tconfig.setIsShare(false);                    // 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token，为 false 时每次登录新建一个 token）\n\t\tconfig.setTokenStyle(\"uuid\");               // token 风格\n\t\tconfig.setIsLog(false);                     // 是否输出操作日志 \n\t}\n}\n```\n<!---------------------------- tabs:end ------------------------------>\n\n两者的区别在于：\n- 模式 1 会覆盖 application.yml 中的配置。\n- 模式 2 会与 application.yml 中的配置合并（代码配置优先）。\n\n\n--- \n### 2、核心包所有可配置项\n\n#### 2.1、核心模块配置\n\n你不必立刻掌握整个表格，只需要在用到某个功能时再详细查阅它即可\n\n| 参数名称\t\t\t\t| 类型\t\t| 默认值\t\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| tokenName\t\t\t\t| String\t| satoken\t| Token 名称 （同时也是 Cookie 名称、数据持久化前缀）\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| timeout\t\t\t\t| long\t\t| 2592000\t| Token 有效期（单位：秒），默认30天，-1代表永不过期 [参考：token有效期详解](/fun/token-timeout)\t\t|\n| activeTimeout\t\t\t| long\t\t| -1\t\t| Token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结（例如可以设置为1800代表30分钟内无操作就冻结） \t[参考：token有效期详解](/fun/token-timeout)\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| dynamicActiveTimeout\t| Boolean\t| false\t\t| 是否启用动态 activeTimeout 功能，如不需要请设置为 false，节省缓存请求次数\t|\n| isConcurrent\t\t\t| Boolean\t| true\t\t| 是否允许同一账号并发登录 （为 true 时允许一起登录，为 false 时新登录挤掉旧登录）\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| isShare\t\t\t\t| Boolean\t| false\t\t| 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token，为 false 时每次登录新建一个 token，login 时提供了 Extra 数据后，即使配置了为 true 也不能复用旧 Token，必须创建新 Token） \t|\n| replacedLoginExitMode\t| SaReplacedLoginExitMode\t| OLD_DEVICE\t| 在 isConcurrent=false 时，决定新旧设备谁将放弃会话 (OLD_DEVICE=旧设备下线，新设备登录成功, NEW_DEVICE=新设备登录失败，旧设备维持在线)\t|\n| replacedRange\t| SaReplacedRange\t| CURR_DEVICE_TYPE\t\t| 在 isConcurrent=false 时，顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) |\n| maxLoginCount\t\t\t| int\t\t| 12\t\t| 同一账号最大登录数量，-1代表不限 （只有在 `isConcurrent=true`，`isShare=false` 时此配置才有效），[详解](/use/config?id=配置项详解：maxlogincount)\t|\n| overflowLogoutMode\t| SaLogoutMode\t| LOGOUT\t| 溢出 maxLoginCount 的客户端，将以何种方式注销下线   (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线)\t\t\t|\n| maxTryTimes\t\t\t| int\t\t| 12\t\t| 在每次创建 Token 时的最高循环次数，用于保证 Token 唯一性（-1=不循环重试，直接使用）\t\t\t|\n| isReadBody\t\t\t| Boolean\t| true\t\t| 是否尝试从 请求体 里读取 Token\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| isReadHeader\t\t\t| Boolean\t| true\t\t| 是否尝试从 header 里读取 Token\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| isReadCookie\t\t\t| Boolean\t| true\t\t| 是否尝试从 cookie 里读取 Token，此值为 false 后，`StpUtil.login(id)` 登录时也不会再往前端注入Cookie\t\t\t\t|\n| isLastingCookie\t\t| Boolean\t| true\t\t| 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）\t\t\t\t\t\t|\n| isWriteHeader\t\t\t| Boolean\t| false\t\t| 是否在登录后将 Token 写入到响应头\t\t\t\t\t\t\t|\n| logoutRange\t\t| SaLogoutRange\t| TOKEN\t\t| 注销范围 (TOKEN=只注销当前 token 的会话，ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话) (此参数只在调用 StpUtil.logout() 时有效)\t\t\t\t\t\t|\n| isLogoutKeepFreezeOps\t\t| Boolean\t| false\t| 如果 token 已被冻结，是否保留其操作权 (是否允许此 token 调用注销API)\t(此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue(\"token\") 时有效)\t\t\t|\n| isLogoutKeepTokenSession\t| Boolean\t| false\t| 在注销 token 后，是否保留其对应的 Token-Session\t\t\t\t\t|\n| rightNowCreateTokenSession| Boolean\t| false\t| 在登录时，是否立即创建对应的 Token-Session （true=在登录时立即创建，false=在第一次调用 getTokenSession() 时创建）\t|\n| tokenStyle\t\t\t| String\t| uuid\t\t| token风格， [参考：自定义Token风格](/up/token-style)\t\t\t\t\t\t\t\t\t\t|\n| dataRefreshPeriod\t\t| int\t\t| 30\t\t| 默认数据持久组件实现类中，每次清理过期数据间隔的时间 （单位: 秒） ，默认值30秒，设置为-1代表不启动定时清理 \t\t|\n| tokenSessionCheckLogin\t| Boolean\t| true\t| 获取 `Token-Session` 时是否必须登录 （如果配置为true，会在每次获取 `Token-Session` 时校验是否登录），[详解](/use/config?id=配置项详解：tokenSessionCheckLogin)\t\t|\n| autoRenew\t\t\t\t| Boolean\t| true\t\t| 是否打开自动续签 （如果此值为true，框架会在每次直接或间接调用 `getLoginId()` 时进行一次过期检查与续签操作），[参考：token有效期详解](/fun/token-timeout)\t\t|\n| tokenPrefix\t\t\t| String\t| null\t\t| token前缀，例如填写 `Bearer` 实际传参 `satoken: Bearer xxxx-xxxx-xxxx-xxxx` \t[参考：自定义Token前缀](/up/token-prefix) \t\t\t|\n| cookieAutoFillPrefix\t| Boolean\t| false\t\t| cookie 模式是否自动填充 token 提交前缀\t\t\t\t|\n| isPrint\t\t\t\t| Boolean\t| true\t\t| 是否在初始化配置时打印版本字符画\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| isLog\t\t\t\t\t| Boolean\t| false\t\t| 是否打印操作日志\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| logLevel\t\t\t\t| String\t| trace\t\t| 日志等级（trace、debug、info、warn、error、fatal），此值与 logLevelInt 联动\t\t\t\t|\n| logLevelInt\t\t\t| int\t\t| 1\t\t\t| 日志等级 int 值（1=trace、2=debug、3=info、4=warn、5=error、6=fatal），此值与 logLevel 联动\t\t|\n| isColorLog\t\t\t| Boolean\t| null\t\t| 是否打印彩色日志，true=打印彩色日志，false=打印黑白日志，null=框架根据运行终端自行判断是否打印彩色日志 \t\t|\n| jwtSecretKey\t\t\t| String\t| null\t\t| jwt秘钥 （只有集成 `sa-token-temp-jwt` 模块时此参数才会生效），[参考：和 jwt 集成](/plugin/jwt-extend)\t|\n| sameTokenTimeout\t\t| long\t\t| 86400\t\t| Same-Token的有效期 （单位: 秒），[参考：内部服务外网隔离](/micro/same-token)\t\t\t\t\t|\n| basic\t\t\t\t\t| String\t| \"\"\t\t| Http Basic 认证的账号和密码 [参考：Http Basic 认证](/up/basic-auth)\t\t\t\t\t\t|\n| currDomain\t\t\t| String\t| null\t\t| 配置当前项目的网络访问地址\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| checkSameToken\t\t\t| Boolean\t\t| false\t\t| 是否校验Same-Token（部分rpc插件有效）\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| cookie\t\t\t\t| Object\t| new SaCookieConfig()\t| Cookie 配置对象\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| sign\t\t\t\t\t| Object\t| new SaSignConfig()\t| API 签名配置对象\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n\n#### 2.2、Cookie相关配置：\n\n| 参数名称\t\t| 类型\t\t| 默认值\t\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| domain\t\t| String\t| null\t\t| 作用域（写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景）\t\t\t|\n| path\t\t\t| String\t| /\t\t| 路径，默认写在域名根路径下\t\t\t|\n| secure\t\t| Boolean\t| false\t\t| 是否只在 https 协议下有效\t\t\t\t\t\t\t\t\t\t\t\t|\n| httpOnly\t\t| Boolean\t| false\t\t| 是否禁止 js 操作 Cookie \t|\n| sameSite\t\t| String\t| Lax\t\t| 第三方限制级别（Strict=完全禁止，Lax=部分允许，None=不限制）\t\t|\n| extraAttrs\t| String\t| new LinkedHashMap()\t\t| 额外扩展属性\t\t|\n\n\nCookie 配置示例:\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token: \n    # Cookie 相关配置 \n    cookie: \n\t\t# 基础属性 \n        domain: stp.com\n        path: /\n        secure: false\n\t\thttpOnly: true\n\t\tsameSite: Lax\n        # 额外扩展属性\n        extraAttrs:\n            # Cookie 优先级\n            Priority: Medium\n            # Cookie 独立分区\n            Partitioned: \"\"\n            # 可以是任意键值对\n            # abc: def\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# Cookie 相关配置 \n# ---- 基础属性\nsa-token.cookie.domain=stp.com\nsa-token.cookie.path=/\nsa-token.cookie.secure=false\nsa-token.cookie.httpOnly=true\nsa-token.cookie.sameSite=Lax\n# ---- 额外扩展属性\n# Cookie 优先级\nsa-token.cookie.extraAttrs.Priority=Medium\n# Cookie 独立分区\nsa-token.cookie.extraAttrs.Partitioned=\"\"\n# 可以是任意键值对\n# sa-token.cookie.extraAttrs.abc=def\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 2.3、Sign 参数签名相关配置\n\n| 参数名称\t\t\t\t| 类型\t\t| 默认值\t\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| secretKey\t\t\t\t| String\t| null\t\t| API 调用签名秘钥\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| timestampDisparity\t| long\t\t| 900000\t| 接口调用时的时间戳允许的差距（单位：ms），-1 代表不校验差距，默认15分钟\t\t|\n| digestAlgo\t\t\t| String\t| md5\t\t| 对 fullStr 的摘要算法\t\t\t\t\t|\n\n示例：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token: \n    # 参数签名配置 \n    sign: \n\t\t# API 接口调用签名秘钥\n        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# API 接口调用签名秘钥\nsa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n#### 2.4、API Key 相关配置\n\n| 参数名称\t\t\t\t| 类型\t\t| 默认值\t\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| prefix\t\t\t\t| String\t| AK-\t\t| API Key 前缀\t\t\t\t\t\t\t\t\t\t\t\t|\n| timeout\t\t\t\t| long\t\t| 2592000\t| API Key 有效期，-1=永久有效，默认30天 （修改此配置项不会影响到已创建的 API Key）\t|\n| isRecordIndex\t\t\t| String\t| true\t\t| 框架是否记录索引信息\t\t\t\t\t|\n\n示例：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token:\n    # API Key 相关配置\n    api-key:\n        # API Key 前缀\n        prefix: AK-\n        # API Key 有效期，-1=永久有效，默认30天 （修改此配置项不会影响到已创建的 API Key）\n        timeout: 2592000\n        # 框架是否记录索引信息\n        is-record-index: true\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# API Key 前缀\nsa-token.pi-key.prefix=AK-\n# API Key 有效期，-1=永久有效，默认30天 （修改此配置项不会影响到已创建的 API Key）\nsa-token.pi-key.timeout=2592000\n# 框架是否记录索引信息\nsa-token.pi-key.is-record-index=true\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n\n\n\n### 3、单点登录相关配置 \n\n#### 3.1、SSO-Server 端配置\n\n| 参数名称\t\t\t| 类型\t\t| 默认值\t\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| mode\t\t\t\t| String\t| \t\t\t| 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\t\t\t|\n| ticketTimeout\t\t| long\t\t| 300\t\t| ticket 有效期 （单位: 秒）\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| homeRoute\t\t\t| String\t|  \t\t\t| 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\t\t\t|\n| isSlo\t\t\t\t| Boolean\t| true\t\t| 是否打开单点注销功能\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| autoRenewTimeout\t| Bolean\t| false\t| 是否在每次下发 ticket 时，自动续期 token 的有效期（根据全局 timeout 值）\t\t\t|\n| maxRegClient\t\t| int\t\t| 32\t\t| 在 Access-Session 上记录 Client 信息的最高数量（-1=无限），超过此值将进行自动清退处理，先进先出\t\t\t|\n| isCheckSign\t\t| Boolean\t| true\t\t| 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\t\t|\n| clients\t\t\t| Map\t\t| new LinkedHashMap<>();\t\t| 以 Map<String, SaSsoClientModel> 格式配置 Client 列表\t\t\t|\n| allowAnonClient\t| Boolean\t| false\t\t| 是否允许匿名 Client 接入。参考： [匿名 client 接入](/sso/anon-client)\t|\n| allowUrl\t\t\t| String\t| \t\t\t| 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用)，参考：[SSO整合：配置域名校验](/sso/sso-check-domain)\t|\n| secretKey\t\t\t| String\t| \t\t\t| API 调用签名秘钥 (全局默认 + 匿名 client 使用)\t\t|\n\n配置示例：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yml\n# Sa-Token 配置\nsa-token: \n    # SSO-Server 配置\n    sso-server:\n        # Ticket有效期 (单位: 秒)，默认五分钟 \n        ticket-timeout: 300\n        # 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\n        home-route: /home\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# SSO-Server 配置\n# Ticket有效期 (单位: 秒)，默认五分钟 \nsa-token.sso-server.ticket-timeout=300\n# 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\nsa-token.sso-server.home-route=/home\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 3.2、SSO-Client 端配置\n\n| 参数名称\t\t\t| 类型\t\t| 默认值\t\t| 说明\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t|\n| mode\t\t\t\t| String\t| \t\t\t\t\t| 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\t\t\t|\n| client\t\t\t| String\t| \"\"\t\t\t\t| 当前 Client 名称标识，用于和 ticket 码的互相锁定\t\t\t|\n| serverUrl\t\t\t| String\t| null\t\t\t\t| 配置 Server 端主机总地址，拼接在 `authUrl`、`checkTicketUrl`、`userinfoUrl`、`sloUrl` 属性前面，用以简化各种 url 配置，参考：[详解](/sso/sso-questions?id=问：模式三配置一堆-xxx-url-，有办法简化一下吗？)\t|\n| authUrl\t\t\t| String\t| /sso/auth\t\t\t| 配置 Server 端单点登录授权地址\t\t\t\t\t|\n| signoutUrl\t\t| String\t| /sso/signout\t\t| 配置 Server 端单点注销地址\t\t\t\t\t\t\t\t\t\t|\n| pushUrl\t\t\t| String\t| /sso/pushS\t\t| 配置 Server 端的推送消息地址\t\t\t\t\t\t|\n| getDataUrl\t\t| String\t| /sso/getData\t\t| 配置 Server 端的 拉取数据 地址\t\t\t\t\t\t\t\t\t|\n| currSsoLogin\t\t| String\t| null\t\t\t\t| 配置当前 Client 端的登录地址（为空时自动获取）\t|\n| currSsoLogoutCall\t| String\t| null\t\t\t\t| 配置当前 Client 端的单点注销回调URL （为空时自动获取）\t|\n| isHttp\t\t\t| Boolean\t| false\t\t\t\t| 是否打开模式三（此值为 true 时将使用 http 请求：校验 ticket 值、单点注销、拉取数据getData），参考：[详解](/use/config?id=配置项详解：isHttp) \t|\n| isSlo\t\t\t\t| Boolean\t| true\t\t\t\t| 是否打开单点注销功能\t\t\t\t\t\t\t|\n| regLogoutCall\t\t| Boolean\t| false\t\t\t\t| 是否注册单点登录注销回调 (为 true 时，登录时附带单点登录回调地址，并且开放 /sso/logoutCall 地址)\t\t\t\t\t\t\t|\n| secretKey\t\t\t| String\t| \"\"\t\t\t\t| API 调用签名秘钥\t\t\t\t\t|\n| isCheckSign\t\t| Boolean\t| true\t\t\t\t| 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\t\t|\n\n配置示例：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token: \n    # SSO-相关配置\n    sso-client: \n        # sso-server 端主机地址\n        server-url: http://sa-sso-server.com:9000\n        # 是否打开单点注销功能 \n        is-slo: true\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# sso-server 端主机地址\nsa-token.sso-client.server-url=http://sa-sso-server.com:9000\n# 是否打开单点注销功能 \nsa-token.sso-client.is-slo=true\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n#### 3.3、SaSsoClientModel 配置\n\n| 参数名称\t\t\t| 类型\t\t| 默认值\t\t| 说明\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t|\n| client\t\t\t| String\t| \"\"\t\t\t\t| 当前 Client 名称标识，用于和 ticket 码的互相锁定\t\t\t|\n| allowUrl\t\t\t| String\t| \t\t\t| 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用)，参考：[SSO整合：配置域名校验](/sso/sso-check-domain)\t|\n| isPush\t\t\t| Boolean\t| false\t\t\t\t| 是否接收推送消息\t\t\t|\n| isSlo\t\t\t\t| Boolean\t| true\t\t\t\t| 是否打开单点注销功能\t\t\t\t\t\t\t|\n| secretKey\t\t\t| String\t| \"\"\t\t\t\t| API 调用签名秘钥\t\t\t\t\t|\n| serverUrl\t\t\t| String\t| null\t\t\t\t| 配置 Server 端主机总地址，拼接在 `authUrl`、`checkTicketUrl`、`userinfoUrl`、`sloUrl` 属性前面，用以简化各种 url 配置，参考：[详解](/sso/sso-questions?id=问：模式三配置一堆-xxx-url-，有办法简化一下吗？)\t|\n| pushUrl\t\t\t| String\t| /sso/pushC\t\t| 配置此 Client 端的推送消息地址\t\t\t\t\t\t|\n\n配置示例：\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yml\n# Sa-Token 配置\nsa-token: \n    # SSO-Server 配置\n    sso-server:\n        # 应用列表：配置接入的应用信息\n        clients:\n            # 应用 sso-client1 \n            sso-client1:\n                client: sso-client1\n                allow-url: \"*\"\n                secret-key: SSO-C1-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n            # 应用 sso-client2 \n            sso-client2:\n                client: sso-client2\n                allow-url: \"*\"\n                secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# 应用列表：配置接入的应用信息\n# 应用 sso-client1 \nsa-token.sso-server.clients.sso-client1.client=sso-client1\nsa-token.sso-server.clients.sso-client1.allow-url=*\nsa-token.sso-server.clients.sso-client1.secret-key=SSO-C1-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n\n# 应用 sso-client2 \nsa-token.sso-server.clients.sso-client2.client=sso-client2\nsa-token.sso-server.clients.sso-client2.allow-url=*\nsa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor\n```\n\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n\n\n### 4、OAuth2.0相关配置 \n\n#### 4.1、OAuth2-Server 相关配置\n\n| 参数名称\t\t\t\t\t| 类型\t\t| 默认值\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| enableAuthorizationCode\t| Boolean\t| true\t\t| 是否打开模式：授权码（`Authorization Code`）\t\t\t\t\t\t\t\t|\n| enableImplicit\t\t\t| Boolean\t| true\t\t| 是否打开模式：隐藏式（`Implicit`）\t\t\t\t\t\t\t\t\t\t\t|\n| enablePassword\t\t\t| Boolean\t| true\t\t| 是否打开模式：密码式（`Password`）\t\t\t\t\t\t\t\t\t\t\t|\n| enableClientCredentials\t| Boolean\t| true\t\t| 是否打开模式：凭证式（`Client Credentials`）\t\t\t\t\t\t\t\t|\n| codeTimeout\t\t\t\t| long\t\t| 300\t\t| Code授权码 保存的时间（单位：秒） 默认五分钟\t\t\t\t\t\t\t\t\t|\n| accessTokenTimeout\t\t| long\t\t| 7200\t\t| 全局默认配置所有应用：`Access-Token` 保存的时间（单位：秒）默认两个小时\t\t\t\t\t\t\t\t|\n| refreshTokenTimeout\t\t| long\t\t| 2592000\t| 全局默认配置所有应用：`Refresh-Token` 保存的时间（单位：秒） 默认30 天\t\t\t\t\t\t\t\t|\n| clientTokenTimeout\t\t| long\t\t| 7200\t\t| 全局默认配置所有应用：`Client-Token` 保存的时间（单位：秒） 默认两个小时\t\t\t\t\t\t\t\t|\n| maxAccessTokenCount\t\t| int\t\t| 12\t\t| 全局默认配置所有应用：单个应用单个用户最多同时存在的 Access-Token 数量\t\t\t\t|\n| maxRefreshTokenCount\t\t| int\t\t| 12\t\t| 全局默认配置所有应用：单个应用单个用户最多同时存在的 Refresh-Token 数量\t\t\t|\n| maxClientTokenCount\t\t| int\t\t| 12\t\t| 全局默认配置所有应用：单个应用最多同时存在的 Client-Token 数量\t\t\t|\n| isNewRefresh\t\t\t\t| Boolean\t| false\t\t| 全局默认配置所有应用：是否在每次 `Refresh-Token` 刷新 `Access-Token` 时，产生一个新的 `Refresh-Token`\t|\n| openidDigestPrefix\t\t| String\t| openid_default_digest_prefix\t\t| 默认 openid 生成算法中使用的摘要前缀\t\t\t\t \t|\n| unionidDigestPrefix\t\t| String\t| unionid_default_digest_prefix\t\t| 默认 unionid 生成算法中使用的摘要前缀\t\t\t\t \t|\n| higherScope\t\t\t\t| String\t| \t\t| 指定高级权限，多个用逗号隔开\t\t\t\t \t|\n| lowerScope\t\t\t\t| String\t| \t\t| 指定低级权限，多个用逗号隔开\t\t\t\t \t|\n| mode4ReturnAccessToken\t| Boolean\t| false\t| 模式4是否返回 AccessToken 字段，用于兼容OAuth2标准协议\t\t\t \t|\n| hideStatusField\t\t\t| Boolean\t| false\t| 是否在返回值中隐藏默认的状态字段 (code、msg、data)\t\t\t \t|\n| oidc\t\t\t\t\t\t| SaOAuth2OidcConfig\t| new SaOAuth2OidcConfig()\t| OIDC 相关配置\t\t\t \t|\n| clients\t\t\t\t\t| Map<String, SaClientModel>\t| 配置 SaClientModel 列表信息\t\t\t \t|\n\n配置示例：\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token: \n    token-name: sa-token-oauth2-server\n    # OAuth2.0 配置 \n    oauth2-server: \n        enable-authorization-code: true\n        enable-implicit: true\n        enable-password: true\n        enable-client-credentials: true\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\n# Sa-Token 配置 \nsa-token.token-name=sa-token-oauth2-server\n# OAuth2.0 配置 \nsa-token.oauth2-server.enable-authorization-code=true\nsa-token.oauth2-server.enable-implicit=true\nsa-token.oauth2-server.enable-password=true\nsa-token.oauth2-server.enable-client-credentials=true\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n#### 4.2、OIDC 相关配置\n| 参数名称\t\t\t\t\t| 类型\t\t| 默认值\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t\t| :--------\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| iss\t\t\t\t\t\t| String\t| \t\t\t| iss 值，如不配置则自动计算\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| idTokenTimeout\t\t\t| long\t\t| 600\t\t| idToken 有效期（单位秒） 默认十分钟\t\t\t\t\t\t\t\t\t\t\t|\n\n<!---------------------------- tabs:start ---------------------------->\n<!------------- tab:yaml 风格  ------------->\n``` yaml\n# Sa-Token 配置\nsa-token: \n    oauth2-server: \n\t\toidc: \n\t\t\tiss: xxx\n\t\t\tidTokenTimeout: 600\n```\n<!------------- tab:properties 风格  ------------->\n``` properties\nsa-token.oauth2-server.oidc.iss=xxx\nsa-token.oauth2-server.oidc.idTokenTimeout=600\n```\n<!---------------------------- tabs:end ---------------------------->\n\n\n\n#### 4.3、SaClientModel属性定义\n| 参数名称\t\t\t\t| 类型\t\t\t| 默认值\t| 说明\t\t\t\t\t\t\t\t\t\t\t\t\t|\n| :--------\t\t\t\t| :--------\t\t| :--------\t| :--------\t\t\t\t\t\t\t\t\t\t\t|\n| clientId\t\t\t\t| String\t\t| null\t\t| 应用id，应该全局唯一\t\t\t\t\t\t\t\t|\n| clientSecret\t\t\t| String\t\t| null\t\t| 应用秘钥\t\t\t\t\t\t\t\t\t\t\t|\n| contractScopes\t\t| List<String>\t| []\t\t| 应用签约的所有权限 \t\t\t\t\t\t\t\t\t|\n| allowRedirectUris\t\t| List<String>\t| []\t\t| 应用允许授权的所有URL（可以使用 `*` 号通配符）\t\t\t|\n| allowGrantTypes\t\t| List<String>\t| []\t\t| 应用允许的所有 `grant_type`\t\t\t\t\t\t\t|\n| subjectId\t\t\t\t| String\t\t| null\t\t| 应用主体id\t\t\t\t\t\t\t|\n| accessTokenTimeout\t| long\t\t\t| 取全局配置 (7200)\t| 此应用`Access-Token` 保存的时间（单位：秒）  [默认取全局配置]\t|\n| refreshTokenTimeout\t| long\t\t\t| 取全局配置 (2592000)| 此应用`Refresh-Token` 保存的时间（单位：秒） [默认取全局配置]\t|\n| clientTokenTimeout\t| Boolean\t\t| 取全局配置 (7200)| 此应用`Client-Token` 保存的时间（单位：秒） [默认取全局配置]\t|\n| maxAccessTokenCount\t| long\t\t\t| 取全局配置 (12)| 此应用单个用户最多同时存在的 Access-Token 数量\t|\n| maxRefreshTokenCount\t| long\t\t\t| 取全局配置 (12)| 此应用单个用户最多同时存在的 Refresh-Token 数量\t|\n| maxClientTokenCount\t| long\t\t\t| 取全局配置 (12)| 此应用最多同时存在的 Client-Token 数量\t|\n| isNewRefresh\t\t\t| Boolean\t\t| 取全局配置\t\t| 单独配置此 Client：是否在每次 `Refresh-Token` 刷新 `Access-Token` 时，产生一个新的 Refresh-Token [ 默认取全局配置 ]\t|\n| isAutoConfirm\t\t\t| Boolean\t\t| false\t\t| 是否允许此应用自动确认授权 <span style=\"color: red;\">（高危配置，禁止向不被信任的第三方开启此选项）</span>\t|\n\n\n\n\n\n### 5、部分配置项详解\n\n对部分配置项做一下详解 \n\n#### 配置项详解：maxLoginCount\n\n配置含义：同一账号最大登录数量。\n\n在配置 `isConcurrent=true`, `isShare=false` 时，Sa-Token 将允许同一账号并发登录，且每次登录都会产生一个新Token，\n这些 Token 都会以 `SaTerminalInfo` 的形式记录在其 `Account-Session` 之上，这就造成一个问题：\n\n随着同一账号登录的次数越来越多，SaTerminalInfo 的列表也会越来越大，极端情况下，列表长度可能达到成百上千以上，严重拖慢数据处理速度，\n为此 Sa-Token 对这个 SaTerminalInfo 列表的大小设定一个上限值，也就是 `maxLoginCount`，默认值=12。\n\n假设一个账号的登录数量超过 `maxLoginCount` 后，将会主动注销第一个登录的会话（先进先出），以此保证队列中的有效会话数量始终 `<= maxLoginCount` 值。\n\n\n#### 配置项详解：tokenSessionCheckLogin\n配置含义：获取 `Token-Session` 时是否必须登录 （如果配置为true，会在每次获取 `Token-Session` 时校验是否登录）。\n\n在调用 `StpUtil.login(id)` 登录后，\n\n- 调用 `StpUtil.getSession()` 可以获取这个会话的 `Account-Session` 对象。\n- 调用 `StpUtil.getTokenSession()` 可以获取这个会话 `Token-Session` 对象。\n\n关于两种 Session 有何区别，可以参考这篇：[Session模型详解](/fun/session-model)，此处暂不赘述。\n\n从设计上讲，无论会话是否已经登录，只要前端提供了Token，我们就可以找到这个 Token 的专属 `Token-Session` 对象，**这非常灵活但不安全**，\n因为前端提交的 Token 可能是任意伪造的。\n\n为了解决这个问题，`StpUtil.getTokenSession()` 方法在获取 `Token-Session` 时，会率先检测一下这个 Token 是否是一个有效Token：\n- 如果是有效Token，正常返回 `Token-Session` 对象\n- 如果是无效Token，则抛出异常。\n\n这样就保证了伪造的 Token 是无法获取 `Token-Session` 对象的。\n\n但是 —— 有的场景下我们又确实需要在登录之前就使用 Token-Session 对象，这时候就把配置项 `tokenSessionCheckLogin` 值改为 `false` 即可。\n\n<!-- \n#### 配置项详解：isAutoMode\n\n配置含义：是否自动判断此 Client 开放的授权模式。\n\n- 此值为 true 时：四种模式（`isCode、isImplicit、isPassword、isClient`）是否生效，依靠全局设置；\n- 此值为 false 时：四种模式（`isCode、isImplicit、isPassword、isClient`）是否生效，依靠局部配置+全局配置（两个都为 true 时才打开） \n -->\n\n#### 配置项详解：isHttp\n\n配置含义：是否打开单点登录模式三。\n\n- 此配置项为 false 时，代表使用SSO模式二：使用 Redis 校验 ticket 值、删除 Redis 数据做到单点注销、使用 Redis 同步 Userinfo 数据。\n- 此配置项为 true 时，代表使用SSO模式三：使用 Http 请求校验 ticket 值、使用 Http 请求做到单点注销、使用 Http 请求同步 Userinfo 数据。\n\n\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/resources/application.yml\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 框架配置 —— [ application.yml ]\n</a>\n<a class=\"dt-btn\" href=\"https://www.wenjuan.ltd/s/nUfe2iU/\" target=\"_blank\">本章小练习：Sa-Token 基础 - 框架配置，章节测试</a>\n\n"
  },
  {
    "path": "sa-token-doc/use/dao-extend.md",
    "content": "# 持久层扩展\n--- \n\nSa-token默认将会话数据保存在内存中，此模式读写速度最快，且避免了序列化与反序列化带来的性能消耗，但是此模式也有一些缺点，比如：重启后数据会丢失，无法在集群模式下共享数据\n\n为此，Sa-Token将数据持久操作全部抽象到 `SaTokenDao` 接口中，保证大家对框架进行灵活扩展，比如我们可以将会话数据存储在 `Redis`、`Memcached`等专业的缓存中间件中，做到重启数据不丢失，而且保证分布式环境下多节点的会话一致性\n\n除了框架内部对`SaTokenDao`提供的基于内存的默认实现，官方仓库还提供了以下扩展方案：<br>\n\n\n### 1. Sa-Token 整合 Redis (使用jdk默认序列化方式)\n``` xml \n<!-- Sa-Token整合redis (使用jdk默认序列化方式) -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n优点：兼容性好，缺点：Session序列化后基本不可读，对开发者来讲等同于乱码\n\n\n### 2. Sa-Token 整合 RedisTemplate\n``` xml \n<!-- Sa-Token 整合 RedisTemplate -->\n<dependency>\n\t<groupId>cn.dev33</groupId>\n\t<artifactId>sa-token-redis-template</artifactId>\n\t<version>${sa.top.version}</version>\n</dependency>\n```\n优点：Session 序列化后可读性强（默认 JSON 格式），可灵活手动修改\n\n\n<br>\n\n### 集成Redis请注意：\n\n\n**1. 无论使用哪种序列化方式，你都必须为项目提供一个Redis实例化方案，例如：**\n``` xml\n<!-- 提供redis连接池 -->\n<dependency>\n\t<groupId>org.apache.commons</groupId>\n\t<artifactId>commons-pool2</artifactId>\n</dependency>\n```\n\n**2. 引入了依赖，我还需要为Redis配置连接信息吗？** <br>\n需要！只有项目初始化了正确的Redis实例，`Sa-Token`才可以使用Redis进行数据持久化，参考以下`yml配置`：\n``` java\n# 端口\nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 1\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        # password: \n        # 连接超时时间（毫秒）\n        timeout: 1000ms\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n```\n\n\n**3. 集成Redis后，是我额外手动保存数据，还是框架自动保存？** <br>\n框架自动保存。集成`Redis`只需要引入对应的`pom依赖`即可，框架所有上层API保持不变\n\n\n<br><br>\n更多框架的集成方案正在更新中... (欢迎大家提交pr)\n\n\n\n"
  },
  {
    "path": "sa-token-doc/use/jur-auth.md",
    "content": "# 权限认证\n\n--- \n\n### 1、设计思路\n\n权限认证的最终目的在于：规定哪些用户可以访问哪些 接口/页面/资源。\n\n例如对于同一个页面：\n- 管理员账号访问：<green>正常返回数据</green>。\n- 普通账号访问：<red>权限不足，拒绝访问</red>。\n\n<img class=\"w-100\" src=\"/big-file/doc/use/use-jur-auth.svg\" />\n\n\n那么框架是如何判断，一个账号是否有权限访问某个接口的呢？\n\n从底层数据的角度来讲，<green>**每个账号都会拥有一组权限码集合，框架要做的就是校验这个集合中是否包含指定的权限码。**</green>\n\n- 有，就让你通过。\n- 没有？那么禁止访问！\n\n<img class=\"w-100\" src=\"/big-file/doc/use/use-jur-check.svg\" />\n\n\n所以现在问题的核心就是两个：\n1. 如何定义一个账号所拥有的权限码集合？\n2. 本次操作需要校验的权限码是哪个？\n\n\n### 2、获取当前账号权限码集合\n\n在进行具体的权限校验之前，你需要实现 `StpInterface`接口，告诉框架指定账号拥有的权限码集合是哪些：\n\n``` java \n/**\n * 自定义权限加载接口实现类\n */\n@Component\t// 保证此类被 SpringBoot 扫描，完成 Sa-Token 的自定义权限验证扩展 \npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\t// 本 list 仅做模拟，实际项目中要根据具体业务逻辑来查询权限\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"101\");\n\t\tlist.add(\"user.add\");\n\t\tlist.add(\"user.update\");\n\t\tlist.add(\"user.get\");\n\t\t// list.add(\"user.delete\");\n\t\tlist.add(\"art.*\");\n\t\treturn list;\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)\n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\t// 本 list 仅做模拟，实际项目中要根据具体业务逻辑来查询角色\n\t\tList<String> list = new ArrayList<String>();\t\n\t\tlist.add(\"admin\");\n\t\tlist.add(\"super-admin\");\n\t\treturn list;\n\t}\n\n}\n```\n\n**参数解释：**\n- loginId：账号id，即你在调用 `StpUtil.login(id)` 时写入的`唯一标识`值。\n- loginType：账号体系标识，此处可以暂时忽略，在 [ 多账户认证 ] 章节下会对这个概念做详细的解释。\n\n可参考代码：[码云：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)\n\n> [!WARNING| label:有同学会产生疑问：我实现了此接口，但是程序启动时好像并没有执行，是不是我写错了？] \n> 答：不执行是正常现象，程序启动时不会执行这个接口的方法，在每次调用鉴权代码时，才会执行到此。\n\n\n### 3、权限校验\n然后就可以用以下 api 来鉴权了\n\n``` java\t\n// 获取：当前账号所拥有的权限集合\nStpUtil.getPermissionList();\n\n// 判断：当前账号是否含有指定权限, 返回 true 或 false\nStpUtil.hasPermission(\"user.add\");\t\t\n\n// 校验：当前账号是否含有指定权限, 如果验证未通过，则抛出异常: NotPermissionException \nStpUtil.checkPermission(\"user.add\");\t\t\n\n// 校验：当前账号是否含有指定权限 [指定多个，必须全部验证通过]\nStpUtil.checkPermissionAnd(\"user.add\", \"user.delete\", \"user.get\");\t\t\n\n// 校验：当前账号是否含有指定权限 [指定多个，只要其一验证通过即可]\nStpUtil.checkPermissionOr(\"user.add\", \"user.delete\", \"user.get\");\t\n```\n\n扩展：<red>`NotPermissionException`</red> 异常对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常\n\n\n### 4、角色校验\n在 Sa-Token 中，角色和权限可以分开独立验证\n\n``` java\n// 获取：当前账号所拥有的角色集合\nStpUtil.getRoleList();\n\n// 判断：当前账号是否拥有指定角色, 返回 true 或 false\nStpUtil.hasRole(\"super-admin\");\t\t\n\n// 校验：当前账号是否含有指定角色标识, 如果验证未通过，则抛出异常: NotRoleException\nStpUtil.checkRole(\"super-admin\");\t\t\n\n// 校验：当前账号是否含有指定角色标识 [指定多个，必须全部验证通过]\nStpUtil.checkRoleAnd(\"super-admin\", \"shop-admin\");\t\t\n\n// 校验：当前账号是否含有指定角色标识 [指定多个，只要其一验证通过即可] \nStpUtil.checkRoleOr(\"super-admin\", \"shop-admin\");\t\t\n```\n\n扩展：<red>NotRoleException</red> 异常对象可通过 `getLoginType()` 方法获取具体是哪个 `StpLogic` 抛出的异常\n\n\n\n### 5、拦截全局异常\n有同学要问，鉴权失败，抛出异常，然后呢？要把异常显示给用户看吗？**当然不可以！**\n\n你可以创建一个全局异常拦截器，统一返回给前端的格式，参考：\n\n``` java\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n    // 全局异常拦截 \n    @ExceptionHandler\n    public SaResult handlerException(Exception e) {\n        e.printStackTrace(); \n        return SaResult.error(e.getMessage());\n    }\n}\n```\n\n可参考：[码云：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)\n\n\n### 6、权限通配符\nSa-Token允许你根据通配符指定**泛权限**，例如当一个账号拥有`art.*`的权限时，`art.add`、`art.delete`、`art.update`都将匹配通过\n\n``` java\n// 当拥有 art.* 权限时\nStpUtil.hasPermission(\"art.add\");        // true\nStpUtil.hasPermission(\"art.update\");     // true\nStpUtil.hasPermission(\"goods.add\");      // false\n\n// 当拥有 *.delete 权限时\nStpUtil.hasPermission(\"art.delete\");      // true\nStpUtil.hasPermission(\"user.delete\");     // true\nStpUtil.hasPermission(\"user.update\");     // false\n\n// 当拥有 *.js 权限时\nStpUtil.hasPermission(\"index.js\");        // true\nStpUtil.hasPermission(\"index.css\");       // false\nStpUtil.hasPermission(\"index.html\");      // false\n```\n\n> [!WARNING| label:上帝权限] \n> 当一个账号拥有 `\"*\"` 权限时，他可以验证通过任何权限码 （角色认证同理）\n\n\n### 7、如何把权限精确到按钮级？\n权限精确到按钮级的意思就是指：**权限范围可以控制到页面上的每一个按钮是否显示**。\n\n<img class=\"w-100\" src=\"/big-file/doc/use/use-jur-btn.svg\" />\n\n思路：如此精确的范围控制只依赖后端已经难以完成，此时需要前端进行一定的逻辑判断。\n\n如果是前后端一体项目，可以参考：[Thymeleaf 标签方言](/plugin/thymeleaf-extend)，如果是前后端分离项目，则：\n\n1. 在登录时，把当前账号拥有的所有权限码一次性返回给前端。\n2. 前端将权限码集合保存在`localStorage`或其它全局状态管理对象中。\n3. 在需要权限控制的按钮上，使用 js 进行逻辑判断，例如在`Vue`框架中我们可以使用如下写法：\n``` js\n// `arr`是当前用户拥有的权限码数组\n// `user.delete`是显示按钮需要拥有的权限码\n// `删除按钮`是用户拥有权限码才可以看到的内容。\n<div>\n\t<button v-if=\"arr.indexOf('user.get') > -1\">查询用户</button>\n\t<button v-if=\"arr.indexOf('user.update') > -1\">修改用户</button>\n\t<button v-if=\"arr.indexOf('user.delete') > -1\">删除按钮</button>\n</div>\n```\n\n以上写法只为提供一个参考示例，不同框架有不同写法，大家可根据项目技术栈灵活封装进行调用。\n\n\n> [!ATTENTION| label:前端有了鉴权后端还需要鉴权吗？] \n> **需要！** <br>\n> 前端的鉴权只是一个辅助功能，对于专业人员这些限制都是可以轻松绕过的，为保证服务器安全：**无论前端是否进行了权限校验，后端接口都需要对会话请求再次进行权限校验！**\n\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/JurAuthController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 权限认证 —— [ JurAuthController.java ]\n</a>\n<a class=\"dt-btn\" href=\"https://www.wenjuan.ltd/s/ZfIjYr9/\" target=\"_blank\">本章小练习：Sa-Token 基础 - 权限认证，章节测试</a>\n"
  },
  {
    "path": "sa-token-doc/use/kick.md",
    "content": "# 踢人下线\n\n所谓踢人下线，核心操作就是找到指定 `loginId` 对应的 `Token`，并设置其失效。\n\n<img src=\"/big-file/doc/use/kickout.png\" alt=\"踢下线\">\n\n--- \n\n\n### 1、强制注销\n``` java\nStpUtil.logout(10001);                    // 强制指定账号注销下线 \nStpUtil.logout(10001, \"PC\");              // 强制指定账号指定端注销下线 \nStpUtil.logoutByTokenValue(\"token\");      // 强制指定 Token 注销下线 \n```\n\n\n### 2、踢人下线\n``` java\nStpUtil.kickout(10001);                    // 将指定账号踢下线 \nStpUtil.kickout(10001, \"PC\");              // 将指定账号指定端踢下线\nStpUtil.kickoutByTokenValue(\"token\");      // 将指定 Token 踢下线\n```\n\n强制注销 和 踢人下线 的区别在于：\n- 强制注销等价于对方主动调用了注销方法，再次访问会提示：Token无效。\n- 踢人下线不会清除Token信息，而是将其打上特定标记，再次访问会提示：Token已被踢下线。\n\n\n<button class=\"show-img\" img-src=\"/big-file/doc/use/g3--kickout.gif\">加载动态演示图</button>\n\n\n### 3、顶人下线\n“顶人下线” 操作发生在框架登录时顶退旧登录设备，属于框架内部操作，一般情形下你不会调用到此 API：\n``` java\nStpUtil.replaced(10001);                    // 将指定账号顶下线 \nStpUtil.replaced(10001, \"PC\");              // 将指定账号指定端顶下线\nStpUtil.replacedByTokenValue(\"token\");      // 将指定 Token 顶下线\n```\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/KickoutController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 踢人下线 —— [ KickoutController.java ]\n</a>\n<a class=\"dt-btn\" href=\"https://www.wenjuan.ltd/s/MFNN7bK/\" target=\"_blank\">本章小练习：Sa-Token 基础 - 踢人下线，章节测试</a>\n\n\n"
  },
  {
    "path": "sa-token-doc/use/login-auth.md",
    "content": "# 登录认证\n\n--- \n\n\n### 1、开始登录\n\n一个完整的登录认证包含哪些步骤？让我们代入用户视角：在打开 网站/APP 后，用户的操作流程大致可以概括为：\n1. 打开 网站/APP，进入登录页。\n2. 输入 账号+密码 进行登录。\n3. 进入首页，进行业务相关操作。\n4. 注销登录，关闭 网站/APP。\n\n在整个流程中，Sa-Token 负责哪些部分呢？ 下图可以帮助你理解：\n\n<img class=\"w-100\" src=\"/big-file/doc/use/use-login-auth.svg\" />\n\n如上图所示：<green>**无论用户采用何种登录方式，本质上都是通过提交一定的认证信息，使系统可以定位到 Ta 的唯一标识 —— userId**</green>。\n\n当我们拿到 userId 后，便可以调用框架提供的 API 进行登录：\n\n``` java\n// 会话登录：参数填写要登录的账号id，建议的数据类型：long | int | String， 不可以传入复杂类型，如：User、Admin 等等\nStpUtil.login(Object userId);\t \n```\n\n只此一句代码，便可以使会话登录成功。实际上，Sa-Token 在背后做了大量的工作，包括但不限于：\n \n1. 检查此账号是否之前已有登录；\n2. 为账号生成 Token 凭证与 Session 会话；\n3. 记录 Token 活跃时间；\n4. 通知全局侦听器，xx 账号登录成功；\n5. 检查此账号登录数量是否已达上限；\n6. 将 Token 注入到请求上下文；\n7. 等等其它工作…… \n\n你暂时不需要完整了解完整过程，你只需要记住关键一点：<green>**Sa-Token 为这个账号创建了一个 token 凭证，且通过 Cookie 上下文返回给了前端**</green>。\n\n所以一般情况下，我们的登录接口代码，会大致类似如下：\n\n``` java\n// 会话登录接口 \n@RequestMapping(\"doLogin\")\npublic SaResult doLogin(String name, String pwd) {\n\t// 第一步：比对前端提交的账号名称、密码\n\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t// 第二步：根据账号id，进行登录 \n\t\tStpUtil.login(10001);\n\t\treturn SaResult.ok(\"登录成功\");\n\t}\n\treturn SaResult.error(\"登录失败\");\n}\n```\n\n如果你对以上代码阅读没有压力，你可能会注意到略显奇怪的一点：<green>**此处仅仅做了会话登录，但并没有主动向前端返回 token 信息。**</green>\n\n是因为不需要吗？严格来讲是需要的，只不过 `StpUtil.login(id)` 方法利用了 Cookie 自动注入的特性，省略了你手写返回 token 的代码。\n\n> [!TIP| label:Cookie 是什么？] \n> 如果你对 Cookie 功能还不太了解，也不用担心，我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能，现在你只需要了解最基本的两点：\n> \n> - Cookie 可以从后端控制往浏览器中写入 token 值。\n> - Cookie 会在前端每次发起请求时自动提交 token 值。\n> \n> 因此，在 Cookie 功能的加持下，我们可以仅靠 `StpUtil.login(id)` 一句代码就完成登录认证。\n> \n> 在浏览器打开 f12 控制台，即可看到被注入的 Cookie 值：\n> \n> <button class=\"show-img\" img-src=\"/big-file/doc/use/sa-login-cookie-pre.png\">加载演示图</button>\n\n\n### 2、校验是否登录\n\n对于一些登录之后才能访问的接口（例如：查询我的账号资料），我们通常的做法是增加一层接口校验：\n\n- 如果校验通过，则：<green>正常返回数据。</green>\n- 如果校验未通过，则：<red>抛出异常，告知其需要先进行登录。</red>\n\n<img class=\"w-100\" src=\"/big-file/doc/use/use-login-check.svg\" />\n\n<!-- <button class=\"show-img\" img-src=\"/big-file/doc/use/g3--login-auth.gif\">加载动态演示图</button> -->\n\n使用以下方法判断当前会话是否已登录：\n\n``` java\n// 判断当前会话是否已经登录，返回 true=已登录，false=未登录\nStpUtil.isLogin();\n\n// 检验当前会话是否已经登录, 如果已登录代码会安全通过，未登录则抛出异常：`NotLoginException`\nStpUtil.checkLogin();\n```\n\n例如我们可以在接口内，根据是否登录返回不同的信息：\n\n``` java\n// 获取我的资料信息 \n@RequestMapping(\"myInfo\")\npublic String myInfo() {\n\tif( StpUtil.isLogin() ) {\n\t\t// ... \n\t\treturn \"我的资料信息...\";\n\t} else {\n\t\treturn \"未登录，请先登录\";\n\t}\n}\n```\n\n或者在未登录时直接抛出全局异常：\n\n``` java\n// 获取我的资料信息 \n@RequestMapping(\"myInfo\")\npublic String myInfo() {\n\tStpUtil.checkLogin();  // 如果当前未登录，这句代码会直接抛出异常 `NotLoginException`\n\treturn \"我的资料信息\";\n}\n```\n\n配合全局异常处理器，统一返回固定格式数据到前端：\n``` java\n@RestControllerAdvice\npublic class GlobalException {\n\t@ExceptionHandler(NotLoginException.class)\n\tpublic SaResult handlerException(NotLoginException e) {\n\t\treturn SaResult.error(e.getMessage());\n\t}\n}\n```\n\n异常 <red>`NotLoginException`</red> 代表当前会话暂未登录，可能的原因有很多：\n- 前端没有提交 token。\n- 前端提交的 token 是无效的。\n- 前端提交的 token 已经过期。 \n- …… \n\n可参照此篇：[未登录场景值](/fun/not-login-scene)，了解如何获取未登录的场景值。\n\n\n### 3、会话查询\n\n如果你想要获取当前登录的是谁：\n\n``` java\n// 获取当前会话账号id, 如果未登录，则抛出异常：`NotLoginException`\nStpUtil.getLoginId();\n\n// 类似查询API还有：\nStpUtil.getLoginIdAsString();    // 获取当前会话账号id, 并转化为`String`类型\nStpUtil.getLoginIdAsInt();       // 获取当前会话账号id, 并转化为`int`类型\nStpUtil.getLoginIdAsLong();      // 获取当前会话账号id, 并转化为`long`类型\n\n// ---------- 以下方法可以指定未登录情形下返回的默认值 ----------\n\n// 获取当前会话账号id, 如果未登录，则返回 null \nStpUtil.getLoginIdDefaultNull();\n\n// 获取当前会话账号id, 如果未登录，则返回默认值 （`defaultValue`可以为任意类型）\nStpUtil.getLoginId(T defaultValue);\n```\n\n\n### 4、token 查询\n``` java\n// 获取当前会话的 token 值\nStpUtil.getTokenValue();\n\n// 获取当前`StpLogic`的 token 名称\nStpUtil.getTokenName();\n\n// 获取指定 token 对应的账号id，如果未登录，则返回 null\nStpUtil.getLoginIdByToken(String tokenValue);\n\n// 获取当前会话剩余有效期（单位：s，返回-1代表永久有效）\nStpUtil.getTokenTimeout();\n\n// 获取当前会话的 token 信息参数\nStpUtil.getTokenInfo();\n```\n\n有关`TokenInfo`参数详解，请参考：[TokenInfo参数详解](/fun/token-info)\t\n\n\n### 5、会话注销\n``` java\n// 当前会话注销登录\nStpUtil.logout();\n```\n\n\n### 6、来个小测试，加深一下理解\n新建 `LoginController`，复制或手动敲出以下代码\n``` java\n/**\n * 登录测试 \n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n\t// 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n\t@RequestMapping(\"doLogin\")\n\tpublic SaResult doLogin(String name, String pwd) {\n\t\t// 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n\t\tif(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n\t\t\tStpUtil.login(10001);\n\t\t\treturn SaResult.ok(\"登录成功\");\n\t\t}\n\t\treturn SaResult.error(\"登录失败\");\n\t}\n\n\t// 查询登录状态  ---- http://localhost:8081/acc/isLogin\n\t@RequestMapping(\"isLogin\")\n\tpublic SaResult isLogin() {\n\t\treturn SaResult.ok(\"是否登录：\" + StpUtil.isLogin());\n\t}\n\t\n\t// 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n\t@RequestMapping(\"tokenInfo\")\n\tpublic SaResult tokenInfo() {\n\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t}\n\t\n\t// 测试注销  ---- http://localhost:8081/acc/logout\n\t@RequestMapping(\"logout\")\n\tpublic SaResult logout() {\n\t\tStpUtil.logout();\n\t\treturn SaResult.ok();\n\t}\n\t\n}\n```\n\n \n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/LoginAuthController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 登录认证 —— [ LoginAuthController.java ]\n</a>\n<a class=\"dt-btn\" href=\"https://www.wenjuan.ltd/s/UZBZJvb2ej/\" target=\"_blank\">本章小练习：Sa-Token 基础 - 登录认证，章节测试</a>\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "sa-token-doc/use/route-check.md",
    "content": "# 路由拦截鉴权\n\n假设我们有如下需求：<u>*项目中所有接口均需要登录校验，只有 “登录接口” 本身对外开放*</u>。\n\n如果给每个接口都手动加上注解鉴权，将会是一件比较麻烦的事情，这时候使用拦截器鉴权模式将大大降低我们的代码量。\n\n<!-- ![基础-拦截器鉴权.svg](../big-file/use/use-route-check.svg 'w-100') -->\n\n<img class=\"w-100\" src=\"/big-file/doc/use/use-route-check.svg\" />\n\n如上图所示，拦截器将拦截除登录以外的所以请求，并进行一道前置审核决定是否通过。\n\n--- \n\n\n### 1、注册 Sa-Token 路由拦截器\n以`SpringBoot2.0`为例，新建配置类`SaTokenConfigure.java`\n``` java \n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t// 注册拦截器\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器，校验规则为 StpUtil.checkLogin() 登录校验。\n\t\tregistry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))\n\t\t\t\t.addPathPatterns(\"/**\")\n\t\t\t\t.excludePathPatterns(\"/user/doLogin\"); \n\t}\n}\n```\n以上代码，我们注册了一个基于 `StpUtil.checkLogin()` 的登录校验拦截器，并且排除了`/user/doLogin`接口用来开放登录（除了`/user/doLogin`以外的所有接口都需要登录才能访问）。\n\n> [!WARNING| label:版本升级] \n> `SaInterceptor` 是新版本提供的拦截器，点此 [查看旧版本代码迁移示例](https://blog.csdn.net/shengzhang_/article/details/126458949)。\n\n### 2、校验函数详解  \n自定义认证规则：`new SaInterceptor(handle -> StpUtil.checkLogin())` 是最简单的写法，代表只进行登录校验功能。\n\n我们可以往构造函数塞一个完整的 lambda 函数，来定义详细的校验规则，例如：\n\n``` java \n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册 Sa-Token 拦截器，定义详细认证规则 \n\t\tregistry.addInterceptor(new SaInterceptor(handler -> {\n\t\t\t// 指定一条 match 规则\n\t\t\tSaRouter\n\t\t\t\t.match(\"/**\")\t// 拦截的 path 列表，可以写多个 */\n\t\t\t\t.notMatch(\"/user/doLogin\")\t\t// 排除掉的 path 列表，可以写多个 \n\t\t\t\t.check(r -> StpUtil.checkLogin());\t\t// 要执行的校验动作，可以写完整的 lambda 表达式\n\t\t\t\t\n\t\t\t// 根据路由划分模块，不同模块不同鉴权 \n\t\t\tSaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n\t\t\tSaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n\t\t\tSaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n\t\t\tSaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n\t\t\tSaRouter.match(\"/notice/**\", r -> StpUtil.checkPermission(\"notice\"));\n\t\t\tSaRouter.match(\"/comment/**\", r -> StpUtil.checkPermission(\"comment\"));\n\t\t})).addPathPatterns(\"/**\");\n\t}\n}\n```\n\nSaRouter.match() 匹配函数有两个参数：\n- 参数一：要匹配的path路由。\n- 参数二：要执行的校验函数。\n\n在校验函数内不只可以使用 `StpUtil.checkPermission(\"xxx\")` 进行权限校验，你还可以写任意代码，例如：\n\n``` java \n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n\t// 注册 Sa-Token 的拦截器\n\t@Override\n\tpublic void addInterceptors(InterceptorRegistry registry) {\n\t\t// 注册路由拦截器，自定义认证规则 \n\t\tregistry.addInterceptor(new SaInterceptor(handler -> {\n\t\t\t\n\t\t\t// 登录校验 -- 拦截所有路由，并排除/user/doLogin 用于开放登录 \n\t\t\tSaRouter.match(\"/**\", \"/user/doLogin\", r -> StpUtil.checkLogin());\n\n\t\t\t// 角色校验 -- 拦截以 admin 开头的路由，必须具备 admin 角色或者 super-admin 角色才可以通过认证 \n\t\t\tSaRouter.match(\"/admin/**\", r -> StpUtil.checkRoleOr(\"admin\", \"super-admin\"));\n\n\t\t\t// 权限校验 -- 不同模块校验不同权限 \n\t\t\tSaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n\t\t\tSaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n\t\t\tSaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n\t\t\tSaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n\t\t\tSaRouter.match(\"/notice/**\", r -> StpUtil.checkPermission(\"notice\"));\n\t\t\tSaRouter.match(\"/comment/**\", r -> StpUtil.checkPermission(\"comment\"));\n\t\t\t\n\t\t\t// 甚至你可以随意的写一个打印语句\n\t\t\tSaRouter.match(\"/**\", r -> System.out.println(\"----啦啦啦----\"));\n\n            // 连缀写法\n            SaRouter.match(\"/**\").check(r -> System.out.println(\"----啦啦啦----\"));\n\t\t\t\n\t\t})).addPathPatterns(\"/**\");\n\t}\n}\n```\n\n\n### 3、匹配特征详解\n\n除了上述示例的 path 路由匹配，还可以根据很多其它特征进行匹配，以下是所有可匹配的特征：\n\n``` java\n// 基础写法样例：匹配一个path，执行一个校验函数 \nSaRouter.match(\"/user/**\").check(r -> StpUtil.checkLogin());\n\n// 根据 path 路由匹配   ——— 支持写多个path，支持写 restful 风格路由 \n// 功能说明: 使用 /user , /goods 或者 /art/get 开头的任意路由都将进入 check 方法\nSaRouter.match(\"/user/**\", \"/goods/**\", \"/art/get/{id}\").check( /* 要执行的校验函数 */ );\n\n// 根据 path 路由排除匹配 \n// 功能说明: 使用 .html , .css 或者 .js 结尾的任意路由都将跳过, 不会进入 check 方法\nSaRouter.match(\"/**\").notMatch(\"*.html\", \"*.css\", \"*.js\").check( /* 要执行的校验函数 */ );\n\n// 根据请求类型匹配 \nSaRouter.match(SaHttpMethod.GET).check( /* 要执行的校验函数 */ );\n\n// 根据一个 boolean 条件进行匹配 \nSaRouter.match( StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );\n\n// 根据一个返回 boolean 结果的lambda表达式匹配 \nSaRouter.match( r -> StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );\n\n// 多个条件一起使用 \n// 功能说明: 必须是 Get 请求 并且 请求路径以 `/user/` 开头 \nSaRouter.match(SaHttpMethod.GET).match(\"/user/**\").check( /* 要执行的校验函数 */ );\n\n// 可以无限连缀下去 \n// 功能说明: 同时满足 Get 方式请求, 且路由以 /admin 开头, 路由中间带有 /send/ 字符串, 路由结尾不能是 .js 和 .css\nSaRouter\n\t.match(SaHttpMethod.GET)\n\t.match(\"/admin/**\")\n\t.match(\"/**/send/**\") \n\t.notMatch(\"/**/*.js\")\n\t.notMatch(\"/**/*.css\")\n\t// ....\n\t.check( /* 只有上述所有条件都匹配成功，才会执行最后的check校验函数 */ );\n```\n\n\n\n### 4、提前退出匹配链 \n使用 `SaRouter.stop()` 可以提前退出匹配链，例：\n\n``` java\nregistry.addInterceptor(new SaInterceptor(handler -> {\n\tSaRouter.match(\"/**\").check(r -> System.out.println(\"进入1\"));\n\tSaRouter.match(\"/**\").check(r -> System.out.println(\"进入2\")).stop();\n\tSaRouter.match(\"/**\").check(r -> System.out.println(\"进入3\"));\n\tSaRouter.match(\"/**\").check(r -> System.out.println(\"进入4\"));\n\tSaRouter.match(\"/**\").check(r -> System.out.println(\"进入5\"));\n})).addPathPatterns(\"/**\");\n```\n如上示例，代码运行至第2条匹配链时，会在stop函数处提前退出整个匹配函数，从而忽略掉剩余的所有match匹配 \n\n除了`stop()`函数，`SaRouter`还提供了 `back()` 函数，用于：停止匹配，结束执行，直接向前端返回结果\n``` java\n// 执行back函数后将停止匹配，也不会进入Controller，而是直接将 back参数 作为返回值输出到前端\nSaRouter.match(\"/user/back\").back(\"要返回到前端的内容\");\n```\n\nstop() 与 back() 函数的区别在于：\n- `SaRouter.stop()` 会停止匹配，进入Controller。\n- `SaRouter.back()` 会停止匹配，直接返回结果到前端。\n\n\n### 5、使用free打开一个独立的作用域\n\n``` java\n// 进入 free 独立作用域 \nSaRouter.match(\"/**\").free(r -> {\n\tSaRouter.match(\"/a/**\").check(/* --- */);\n\tSaRouter.match(\"/b/**\").check(/* --- */).stop();\n\tSaRouter.match(\"/c/**\").check(/* --- */);\n});\n// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配 \nSaRouter.match(\"/**\").check(/* --- */);\n```\n\nfree() 的作用是：打开一个独立的作用域，使内部的 stop() 不再一次性跳出整个 Auth 函数，而是仅仅跳出当前 free 作用域。\n\n\n### 6、使用注解忽略掉路由拦截校验\n\n我们可以使用 `@SaIgnore` 注解，忽略掉路由拦截认证：\n\n1、先配置好了拦截规则：\n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handler -> {\n\t\t// 根据路由划分模块，不同模块不同鉴权 \n\t\tSaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n\t\tSaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n\t\tSaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n\t\t// ... \n\t})).addPathPatterns(\"/**\");\n}\n```\n\n2、然后在 `Controller` 里又添加了忽略校验的注解\n``` java\n@SaIgnore\n@RequestMapping(\"/user/getList\")\npublic SaResult getList() {\n\tSystem.out.println(\"------------ 访问进来方法\"); \n\treturn SaResult.ok(); \n}\n```\n\n请求将会跳过拦截器的校验，直接进入 Controller 的方法中。\n\n> [!WARNING| label:注意点] \n> 注解 `@SaIgnore` 的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效，对自定义拦截器与过滤器不生效。\n\n\n\n\n### 7、关闭注解校验\n\n`SaInterceptor` 只要注册到项目中，默认就会打开注解校验，如果要关闭此能力，需要指定 `isAnnotation` 为 false：\n\n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(\n\t\tnew SaInterceptor(handle -> {\n\t\t\tSaRouter.match(\"/**\").check(r -> StpUtil.checkLogin());\n\t\t}).isAnnotation(false)  // 指定关闭掉注解鉴权能力，这样框架就只会做路由拦截校验了 \n\t).addPathPatterns(\"/**\");\n}\n```\n\n\n你也可以使用 `setBeforeAuth` 注册认证前置函数：\n\n``` java\n@Override\npublic void addInterceptors(InterceptorRegistry registry) {\n\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\tSystem.out.println(1);\n\t})\n\t.setBeforeAuth(handle -> {\n\t\tSystem.out.println(2);\n\t})\n\t).addPathPatterns(\"/**\");\n}\n```\n\n如上代码，先执行 2，再执行注解鉴权，再执行 1，如果 beforeAuth 里包含 `SaRouter.stop()` 将跳过后续的注解鉴权和 auth 认证环节。\n\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/SaTokenConfigure.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token 路由拦截鉴权 —— [ SaTokenConfigure.java ]\n</a>\n<a class=\"dt-btn\" href=\"https://www.wenjuan.ltd/s/rY7VFv/\" target=\"_blank\">本章小练习：Sa-Token 基础 - 路由拦截鉴权，章节测试</a>\n\n\n"
  },
  {
    "path": "sa-token-doc/use/session.md",
    "content": "# Session会话\n\n--- \n\n### 1、Session是什么？\n\nSession 是会话中专业的数据缓存组件，通过 Session 我们可以很方便的缓存一些高频读写数据，提高程序性能，例如：\n\n``` java\n// 在登录时缓存 user 对象 \nStpUtil.getSession().set(\"user\", user);\n\n// 然后我们就可以在任意处使用这个 user 对象\nSysUser user = (SysUser) StpUtil.getSession().get(\"user\");\n```\n\n在 Sa-Token 中，Session 分为三种，分别是：\n\n- `Account-Session`: 指的是框架为每个 账号id 分配的 Session \n- `Token-Session`: 指的是框架为每个 token 分配的 Session  \n- `Custom-Session`: 指的是以一个 特定的值 作为SessionId，来分配的 Session \n\n> [!TIP| style:callout] \n> 有关 Account-Session 与 Token-Session 的详细区别，可参考：[Session模型详解](/fun/session-model)\n\n\n### 2、Account-Session\n有关 账号-Session 的 API 如下：\n``` java\n// 获取当前账号 id 的 Account-Session (必须是登录后才能调用)\nStpUtil.getSession();\n\n// 获取当前账号 id 的 Account-Session, 并决定在 Session 尚未创建时，是否新建并返回\nStpUtil.getSession(true);\n\n// 获取账号 id 为 10001 的 Account-Session\nStpUtil.getSessionByLoginId(10001);\n\n// 获取账号 id 为 10001 的 Account-Session, 并决定在 Session 尚未创建时，是否新建并返回\nStpUtil.getSessionByLoginId(10001, true);\n\n// 获取 SessionId 为 xxxx-xxxx 的 Account-Session, 在 Session 尚未创建时, 返回 null \nStpUtil.getSessionBySessionId(\"xxxx-xxxx\");\n```\n\n\n### 3、Token-Session\n有关 令牌-Session 的 API 如下：\n``` java\n// 获取当前 Token 的 Token-Session 对象\nStpUtil.getTokenSession();\n\n// 获取指定 Token 的 Token-Session 对象\nStpUtil.getTokenSessionByToken(token);\n```\n\n\n### 4、Custom-Session\n自定义 Session 指的是以一个`特定的值`作为 SessionId 来分配的`Session`, 借助自定义Session，你可以为系统中的任意元素分配相应的session<br>\n例如以商品 id 作为 key 为每个商品分配一个Session，以便于缓存和商品相关的数据，其相关API如下：\n``` java\n// 查询指定key的Session是否存在\nSaSessionCustomUtil.isExists(\"goods-10001\");\n\n// 获取指定key的Session，如果没有，则新建并返回\nSaSessionCustomUtil.getSessionById(\"goods-10001\");\n\n// 获取指定key的Session，如果没有，第二个参数决定是否新建并返回  \nSaSessionCustomUtil.getSessionById(\"goods-10001\", false);   \n\n// 删除指定key的Session\nSaSessionCustomUtil.deleteSessionById(\"goods-10001\");\n```\n\n\n### 5、在 Session 上存取值\n\n以上三种 Session 均为框架设计概念上的区分，实际上在获取它们时，返回的都是 SaSession 对象，你可以使用以下 API 在 SaSession 对象上存取值：\n\n``` java\n// 写值 \nsession.set(\"name\", \"zhang\"); \n\n// 写值 (只有在此key原本无值的时候才会写入)\nsession.setDefaultValue(\"name\", \"zhang\");\n\n// 取值\nsession.get(\"name\");\n\n// 取值 (指定默认值)\nsession.get(\"name\", \"<defaultValue>\"); \n\n// 取值 (若无值则执行参数方法, 之后将结果保存到此键名下,并返回此结果   若有值则直接返回, 无需执行参数方法)\nsession.get(\"name\", () -> {\n            return ...;\n        });\n\n// ---------- 数据类型转换： ----------\nsession.getInt(\"age\");         // 取值 (转int类型)\nsession.getLong(\"age\");        // 取值 (转long类型)\nsession.getString(\"name\");     // 取值 (转String类型)\nsession.getDouble(\"result\");   // 取值 (转double类型)\nsession.getFloat(\"result\");    // 取值 (转float类型)\nsession.getModel(\"key\", Student.class);     // 取值 (指定转换类型)\nsession.getModel(\"key\", Student.class, <defaultValue>);  // 取值 (指定转换类型, 并指定值为Null时返回的默认值)\n\n// 是否含有某个key (返回 true 或 false)\nsession.has(\"key\"); \n\n// 删值 \nsession.delete('name');          \n\n// 清空所有值 \nsession.clear();                 \n\n// 获取此 Session 的所有key (返回Set<String>)\nsession.keys();      \n```\n\n\n### 6、其它操作\n\n``` java\n// 返回此 Session 的id \nsession.getId();                          \n\n// 返回此 Session 的创建时间 (时间戳) \nsession.getCreateTime();                  \n\n// 返回此 Session 会话上的底层数据对象（如果更新map里的值，请调用session.update()方法避免产生脏数据）\nsession.getDataMap();                     \n\n// 将这个 Session 从持久库更新一下\nsession.update();                         \n\n// 注销此 Session 会话 (从持久库删除此Session)\nsession.logout();                         \n```\n\n\n### 7、避免与 HttpSession 混淆使用\n经常有同学会把 `SaSession` 与 `HttpSession` 进行混淆，例如：\n``` java\n@PostMapping(\"/resetPoints\")\npublic void reset(HttpSession session) {\n\t// 在 HttpSession 上写入一个值 \n    session.setAttribute(\"name\", 66);\n\t// 在 SaSession 进行取值\n    System.out.println(StpUtil.getSession().get(\"name\"));\t// 输出null\n}\n```\n**要点：**\n1. `SaSession` 与 `HttpSession` 没有任何关系，在`HttpSession`上写入的值，在`SaSession`中无法取出\n2. `HttpSession`并未被框架接管，在使用Sa-Token时，请在任何情况下均使用`SaSession`，不要使用`HttpSession` \n\n\n### 8、未登录场景下获取 Token-Session \n\n默认场景下，只有登录后才能通过 `StpUtil.getTokenSession()` 获取 `Token-Session`。\n\n如果想要在未登录场景下获取 Token-Session ，有两种方法：\n\n- 方法一：将全局配置项 `tokenSessionCheckLogin` 改为 false，详见：[框架配置](/use/config?id=所有可配置项)\n- 方法二：使用匿名 Token-Session\n\n``` java\n// 获取当前 Token 的匿名 Token-Session （可在未登录情况下使用的 Token-Session）\nStpUtil.getAnonTokenSession();\n```\n\n注意点：如果前端没有提交 Token ，或者提交的 Token 是一个无效 Token 的话，框架将不会根据此 Token 创建 `Token-Session` 对象，\n而是随机一个新的 Token 值来创建 `Token-Session` 对象，此 Token 值可以通过 `StpUtil.getTokenValue()` 获取到。\n\n\n---\n\n<a class=\"case-btn\" href=\"https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/use/SaSessionController.java\"\n\ttarget=\"_blank\">\n\t本章代码示例：Sa-Token Session 会话 —— [ SaSessionController.java ]\n</a>\n<a class=\"dt-btn\" href=\"https://www.wenjuan.ltd/s/MNnUr2V/\" target=\"_blank\">本章小练习：Sa-Token 基础 - Session 会话，章节测试</a>\n\n"
  },
  {
    "path": "sa-token-plugin/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n   \n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-parent</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>pom</packaging>\n    \n\t<name>sa-token-plugin</name>\n    <artifactId>sa-token-plugin</artifactId>\n\t<description>sa-token plugins</description>\n\t\n\t<!-- 所有子模块 -->\n    <modules>\n        <!-- 通用插件 -->\n        <module>sa-token-jackson</module>\n        <module>sa-token-jackson3</module>\n        <module>sa-token-fastjson</module>\n        <module>sa-token-fastjson2</module>\n        <module>sa-token-snack3</module>\n        <module>sa-token-snack4</module>\n        <module>sa-token-hutool-timed-cache</module>\n        <module>sa-token-caffeine</module>\n        <module>sa-token-thymeleaf</module>\n        <module>sa-token-freemarker</module>\n        <module>sa-token-dubbo</module>\n        <module>sa-token-dubbo3</module>\n        <module>sa-token-temp-jwt</module>\n        <module>sa-token-jwt</module>\n        <module>sa-token-sso</module>\n        <module>sa-token-oauth2</module>\n        <module>sa-token-apikey</module>\n        <module>sa-token-sign</module>\n        <module>sa-token-redisson</module>\n        <module>sa-token-redisx</module>\n        <module>sa-token-serializer-features</module>\n        <module>sa-token-forest</module>\n        <module>sa-token-okhttps</module>\n\n        <!-- SpringBoot 环境插件 -->\n        <module>sa-token-redis-template</module>\n        <module>sa-token-redis-template-jdk-serializer</module>\n        <module>sa-token-redis-jackson</module>\n        <module>sa-token-alone-redis</module>\n        <module>sa-token-alone-redis-by-spring-boot4</module>\n        <module>sa-token-spring-aop</module>\n        <module>sa-token-spring-el</module>\n        <module>sa-token-grpc</module>\n        <module>sa-token-quick-login</module>\n        <module>sa-token-redisson-spring-boot-starter</module>\n    </modules>\n\n    <dependencyManagement>\n        <dependencies>\n\n            <!-- 默认使用 sa-token springboot2 相关依赖版本定义，子模块也可以引入其它的进行覆盖 -->\n            <dependency>\n                <groupId>cn.dev33</groupId>\n                <artifactId>sa-token-spring-boot2-dependencies</artifactId>\n                <version>${revision}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-alone-redis/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-alone-redis</name>\n    <artifactId>sa-token-alone-redis</artifactId>\n\t<description>sa-token-alone-redis</description>\n\n\t<dependencies>\n\t\t<!-- Sa-Token Redis Dependency -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n\t\t\t<optional>true</optional>\n        </dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template-jdk-serializer</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n        <!-- redis pool -->\n\t\t<dependency>\n\t\t    <groupId>org.apache.commons</groupId>\n\t\t    <artifactId>commons-pool2</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.alone;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoDefaultImpl;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport org.apache.commons.pool2.impl.GenericObjectPoolConfig;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.data.redis.RedisProperties;\nimport org.springframework.boot.autoconfigure.data.redis.RedisProperties.Lettuce;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.context.properties.bind.Binder;\nimport org.springframework.context.EnvironmentAware;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.env.Environment;\nimport org.springframework.data.redis.connection.*;\nimport org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;\nimport org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;\nimport org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * 为 SaToken 单独设置 Redis 连接信息，使权限缓存与业务缓存分离\n *\n * <p>\n *     使用方式：在引入 sa-token redis 集成相关包的前提下，继续引入当前依赖 <br> <br>\n *     注意事项：目前本依赖仅对以下插件有 Redis 分离效果： <br>\n *     sa-token-redis-template  <br>\n *     sa-token-redis-template-jdk-serializer  <br>\n * </p>\n *\n *\n * @author click33\n * @since 1.21.0\n */\n@Configuration\npublic class SaAloneRedisInject implements EnvironmentAware{\n\n\t/**\n\t * 配置信息的前缀 \n\t */\n\tpublic static final String ALONE_PREFIX = \"sa-token.alone-redis\";\n\t\n\t/**\n\t * Sa-Token 持久层接口 \n\t */\n\t@Autowired(required = false)\n\tpublic SaTokenDao saTokenDao;\n\t\n\t/**\n\t * 开始注入 \n\t */\n\t@Override\n\tpublic void setEnvironment(Environment environment) {\n\t\ttry {\n\t\t\t// 如果 saTokenDao 为空或者为默认实现，则不进行任何操作\n\t\t\tif(saTokenDao == null || saTokenDao instanceof SaTokenDaoDefaultImpl) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t\n\t\t\t// ------------------- 开始注入 \n\t\t\t\n\t\t\t// 获取cfg对象，解析开发者配置的 sa-token.alone-redis 相关信息\n\t\t\tRedisProperties cfg = Binder.get(environment).bind(ALONE_PREFIX, RedisProperties.class).get();\n\n\t\t\t// 1. Redis配置\n\t\t\tRedisConfiguration redisAloneConfig;\n\t\t\tString pattern = environment.getProperty(ALONE_PREFIX + \".pattern\", \"single\");\n\t\t\tif (pattern.equals(\"single\")) {\n\t\t\t\t// 单体模式\n\t\t\t\tRedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();\n\t\t\t\tredisConfig.setHostName(cfg.getHost());\n\t\t\t\tredisConfig.setPort(cfg.getPort());\n\t\t\t\tredisConfig.setDatabase(cfg.getDatabase());\n\t\t\t\tredisConfig.setPassword(RedisPassword.of(cfg.getPassword()));\n\t\t\t\tredisConfig.setDatabase(cfg.getDatabase());\n\t\t\t\t// 低版本没有 username 属性，捕获异常给个提示即可，无需退出程序\n\t\t\t\ttry {\n\t\t\t\t\tredisConfig.setUsername(cfg.getUsername());\n\t\t\t\t} catch (NoSuchMethodError e){\n\t\t\t\t\tSystem.err.println(e.getMessage());\n\t\t\t\t}\n\t\t\t\tredisAloneConfig = redisConfig;\n\n\t\t\t} else if (pattern.equals(\"cluster\")){\n\t\t\t\t// 普通集群模式\n\t\t\t\tRedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration();\n\t\t\t\t// 低版本没有 username 属性，捕获异常给个提示即可，无需退出程序\n\t\t\t\ttry {\n\t\t\t\t\tredisClusterConfig.setUsername(cfg.getUsername());\n\t\t\t\t} catch (NoSuchMethodError e){\n\t\t\t\t\tSystem.err.println(e.getMessage());\n\t\t\t\t}\n\t\t\t\tredisClusterConfig.setPassword(RedisPassword.of(cfg.getPassword()));\n\n\t\t\t\tRedisProperties.Cluster cluster = cfg.getCluster();\n\t\t\t\tList<RedisNode> serverList = cluster.getNodes().stream().map(node -> {\n\t\t\t\t\tString[] ipAndPort = node.split(\":\");\n\t\t\t\t\treturn new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1]));\n\t\t\t\t}).collect(Collectors.toList());\n\t\t\t\tredisClusterConfig.setClusterNodes(serverList);\n\t\t\t\tredisClusterConfig.setMaxRedirects(cluster.getMaxRedirects());\n\n\t\t\t\tredisAloneConfig = redisClusterConfig;\n\t\t\t} else if (pattern.equals(\"sentinel\")) {\n\t\t\t\t// 哨兵集群模式\n\t\t\t\tRedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();\n\t\t\t\tredisSentinelConfiguration.setDatabase(cfg.getDatabase());\n\t\t\t\t// 低版本没有 username 属性，捕获异常给个提示即可，无需退出程序\n\t\t\t\ttry {\n\t\t\t\t\tredisSentinelConfiguration.setUsername(cfg.getUsername());\n\t\t\t\t} catch (NoSuchMethodError e){\n\t\t\t\t\tSystem.err.println(e.getMessage());\n\t\t\t\t}\n\t\t\t\tredisSentinelConfiguration.setPassword(RedisPassword.of(cfg.getPassword()));\n\n\t\t\t\tRedisProperties.Sentinel sentinel = cfg.getSentinel();\n\t\t\t\tredisSentinelConfiguration.setMaster(sentinel.getMaster());\n\t\t\t\tredisSentinelConfiguration.setSentinelPassword(sentinel.getPassword());\n\t\t\t\tList<RedisNode> serverList = sentinel.getNodes().stream().map(node -> {\n\t\t\t\t\tString[] ipAndPort = node.split(\":\");\n\t\t\t\t\treturn new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1]));\n\t\t\t\t}).collect(Collectors.toList());\n\t\t\t\tredisSentinelConfiguration.setSentinels(serverList);\n\n\t\t\t\tredisAloneConfig = redisSentinelConfiguration;\n\t\t\t} else if (pattern.equals(\"socket\")) {\n\t\t\t\t// socket 连接单体 Redis\n\t\t\t\tRedisSocketConfiguration redisSocketConfiguration = new RedisSocketConfiguration();\n\t\t\t\tredisSocketConfiguration.setDatabase(cfg.getDatabase());\n\t\t\t\t// 低版本没有 username 属性，捕获异常给个提示即可，无需退出程序\n\t\t\t\ttry {\n\t\t\t\t\tredisSocketConfiguration.setUsername(cfg.getUsername());\n\t\t\t\t} catch (NoSuchMethodError e){\n\t\t\t\t\tSystem.err.println(e.getMessage());\n\t\t\t\t}\n\t\t\t\tredisSocketConfiguration.setPassword(RedisPassword.of(cfg.getPassword()));\n\t\t\t\tString socket = environment.getProperty(ALONE_PREFIX + \".socket\", \"\");\n\t\t\t\tredisSocketConfiguration.setSocket(socket);\n\n\t\t\t\tredisAloneConfig = redisSocketConfiguration;\n\t\t\t} else if (pattern.equals(\"aws\")) {\n\t\t\t\t// AWS ElastiCache\n\t\t\t\t// AWS Redis 远程主机地址: String hoseName = \"****.***.****.****.cache.amazonaws.com\";\n\t\t\t\tString hostName = cfg.getHost();\n\t\t\t\tint port = cfg.getPort();\n\t\t\t\tRedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(hostName, port);\n\t\t\t\tredisStaticMasterReplicaConfiguration.setDatabase(cfg.getDatabase());\n\t\t\t\t// 低版本没有 username 属性，捕获异常给个提示即可，无需退出程序\n\t\t\t\ttry {\n\t\t\t\t\tredisStaticMasterReplicaConfiguration.setUsername(cfg.getUsername());\n\t\t\t\t} catch (NoSuchMethodError e){\n\t\t\t\t\tSystem.err.println(e.getMessage());\n\t\t\t\t}\n\t\t\t\tredisStaticMasterReplicaConfiguration.setPassword(RedisPassword.of(cfg.getPassword()));\n\n\t\t\t\tredisAloneConfig = redisStaticMasterReplicaConfiguration;\n\t\t\t} else {\n\t\t\t\t// 模式无法识别\n\t\t\t\tthrow new SaTokenException(\"SaToken 无法识别 Alone-Redis 配置的模式: \" + pattern);\n\t\t\t}\n\n\t\t\t// 2. 连接池配置 \n\t\t\tGenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();\n\t\t\t// pool配置 \n\t\t\tLettuce lettuce = cfg.getLettuce();\n\t\t\tif(lettuce.getPool() != null) {\n\t\t\t\tRedisProperties.Pool pool = cfg.getLettuce().getPool();\n\t\t\t\t// 连接池最大连接数\n\t\t\t\tpoolConfig.setMaxTotal(pool.getMaxActive());\t\n\t\t\t\t// 连接池中的最大空闲连接 \n\t\t\t\tpoolConfig.setMaxIdle(pool.getMaxIdle());   \t\n\t\t\t\t// 连接池中的最小空闲连接\n\t\t\t\tpoolConfig.setMinIdle(pool.getMinIdle());\t\t\n\t\t\t\t// 连接池最大阻塞等待时间（使用负值表示没有限制）\n\t\t\t\tpoolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis());\n\t\t\t}\n\t\t\tLettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();\n\t\t\t// timeout \n\t\t\tif(cfg.getTimeout() != null) {\n\t\t\t\tbuilder.commandTimeout(cfg.getTimeout());\n\t\t\t}\n\t\t\t// shutdownTimeout \n\t\t\tif(lettuce.getShutdownTimeout() != null) {\n\t\t\t\tbuilder.shutdownTimeout(lettuce.getShutdownTimeout());\n\t\t\t}\n\t\t\t// 创建Factory对象\n\t\t\tLettuceClientConfiguration clientConfig = builder.poolConfig(poolConfig).build();\n\t\t\tLettuceConnectionFactory factory = new LettuceConnectionFactory(redisAloneConfig, clientConfig);\n\t\t\tfactory.afterPropertiesSet();\n\t\t\t\n\t\t\t// 3. 开始初始化 SaTokenDao ，此处需要依次判断开发者引入的是哪个 redis 库\n\n\t\t\t// 如果开发者引入的是：sa-token-redis-template-jdk-serializer\n\t\t\ttry {\n\t\t\t\tClass.forName(\"cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer\");\n\t\t\t\tSaTokenDaoForRedisTemplateUseJdkSerializer dao = (SaTokenDaoForRedisTemplateUseJdkSerializer)saTokenDao;\n\t\t\t\tdao.isInit = false;\n\t\t\t\tdao.init(factory);\n\t\t\t\treturn;\n\t\t\t} catch (ClassNotFoundException ignored) {\n\t\t\t}\n\t\t\t// 如果开发者引入的是：sa-token-redis-template\n\t\t\ttry {\n\t\t\t\tClass.forName(\"cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate\");\n\t\t\t\tSaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate)saTokenDao;\n\t\t\t\tdao.isInit = false;\n\t\t\t\tdao.init(factory);\n\t\t\t\treturn;\n\t\t\t} catch (ClassNotFoundException ignored) {\n\t\t\t}\n\n\t\t\t// 至此，说明开发者一个 redis 插件也没引入，或者引入的 redis 插件不在 sa-token-alone-redis 的支持范围内\n\t\t\tthrow new SaTokenException(\"未引入 sa-token-redis-xxx 相关插件，或引入的插件不在 Alone-Redis 支持范围内\");\n\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\t\n\t/**\n\t * 骗过编辑器，增加配置文件代码提示 \n\t * @return 配置对象\n\t */\n\t@ConfigurationProperties(prefix = ALONE_PREFIX)\n\tpublic RedisProperties getSaAloneRedisConfig() {\n\t\treturn new RedisProperties();\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-alone-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.dao.alone.SaAloneRedisInject"
  },
  {
    "path": "sa-token-plugin/sa-token-alone-redis/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.alone.SaAloneRedisInject"
  },
  {
    "path": "sa-token-plugin/sa-token-alone-redis-by-spring-boot4/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-alone-redis-by-spring-boot4</name>\n    <artifactId>sa-token-alone-redis-by-spring-boot4</artifactId>\n\t<description>sa-token-alone-redis for Spring Boot 4</description>\n\n\t<dependencies>\n\t\t<!-- Spring Boot 4 Data Redis (provides DataRedisProperties) -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-data-redis</artifactId>\n\t\t</dependency>\n\t\t<!-- Sa-Token Redis Dependency -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redis-template</artifactId>\n\t\t\t<optional>true</optional>\n        </dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template-jdk-serializer</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n        <!-- redis pool -->\n\t\t<dependency>\n\t\t    <groupId>org.apache.commons</groupId>\n\t\t    <artifactId>commons-pool2</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot4-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\t\t</dependencies>\n\t</dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao.alone;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoDefaultImpl;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport jakarta.annotation.PostConstruct;\nimport org.apache.commons.pool2.impl.GenericObjectPoolConfig;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.boot.context.properties.bind.Binder;\nimport org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.env.Environment;\nimport org.springframework.data.redis.connection.*;\nimport org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;\nimport org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;\nimport org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * 为 SaToken 单独设置 Redis 连接信息，使权限缓存与业务缓存分离 (springboot4 版本专用) \n *\n * <p>\n *     使用方式：在引入 sa-token redis 集成相关包的前提下，继续引入当前依赖 <br> <br>\n *     注意事项：目前本依赖仅对以下插件有 Redis 分离效果： <br>\n *     sa-token-redis-template  <br>\n *     sa-token-redis-template-jdk-serializer  <br>\n * </p>\n *\n *\n * @author click33\n * @since 1.45.0\n */\n@Configuration\npublic class SaAloneRedisInject {\n\n\t/**\n\t * 配置信息的前缀\n\t */\n\tpublic static final String ALONE_PREFIX = \"sa-token.alone-redis\";\n\n\t/**\n\t * Sa-Token 持久层接口\n\t */\n\tprivate final SaTokenDao saTokenDao;\n\tprivate final Environment environment;\n\n\tpublic SaAloneRedisInject(SaTokenDao saTokenDao, Environment environment) {\n\t\tthis.saTokenDao = saTokenDao;\n\t\tthis.environment = environment;\n\t}\n\n\t/**\n\t * 开始注入\n\t */\n\t@PostConstruct\n\tpublic void init() {\n\t\ttry {\n\t\t\t// 如果 saTokenDao 为空或者为默认实现，则不进行任何操作\n\t\t\tif(saTokenDao == null || saTokenDao instanceof SaTokenDaoDefaultImpl) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// ------------------- 开始注入\n\n\t\t\t// 获取cfg对象，解析开发者配置的 sa-token.alone-redis 相关信息\n\t\t\tDataRedisProperties cfg = Binder.get(environment).bind(ALONE_PREFIX, DataRedisProperties.class).get();\n\n\t\t\t// 1. Redis配置\n\t\t\tRedisConfiguration redisAloneConfig;\n\t\t\tString pattern = environment.getProperty(ALONE_PREFIX + \".pattern\", \"single\");\n\t\t\tif (pattern.equals(\"single\")) {\n\t\t\t\t// 单体模式\n\t\t\t\tRedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();\n\t\t\t\tredisConfig.setHostName(cfg.getHost());\n\t\t\t\tredisConfig.setPort(cfg.getPort());\n\t\t\t\tredisConfig.setDatabase(cfg.getDatabase());\n\t\t\t\tredisConfig.setPassword(RedisPassword.of(cfg.getPassword()));\n\t\t\t\tredisConfig.setUsername(cfg.getUsername());\n\t\t\t\tredisAloneConfig = redisConfig;\n\n\t\t\t} else if (pattern.equals(\"cluster\")){\n\t\t\t\t// 普通集群模式\n\t\t\t\tRedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration();\n\t\t\t\tredisClusterConfig.setUsername(cfg.getUsername());\n\t\t\t\tredisClusterConfig.setPassword(RedisPassword.of(cfg.getPassword()));\n\n\t\t\t\tDataRedisProperties.Cluster cluster = cfg.getCluster();\n\t\t\t\tif (cluster == null || cluster.getNodes() == null) {\n\t\t\t\t\tthrow new SaTokenException(\"Alone-Redis 集群模式需要配置 cluster.nodes\");\n\t\t\t\t}\n\t\t\t\tList<RedisNode> serverList = cluster.getNodes().stream().map(node -> {\n\t\t\t\t\tString[] ipAndPort = node.split(\":\");\n\t\t\t\t\treturn new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1]));\n\t\t\t\t}).collect(Collectors.toList());\n\t\t\t\tredisClusterConfig.setClusterNodes(serverList);\n\t\t\t\tif (cluster.getMaxRedirects() != null) {\n\t\t\t\t\tredisClusterConfig.setMaxRedirects(cluster.getMaxRedirects());\n\t\t\t\t}\n\t\t\t\tredisAloneConfig = redisClusterConfig;\n\t\t\t} else if (pattern.equals(\"sentinel\")) {\n\t\t\t\t// 哨兵集群模式\n\t\t\t\tRedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();\n\t\t\t\tredisSentinelConfiguration.setDatabase(cfg.getDatabase());\n\t\t\t\tredisSentinelConfiguration.setUsername(cfg.getUsername());\n\t\t\t\tredisSentinelConfiguration.setPassword(RedisPassword.of(cfg.getPassword()));\n\n\t\t\t\tDataRedisProperties.Sentinel sentinel = cfg.getSentinel();\n\t\t\t\tif (sentinel == null || sentinel.getNodes() == null) {\n\t\t\t\t\tthrow new SaTokenException(\"Alone-Redis 哨兵模式需要配置 sentinel.nodes\");\n\t\t\t\t}\n\t\t\t\tredisSentinelConfiguration.setMaster(sentinel.getMaster());\n\t\t\t\tredisSentinelConfiguration.setSentinelPassword(sentinel.getPassword());\n\t\t\t\tList<RedisNode> serverList = sentinel.getNodes().stream().map(node -> {\n\t\t\t\t\tString[] ipAndPort = node.split(\":\");\n\t\t\t\t\treturn new RedisNode(ipAndPort[0].trim(), Integer.parseInt(ipAndPort[1]));\n\t\t\t\t}).collect(Collectors.toList());\n\t\t\t\tredisSentinelConfiguration.setSentinels(serverList);\n\n\t\t\t\tredisAloneConfig = redisSentinelConfiguration;\n\t\t\t} else if (pattern.equals(\"socket\")) {\n\t\t\t\t// socket 连接单体 Redis\n\t\t\t\tRedisSocketConfiguration redisSocketConfiguration = new RedisSocketConfiguration();\n\t\t\t\tredisSocketConfiguration.setDatabase(cfg.getDatabase());\n\t\t\t\tredisSocketConfiguration.setUsername(cfg.getUsername());\n\t\t\t\tredisSocketConfiguration.setPassword(RedisPassword.of(cfg.getPassword()));\n\t\t\t\tString socket = environment.getProperty(ALONE_PREFIX + \".socket\", \"\");\n\t\t\t\tredisSocketConfiguration.setSocket(socket);\n\n\t\t\t\tredisAloneConfig = redisSocketConfiguration;\n\t\t\t} else if (pattern.equals(\"aws\")) {\n\t\t\t\t// AWS ElastiCache\n\t\t\t\t// AWS Redis 远程主机地址: String hoseName = \"****.***.****.****.cache.amazonaws.com\";\n\t\t\t\tString hostName = cfg.getHost();\n\t\t\t\tint port = cfg.getPort();\n\t\t\t\tRedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(hostName, port);\n\t\t\t\tredisStaticMasterReplicaConfiguration.setDatabase(cfg.getDatabase());\n\t\t\t\tredisStaticMasterReplicaConfiguration.setUsername(cfg.getUsername());\n\t\t\t\tredisStaticMasterReplicaConfiguration.setPassword(RedisPassword.of(cfg.getPassword()));\n\n\t\t\t\tredisAloneConfig = redisStaticMasterReplicaConfiguration;\n\t\t\t} else {\n\t\t\t\t// 模式无法识别\n\t\t\t\tthrow new SaTokenException(\"SaToken 无法识别 Alone-Redis 配置的模式: \" + pattern);\n\t\t\t}\n\n\t\t\t// 2. 连接池配置\n\t\t\tGenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();\n\t\t\t// pool配置\n\t\t\tDataRedisProperties.Lettuce lettuce = cfg.getLettuce();\n\t\t\tif(lettuce.getPool() != null) {\n\t\t\t\tDataRedisProperties.Pool pool = cfg.getLettuce().getPool();\n\t\t\t\t// 连接池最大连接数\n\t\t\t\tpoolConfig.setMaxTotal(pool.getMaxActive());\n\t\t\t\t// 连接池中的最大空闲连接\n\t\t\t\tpoolConfig.setMaxIdle(pool.getMaxIdle());\n\t\t\t\t// 连接池中的最小空闲连接\n\t\t\t\tpoolConfig.setMinIdle(pool.getMinIdle());\n\t\t\t\t// 连接池最大阻塞等待时间（使用负值表示没有限制）\n\t\t\t\tpoolConfig.setMaxWaitMillis(pool.getMaxWait().toMillis());\n\t\t\t}\n\t\t\tLettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();\n\t\t\t// timeout\n\t\t\tif(cfg.getTimeout() != null) {\n\t\t\t\tbuilder.commandTimeout(cfg.getTimeout());\n\t\t\t}\n\t\t\t// shutdownTimeout\n\t\t\tbuilder.shutdownTimeout(lettuce.getShutdownTimeout());\n\t\t\t// 创建Factory对象\n\t\t\tLettuceClientConfiguration clientConfig = builder.poolConfig(poolConfig).build();\n\t\t\tLettuceConnectionFactory factory = new LettuceConnectionFactory(redisAloneConfig, clientConfig);\n\t\t\tfactory.afterPropertiesSet();\n\n\t\t\t// 3. 开始初始化 SaTokenDao ，此处需要依次判断开发者引入的是哪个 redis 库\n\n\t\t\t// 如果开发者引入的是：sa-token-redis-template-jdk-serializer 或 sa-token-redis-template\n\t\t\ttry {\n\t\t\t\tClass.forName(\"cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer\");\n\t\t\t\tSaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate) saTokenDao;\n\t\t\t\tdao.isInit = false;\n\t\t\t\tdao.init(factory);\n\t\t\t\treturn;\n\t\t\t} catch (ClassNotFoundException ignored) {\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tClass.forName(\"cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate\");\n\t\t\t\tSaTokenDaoForRedisTemplate dao = (SaTokenDaoForRedisTemplate) saTokenDao;\n\t\t\t\tdao.isInit = false;\n\t\t\t\tdao.init(factory);\n\t\t\t\treturn;\n\t\t\t} catch (ClassNotFoundException ignored) {\n\t\t\t}\n\n\t\t\t// 至此，说明开发者一个 redis 插件也没引入，或者引入的 redis 插件不在 sa-token-alone-redis 的支持范围内\n\t\t\tthrow new SaTokenException(\"未引入 sa-token-redis-xxx 相关插件，或引入的插件不在 Alone-Redis 支持范围内\");\n\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n\t/**\n\t * 骗过编辑器，增加配置文件代码提示\n\t * @return 配置对象\n\t */\n\t@ConfigurationProperties(prefix = ALONE_PREFIX)\n\tpublic DataRedisProperties getSaAloneRedisConfig() {\n\t\treturn new DataRedisProperties();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-alone-redis-by-spring-boot4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.dao.alone.SaAloneRedisInject\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n    <name>sa-token-apikey</name>\n    <artifactId>sa-token-apikey</artifactId>\n    <description>sa-token-apikey</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/SaApiKeyManager.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey;\n\nimport cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader;\nimport cn.dev33.satoken.apikey.loader.SaApiKeyDataLoaderDefaultImpl;\nimport cn.dev33.satoken.apikey.config.SaApiKeyConfig;\nimport cn.dev33.satoken.apikey.template.SaApiKeyTemplate;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\n\n/**\n * 管理 Sa-Token API Key 所有全局组件\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaApiKeyManager {\n\n\t/**\n\t * API Key 配置 Bean\n\t */\n\tprivate static volatile SaApiKeyConfig config;\n\tpublic static SaApiKeyConfig getConfig() {\n\t\tif (config == null) {\n\t\t\t// 初始化默认值\n\t\t\tsynchronized (SaApiKeyManager.class) {\n\t\t\t\tif (config == null) {\n\t\t\t\t\tsetConfig(new SaApiKeyConfig());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn config;\n\t}\n\tpublic static void setConfig(SaApiKeyConfig config) {\n\t\tSaApiKeyManager.config = config;\n\t}\n\n\t/**\n\t * ApiKey 数据加载器\n\t */\n\tprivate volatile static SaApiKeyDataLoader apiKeyDataLoader;\n\tpublic static void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) {\n\t\tSaApiKeyManager.apiKeyDataLoader = apiKeyDataLoader;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaApiKeyDataLoader\", apiKeyDataLoader);\n\t}\n\tpublic static SaApiKeyDataLoader getSaApiKeyDataLoader() {\n\t\tif (apiKeyDataLoader == null) {\n\t\t\tsynchronized (SaApiKeyManager.class) {\n\t\t\t\tif (apiKeyDataLoader == null) {\n\t\t\t\t\tSaApiKeyManager.apiKeyDataLoader = new SaApiKeyDataLoaderDefaultImpl();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn apiKeyDataLoader;\n\t}\n\n\t/**\n\t * ApiKey 操作类\n\t */\n\tprivate volatile static SaApiKeyTemplate apiKeyTemplate;\n\tpublic static void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) {\n\t\tSaApiKeyManager.apiKeyTemplate = apiKeyTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaApiKeyTemplate\", apiKeyTemplate);\n\t}\n\tpublic static SaApiKeyTemplate getSaApiKeyTemplate() {\n\t\tif (apiKeyTemplate == null) {\n\t\t\tsynchronized (SaApiKeyManager.class) {\n\t\t\t\tif (apiKeyTemplate == null) {\n\t\t\t\t\tSaApiKeyManager.apiKeyTemplate = new SaApiKeyTemplate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn apiKeyTemplate;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/SaCheckApiKey.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.annotation;\n\nimport cn.dev33.satoken.annotation.SaMode;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * API Key 校验：指定请求中必须包含有效的 ApiKey ，并且包含指定的 scope\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.42.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface SaCheckApiKey {\n\n\t/**\n\t * 指定 API key 必须包含的权限 [ 数组 ]\n\t *\n\t * @return /\n\t */\n\tString [] scope() default {};\n\n\t/**\n\t * 验证模式：AND | OR，默认AND\n\t *\n\t * @return /\n\t */\n\tSaMode mode() default SaMode.AND;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/annotation/handle/SaCheckApiKeyHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.annotation.handle;\n\nimport cn.dev33.satoken.annotation.SaMode;\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.apikey.annotation.SaCheckApiKey;\nimport cn.dev33.satoken.apikey.template.SaApiKeyUtil;\nimport cn.dev33.satoken.context.SaHolder;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckApiKey 的处理器\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaCheckApiKeyHandler implements SaAnnotationHandlerInterface<SaCheckApiKey> {\n\n    @Override\n    public Class<SaCheckApiKey> getHandlerAnnotationClass() {\n        return SaCheckApiKey.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckApiKey at, AnnotatedElement element) {\n        _checkMethod(at.scope(), at.mode());\n    }\n\n    public static void _checkMethod(String[] scope, SaMode mode) {\n        String apiKey = SaApiKeyUtil.readApiKeyValue(SaHolder.getRequest());\n        if(mode == SaMode.AND) {\n            SaApiKeyUtil.checkApiKeyScope(apiKey, scope);\n        } else {\n            SaApiKeyUtil.checkApiKeyScopeOr(apiKey, scope);\n        }\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/config/SaApiKeyConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.config;\n\n/**\n * Sa-Token API Key 相关配置\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaApiKeyConfig {\n\n\t/**\n\t * API Key 前缀\n\t */\n\tprivate String prefix = \"AK-\";\n\n\t/**\n\t * API Key 有效期，-1=永久有效，默认30天 （修改此配置项不会影响到已创建的 API Key）\n\t */\n\tprivate long timeout = 2592000;\n\n\t/**\n\t * 框架是否记录索引信息\n\t */\n\tprivate Boolean isRecordIndex = true;\n\n\t/**\n\t * 获取 API Key 前缀\n\t *\n\t * @return /\n\t */\n\tpublic String getPrefix() {\n\t\treturn this.prefix;\n\t}\n\n\t/**\n\t * 设置 API Key 前缀\n\t *\n\t * @param prefix /\n\t * @return 对象自身\n\t */\n\tpublic SaApiKeyConfig setPrefix(String prefix) {\n\t\tthis.prefix = prefix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 API Key 有效期，-1=永久有效，默认30天 （修改此配置项不会影响到已创建的 API Key）\n\t *\n\t * @return /\n\t */\n\tpublic long getTimeout() {\n\t\treturn this.timeout;\n\t}\n\n\t/**\n\t * 设置 API Key 有效期，-1=永久有效，默认30天 （修改此配置项不会影响到已创建的 API Key）\n\t *\n\t * @param timeout /\n\t * @return 对象自身\n\t */\n\tpublic SaApiKeyConfig setTimeout(long timeout) {\n\t\tthis.timeout = timeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 框架是否保存索引信息\n\t *\n\t * @return /\n\t */\n\tpublic Boolean getIsRecordIndex() {\n\t\treturn this.isRecordIndex;\n\t}\n\n\t/**\n\t * 设置 框架是否保存索引信息\n\t *\n\t * @param isRecordIndex /\n\t * @return 对象自身\n\t */\n\tpublic SaApiKeyConfig setIsRecordIndex(Boolean isRecordIndex) {\n\t\tthis.isRecordIndex = isRecordIndex;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaApiKeyConfig{\" +\n\t\t\t\t\"prefix='\" + prefix + '\\'' +\n\t\t\t\t\", timeout=\" + timeout +\n\t\t\t\t\", isRecordIndex=\" + isRecordIndex +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/error/SaApiKeyErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.error;\n\n/**\n * 定义 sa-token-apikey 模块所有异常细分状态码\n * \n * @author click33\n * @since 1.43.0\n */\npublic interface SaApiKeyErrorCode {\n\n\t/** 无效 API Key */\n\tint CODE_12301 = 12301;\n\n\t/** API Key 已过期 */\n\tint CODE_12302 = 12302;\n\n\t/** API Key 已被禁用 */\n\tint CODE_12303 = 12303;\n\n\t/** API Key 字段自检未通过 */\n\tint CODE_12304 = 12304;\n\n\t/** 未开启索引记录功能却调用了相关 API */\n\tint CODE_12305 = 12305;\n\n\t/** API Key 不具有指定 Scope */\n\tint CODE_12311 = 12311;\n\n\t/** API Key 不属于指定用户 */\n\tint CODE_12312 = 12312;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.exception;\n\nimport cn.dev33.satoken.exception.SaTokenException;\n\n/**\n * 一个异常：代表 ApiKey 相关错误\n * \n * @author click33\n * @since 1.42.0\n */\npublic class ApiKeyException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 ApiKey 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic ApiKeyException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 ApiKey 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic ApiKeyException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 ApiKey 值\n\t */\n\tpublic String apiKey;\n\n\tpublic String getApiKey() {\n\t\treturn apiKey;\n\t}\n\n\tpublic ApiKeyException setApiKey(String apiKey) {\n\t\tthis.apiKey = apiKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new ApiKeyException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/exception/ApiKeyScopeException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.exception;\n\n/**\n * 一个异常：代表 ApiKey Scope 相关错误\n * \n * @author click33\n * @since 1.42.0\n */\npublic class ApiKeyScopeException extends ApiKeyException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 ApiKey Scope 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic ApiKeyScopeException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 ApiKey Scope 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic ApiKeyScopeException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 ApiKey 值\n\t */\n\tpublic String apiKey;\n\n\t/**\n\t * 具体引起异常的 scope 值\n\t */\n\tpublic String scope;\n\n\tpublic String getApiKey() {\n\t\treturn apiKey;\n\t}\n\n\tpublic ApiKeyScopeException setApiKey(String apiKey) {\n\t\tthis.apiKey = apiKey;\n\t\treturn this;\n\t}\n\n\tpublic String getScope() {\n\t\treturn scope;\n\t}\n\n\tpublic ApiKeyScopeException setScope(String scope) {\n\t\tthis.scope = scope;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new ApiKeyScopeException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoader.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.loader;\n\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.model.ApiKeyModel;\n\n/**\n * ApiKey 数据加载器\n *\n * @author click33\n * @since 1.42.0\n */\npublic interface SaApiKeyDataLoader {\n\n    /**\n     * 获取：框架是否保存索引信息\n     *\n     * @return /\n     */\n    default Boolean getIsRecordIndex() {\n        return SaApiKeyManager.getConfig().getIsRecordIndex();\n    }\n\n    /**\n     * 根据 apiKey 从数据库获取 ApiKeyModel 信息 （实现此方法无需为数据做缓存处理，框架内部已包含缓存逻辑）\n     *\n     * @param namespace /\n     * @param apiKey /\n     * @return ApiKeyModel\n     */\n    default ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/loader/SaApiKeyDataLoaderDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.loader;\n\n/**\n * ApiKey 数据加载器 默认实现类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaApiKeyDataLoaderDefaultImpl implements SaApiKeyDataLoader {\n\n    // be empty of\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/model/ApiKeyModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.model;\n\nimport cn.dev33.satoken.apikey.error.SaApiKeyErrorCode;\nimport cn.dev33.satoken.apikey.exception.ApiKeyException;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\nimport java.util.*;\n\n/**\n * Model: API Key\n *\n * @author click33\n * @since 1.41.0\n */\npublic class ApiKeyModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * 名称\n\t */\n\tprivate String title;\n\n\t/**\n\t * 介绍\n\t */\n\tprivate String intro;\n\n\t/**\n\t * ApiKey 值\n\t */\n\tprivate String apiKey;\n\n\t/**\n\t * 账号 id\n\t */\n\tprivate Object loginId;\n\n\t/**\n\t * ApiKey 创建时间，13位时间戳\n\t */\n\tprivate long createTime;\n\n\t/**\n\t * ApiKey 到期时间，13位时间戳 (-1=永不过期)\n\t */\n\tprivate long expiresTime;\n\n\t/**\n\t * 是否有效 (true=生效, false=禁用)\n\t */\n\tprivate Boolean isValid = true;\n\n\t/**\n\t * 授权范围\n\t */\n\tprivate List<String> scopes = new ArrayList<>();\n\n\t/**\n\t * 扩展数据\n\t */\n\tprivate Map<String, Object> extraData;\n\n\t/**\n\t * 构造函数\n\t */\n\tpublic ApiKeyModel() {\n\t\tthis.createTime = System.currentTimeMillis();\n\t}\n\n\n\t// method\n\n\t/**\n\t * 添加 Scope\n\t * @param scope /\n\t * @return /\n\t */\n\tpublic ApiKeyModel addScope(String ...scope) {\n\t\tif (this.scopes == null) {\n\t\t\tthis.scopes = new ArrayList<>();\n\t\t}\n\t\tthis.scopes.addAll(Arrays.asList(scope));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加 扩展数据\n\t * @param key /\n\t * @param value /\n\t * @return /\n\t */\n\tpublic ApiKeyModel addExtra(String key, Object value) {\n\t\tif (this.extraData == null) {\n\t\t\tthis.extraData = new LinkedHashMap<>();\n\t\t}\n\t\tthis.extraData.put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 查询扩展数据\n\t */\n\tpublic Object getExtra(String key) {\n\t\tif (this.extraData == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.extraData.get(key);\n\t}\n\n\t/**\n\t * 删除扩展数据\n\t */\n\tpublic Object removeExtra(String key) {\n\t\tif (this.extraData == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn this.extraData.remove(key);\n\t}\n\n\t/**\n\t * 数据自检，判断是否可以保存入库\n\t */\n\tpublic void checkByCanSaved() {\n\t\tif (SaFoxUtil.isEmpty(this.apiKey)) {\n\t\t\tthrow new ApiKeyException(\"ApiKey 值不可为空\").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304);\n\t\t}\n\t\tif (this.loginId == null) {\n\t\t\tthrow new ApiKeyException(\"无效 ApiKey: \" + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304);\n\t\t}\n\t\tif (this.createTime == 0) {\n\t\t\tthrow new ApiKeyException(\"请指定 createTime 创建时间\").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304);\n\t\t}\n\t\tif (this.expiresTime == 0) {\n\t\t\tthrow new ApiKeyException(\"请指定 expiresTime 过期时间\").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304);\n\t\t}\n\t\tif (this.isValid == null) {\n\t\t\tthrow new ApiKeyException(\"请指定 isValid 是否生效\").setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12304);\n\t\t}\n\t}\n\n\t/**\n\t * 获取：此 ApiKey 的剩余有效期（秒）, -1=永不过期\n\t * @return /\n\t */\n\tpublic long expiresIn() {\n\t\tif (expiresTime == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn SaTokenDao.NEVER_EXPIRE;\n\t\t}\n\t\tlong s = (expiresTime - System.currentTimeMillis()) / 1000;\n\t\treturn s < 1 ? -2 : s;\n\t}\n\n\t/**\n\t * 判断：此 ApiKey 是否已超时\n\t * @return /\n\t */\n\tpublic boolean timeExpired() {\n\t\tif (expiresTime == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn false;\n\t\t}\n\t\treturn System.currentTimeMillis() > expiresTime;\n\t}\n\n\n\t// get and set\n\n\t/**\n\t * 获取 名称\n\t *\n\t * @return title 名称\n\t */\n\tpublic String getTitle() {\n\t\treturn this.title;\n\t}\n\n\t/**\n\t * 设置 名称\n\t *\n\t * @param title 名称\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setTitle(String title) {\n\t\tthis.title = title;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 介绍\n\t *\n\t * @return intro 介绍\n\t */\n\tpublic String getIntro() {\n\t\treturn this.intro;\n\t}\n\n\t/**\n\t * 设置 介绍\n\t *\n\t * @param intro 介绍\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setIntro(String intro) {\n\t\tthis.intro = intro;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 ApiKey 值\n\t *\n\t * @return apiKey ApiKey 值\n\t */\n\tpublic String getApiKey() {\n\t\treturn this.apiKey;\n\t}\n\n\t/**\n\t * 设置 ApiKey 值\n\t *\n\t * @param apiKey ApiKey 值\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setApiKey(String apiKey) {\n\t\tthis.apiKey = apiKey;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 账号 id\n\t *\n\t * @return loginId 账号 id\n\t */\n\tpublic Object getLoginId() {\n\t\treturn this.loginId;\n\t}\n\n\t/**\n\t * 设置 账号 id\n\t *\n\t * @param loginId 账号 id\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 ApiKey 创建时间，13位时间戳\n\t *\n\t * @return createTime ApiKey 创建时间，13位时间戳\n\t */\n\tpublic long getCreateTime() {\n\t\treturn this.createTime;\n\t}\n\n\t/**\n\t * 设置 ApiKey 创建时间，13位时间戳\n\t *\n\t * @param createTime ApiKey 创建时间，13位时间戳\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 ApiKey 到期时间，13位时间戳 (-1=永不过期)\n\t *\n\t * @return expiresTime ApiKey 到期时间，13位时间戳 (-1=永不过期)\n\t */\n\tpublic long getExpiresTime() {\n\t\treturn this.expiresTime;\n\t}\n\n\t/**\n\t * 设置 ApiKey 到期时间，13位时间戳 (-1=永不过期)\n\t *\n\t * @param expiresTime ApiKey 到期时间，13位时间戳 (-1=永不过期)\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setExpiresTime(long expiresTime) {\n\t\tthis.expiresTime = expiresTime;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 是否有效 (true=生效 false=禁用)\n\t *\n\t * @return /\n\t */\n\tpublic Boolean getIsValid() {\n\t\treturn this.isValid;\n\t}\n\n\t/**\n\t * 设置 是否有效 (true=生效 false=禁用)\n\t *\n\t * @param isValid /\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setIsValid(Boolean isValid) {\n\t\tthis.isValid = isValid;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 授权范围\n\t *\n\t * @return scopes 授权范围\n\t */\n\tpublic List<String> getScopes() {\n\t\treturn this.scopes;\n\t}\n\n\t/**\n\t * 设置 授权范围\n\t *\n\t * @param scopes 授权范围\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setScopes(List<String> scopes) {\n\t\tthis.scopes = scopes;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 扩展数据\n\t *\n\t * @return extraData 扩展数据\n\t */\n\tpublic Map<String, Object> getExtraData() {\n\t\treturn this.extraData;\n\t}\n\n\t/**\n\t * 设置 扩展数据\n\t *\n\t * @param extraData 扩展数据\n\t * @return 对象自身\n\t */\n\tpublic ApiKeyModel setExtraData(Map<String, Object> extraData) {\n\t\tthis.extraData = extraData;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"ApiKeyModel{\" +\n\t\t\t\t\"title='\" + title +\n\t\t\t\t\", intro='\" + intro +\n\t\t\t\t\", apiKey='\" + apiKey +\n\t\t\t\t\", loginId=\" + loginId +\n\t\t\t\t\", createTime=\" + createTime +\n\t\t\t\t\", expiresTime=\" + expiresTime +\n\t\t\t\t\", isValid=\" + isValid +\n\t\t\t\t\", scopes=\" + scopes +\n\t\t\t\t\", extraData=\" + extraData +\n\t\t\t\t'}';\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.template;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.error.SaApiKeyErrorCode;\nimport cn.dev33.satoken.apikey.exception.ApiKeyException;\nimport cn.dev33.satoken.apikey.exception.ApiKeyScopeException;\nimport cn.dev33.satoken.apikey.model.ApiKeyModel;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.raw.SaRawSessionDelegator;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * API Key 操作类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaApiKeyTemplate {\n\n\t/**\n\t *默认命名空间\n\t */\n\tpublic static final String DEFAULT_NAMESPACE = \"apikey\";\n\n\t/**\n\t * 命名空间\n\t */\n\tpublic String namespace;\n\n\t/**\n\t * Raw Session 读写委托\n\t */\n\tpublic SaRawSessionDelegator rawSessionDelegator;\n\n\t/**\n\t * 在 raw-session 中的保存索引列表使用的 key\n\t */\n\tpublic static final String API_KEY_LIST = \"__HD_API_KEY_LIST\";\n\n\tpublic SaApiKeyTemplate(){\n\t\tthis(DEFAULT_NAMESPACE);\n\t}\n\n\t/**\n\t * 实例化\n\t * @param namespace 命名空间，用于多实例隔离\n\t */\n\tpublic SaApiKeyTemplate(String namespace){\n\t\tif(SaFoxUtil.isEmpty(namespace)) {\n\t\t\tthrow new ApiKeyException(\"namespace 不能为空\");\n\t\t}\n\t\tthis.namespace = namespace;\n\t\tthis.rawSessionDelegator = new SaRawSessionDelegator(namespace);\n\t}\n\n\t// ------------------- ApiKey\n\n\t/**\n\t * 根据 apiKey 从 Cache 获取 ApiKeyModel 信息\n\t * @param apiKey /\n\t * @return /\n\t */\n\tpublic ApiKeyModel getApiKeyModelFromCache(String apiKey) {\n\t\treturn getSaTokenDao().getObject(splicingApiKeySaveKey(apiKey), ApiKeyModel.class);\n\t}\n\n\t/**\n\t * 根据 apiKey 从 Database 获取 ApiKeyModel 信息\n\t * @param apiKey /\n\t * @return /\n\t */\n\tpublic ApiKeyModel getApiKeyModelFromDatabase(String apiKey) {\n\t\treturn SaApiKeyManager.getSaApiKeyDataLoader().getApiKeyModelFromDatabase(namespace, apiKey);\n\t}\n\n\t/**\n\t * 获取 ApiKeyModel，无效的 ApiKey 会返回 null\n\t * @param apiKey /\n\t * @return /\n\t */\n\tpublic ApiKeyModel getApiKey(String apiKey) {\n\t\tif(apiKey == null) {\n\t\t\treturn null;\n\t\t}\n\t\t// 先从缓存中获取，缓存中找不到就尝试从数据库获取\n\t\tApiKeyModel apiKeyModel = getApiKeyModelFromCache(apiKey);\n\t\tif(apiKeyModel == null) {\n\t\t\tapiKeyModel = getApiKeyModelFromDatabase(apiKey);\n\t\t\tsaveApiKey(apiKeyModel);\n\t\t}\n\t\treturn apiKeyModel;\n\t}\n\n\t/**\n\t * 校验 ApiKey，成功返回 ApiKeyModel，失败则抛出异常\n\t * @param apiKey /\n\t * @return /\n\t */\n\tpublic ApiKeyModel checkApiKey(String apiKey) {\n\t\tApiKeyModel ak = getApiKey(apiKey);\n\t\tif(ak == null) {\n\t\t\tthrow new ApiKeyException(\"无效 API Key: \" + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12301);\n\t\t}\n\t\tif(ak.timeExpired()) {\n\t\t\tthrow new ApiKeyException(\"API Key 已过期: \" + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12302);\n\t\t}\n\t\tif(! ak.getIsValid()) {\n\t\t\tthrow new ApiKeyException(\"API Key 已被禁用: \" + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12303);\n\t\t}\n\t\treturn ak;\n\t}\n\n\t/**\n\t * 持久化：ApiKeyModel\n\t * @param ak /\n\t */\n\tpublic void saveApiKey(ApiKeyModel ak) {\n\t\tif(ak == null) {\n\t\t\treturn;\n\t\t}\n\t\t// 数据自检\n\t\tak.checkByCanSaved();\n\n\t\t// 保存 ApiKeyModel\n\t\tString saveKey = splicingApiKeySaveKey(ak.getApiKey());\n\t\tif(ak.timeExpired()) {\n\t\t\tgetSaTokenDao().deleteObject(saveKey);\n\t\t} else {\n\t\t\tgetSaTokenDao().setObject(saveKey, ak, ak.expiresIn());\n\t\t}\n\n\t\t// 记录索引\n\t\tif (getIsRecordIndex()) {\n\t\t\t// 添加索引\n\t\t\tSaSession session = rawSessionDelegator.getSessionById(ak.getLoginId());\n\t\t\tArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);\n\t\t\tif(! apiKeyList.contains(ak.getApiKey())) {\n\t\t\t\tapiKeyList.add(ak.getApiKey());\n\t\t\t\tsession.set(API_KEY_LIST, apiKeyList);\n\t\t\t}\n\n\t\t\t// 调整 ttl\n\t\t\tadjustIndex(ak.getLoginId(), session);\n\t\t}\n\n\t}\n\n\t/**\n\t * 获取 ApiKey 所代表的 LoginId\n\t * @param apiKey ApiKey\n\t * @return LoginId\n\t */\n\tpublic Object getLoginIdByApiKey(String apiKey) {\n\t\treturn checkApiKey(apiKey).getLoginId();\n\t}\n\n\t/**\n\t * 删除 ApiKey\n\t * @param apiKey ApiKey\n\t */\n\tpublic void deleteApiKey(String apiKey) {\n\t\t// 删 ApiKeyModel\n\t\tApiKeyModel ak = getApiKeyModelFromCache(apiKey);\n\t\tif(ak == null) {\n\t\t\treturn;\n\t\t}\n\t\tgetSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey));\n\n\t\t// 删索引\n\t\tif(getIsRecordIndex()) {\n\t\t\t// RawSession 中不存在，提前退出\n\t\t\tSaSession session = rawSessionDelegator.getSessionById(ak.getLoginId(), false);\n\t\t\tif(session == null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// 索引无记录，提前退出\n\t\t\tArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);\n\t\t\tif(! apiKeyList.contains(apiKey)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// 如果只有一个 ApiKey，则整个 RawSession 删掉\n\t\t\tif (apiKeyList.size() == 1) {\n\t\t\t\trawSessionDelegator.deleteSessionById(ak.getLoginId());\n\t\t\t} else {\n\t\t\t\t// 否则移除此 ApiKey 并保存\n\t\t\t\tapiKeyList.remove(apiKey);\n\t\t\t\tsession.set(API_KEY_LIST, apiKeyList);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 删除指定 loginId 的所有 ApiKey\n\t * @param loginId /\n\t */\n\tpublic void deleteApiKeyByLoginId(Object loginId) {\n\t\t// 先判断是否开启索引\n\t\tif(! getIsRecordIndex()) {\n\t\t\tSaManager.getLog().warn(\"当前 API Key 模块未开启索引记录功能，无法执行 deleteApiKeyByLoginId 操作\");\n\t\t\treturn;\n\t\t}\n\n\t\t// RawSession 中不存在，提前退出\n\t\tSaSession session = rawSessionDelegator.getSessionById(loginId, false);\n\t\tif(session == null) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 先删 ApiKeyModel\n\t\tArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);\n\t\tfor (String apiKey : apiKeyList) {\n\t\t\tgetSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey));\n\t\t}\n\n\t\t// 再删索引\n\t\trawSessionDelegator.deleteSessionById(loginId);\n\t}\n\n\t// ------- 创建\n\n\t/**\n\t * 创建一个 ApiKeyModel 对象\n\t *\n\t * @return /\n\t */\n\tpublic ApiKeyModel createApiKeyModel() {\n\t\tString apiKey = SaStrategy.instance.generateUniqueToken.execute(\n\t\t\t\t\"API Key\",\n\t\t\t\tSaManager.getConfig().getMaxTryTimes(),\n\t\t\t\tthis::randomApiKeyValue,\n\t\t\t\t_apiKey -> getApiKey(_apiKey) == null\n\t\t);\n\t\treturn new ApiKeyModel().setApiKey(apiKey);\n\t}\n\n\t/**\n\t * 创建一个 ApiKeyModel 对象\n\t *\n\t * @return /\n\t */\n\tpublic ApiKeyModel createApiKeyModel(Object loginId) {\n\t\tlong timeout = SaApiKeyManager.getConfig().getTimeout();\n\t\tlong expiresTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? SaTokenDao.NEVER_EXPIRE : System.currentTimeMillis() + timeout * 1000;\n\t\treturn createApiKeyModel()\n\t\t\t\t.setLoginId(loginId)\n\t\t\t\t.setIsValid(true)\n\t\t\t\t.setExpiresTime(expiresTime)\n\t\t\t\t;\n\t}\n\n\t/**\n\t * 随机一个 ApiKey 码\n\t *\n\t * @return /\n\t */\n\tpublic String randomApiKeyValue() {\n\t\treturn SaApiKeyManager.getConfig().getPrefix() + SaFoxUtil.getRandomString(36);\n\t}\n\n\n\t// ------------------- 校验\n\n\t/**\n\t * 判断：指定 ApiKey 是否具有指定 Scope 列表 (AND 模式，需要全部具备)，返回 true 或 false\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic boolean hasApiKeyScope(String apiKey, String... scopes) {\n\t\ttry {\n\t\t\tcheckApiKeyScope(apiKey, scopes);\n\t\t\treturn true;\n\t\t} catch (ApiKeyException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：指定 ApiKey 是否具有指定 Scope 列表 (AND 模式，需要全部具备)，如果不具备则抛出异常\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic void checkApiKeyScope(String apiKey, String... scopes) {\n\t\tApiKeyModel ak = checkApiKey(apiKey);\n\t\tif(SaFoxUtil.isEmptyArray(scopes)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (String scope : scopes) {\n\t\t\tif(! ak.getScopes().contains(scope)) {\n\t\t\t\tthrow new ApiKeyScopeException(\"该 API Key 不具备 Scope：\" + scope)\n\t\t\t\t\t\t.setApiKey(apiKey)\n\t\t\t\t\t\t.setScope(scope)\n\t\t\t\t\t\t.setCode(SaApiKeyErrorCode.CODE_12311);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 判断：指定 ApiKey 是否具有指定 Scope 列表 (OR 模式，具备其一即可)，返回 true 或 false\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic boolean hasApiKeyScopeOr(String apiKey, String... scopes) {\n\t\ttry {\n\t\t\tcheckApiKeyScopeOr(apiKey, scopes);\n\t\t\treturn true;\n\t\t} catch (ApiKeyException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：指定 ApiKey 是否具有指定 Scope 列表 (OR 模式，具备其一即可)，如果不具备则抛出异常\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic void checkApiKeyScopeOr(String apiKey, String... scopes) {\n\t\tApiKeyModel ak = checkApiKey(apiKey);\n\t\tif(SaFoxUtil.isEmptyArray(scopes)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (String scope : scopes) {\n\t\t\tif(ak.getScopes().contains(scope)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthrow new ApiKeyScopeException(\"该 API Key 不具备 Scope：\" + scopes[0])\n\t\t\t\t.setApiKey(apiKey)\n\t\t\t\t.setScope(scopes[0])\n\t\t\t\t.setCode(SaApiKeyErrorCode.CODE_12311);\n\t}\n\n\t/**\n\t * 判断：指定 ApiKey 是否属于指定 LoginId，返回 true 或 false\n\t * @param apiKey /\n\t * @param loginId /\n\t */\n\tpublic boolean isApiKeyLoginId(String apiKey, Object loginId) {\n\t\ttry {\n\t\t\tcheckApiKeyLoginId(apiKey, loginId);\n\t\t\treturn true;\n\t\t} catch (ApiKeyException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：指定 ApiKey 是否属于指定 LoginId，如果不是则抛出异常\n\t *\n\t * @param apiKey /\n\t * @param loginId /\n\t */\n\tpublic void checkApiKeyLoginId(String apiKey, Object loginId) {\n\t\tApiKeyModel ak = getApiKey(apiKey);\n\t\tif(ak == null) {\n\t\t\tthrow new ApiKeyException(\"无效 API Key: \" + apiKey).setApiKey(apiKey).setCode(SaApiKeyErrorCode.CODE_12301);\n\t\t}\n\t\tif (SaFoxUtil.notEquals(String.valueOf(ak.getLoginId()), String.valueOf(loginId))) {\n\t\t\tthrow new ApiKeyException(\"该 API Key 不属于用户: \" + loginId)\n\t\t\t\t\t.setApiKey(apiKey)\n\t\t\t\t\t.setCode(SaApiKeyErrorCode.CODE_12312);\n\t\t}\n\t}\n\n\n\t// ------------------- 索引操作\n\n\t/**\n\t * 调整指定 SaSession 的 TTL 值，以保证最小化内存占用\n\t * @param loginId /\n\t * @param session 可填写 null，代表使用 loginId 现场查询\n\t */\n\tpublic void adjustIndex(Object loginId, SaSession session) {\n\t\t// 先判断是否开启索引\n\t\tif(! getIsRecordIndex()) {\n\t\t\tSaManager.getLog().warn(\"当前 API Key 模块未开启索引记录功能，无法执行 adjustIndex 操作\");\n\t\t\treturn;\n\t\t}\n\n\t\t// 未提供则现场查询\n\t\tif(session == null) {\n\t\t\tsession = rawSessionDelegator.getSessionById(loginId, false);\n\t\t\tif(session == null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// 重新整理索引列表\n\t\tArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);\n\t\tArrayList<String> apiKeyNewList = new ArrayList<>();\n\t\tArrayList<ApiKeyModel> apiKeyModelList = new ArrayList<>();\n\t\tfor (String apikey : apiKeyList) {\n\t\t\tApiKeyModel ak = getApiKeyModelFromCache(apikey);\n\t\t\tif(ak == null || ak.timeExpired()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tapiKeyNewList.add(apikey);\n\t\t\tapiKeyModelList.add(ak);\n\t\t}\n\t\t// 如果队列里已无有效值，则删除该 session\n\t\tif(apiKeyNewList.isEmpty()) {\n\t\t\trawSessionDelegator.deleteSessionById(loginId);\n\t\t\treturn;\n\t\t}\n\t\tsession.set(API_KEY_LIST, apiKeyNewList);\n\n\t\t// 调整 SaSession TTL\n\t\tlong maxTtl = 0;\n\t\tfor (ApiKeyModel ak : apiKeyModelList) {\n\t\t\tlong ttl = ak.expiresIn();\n\t\t\tif(ttl == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t\tmaxTtl = SaTokenDao.NEVER_EXPIRE;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif(ttl > maxTtl) {\n\t\t\t\tmaxTtl = ttl;\n\t\t\t}\n\t\t}\n\t\tif(maxTtl != 0) {\n\t\t\tsession.updateTimeout(maxTtl);\n\t\t}\n\t}\n\n\t/**\n\t * 获取指定 loginId 的 ApiKey 列表记录\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic List<ApiKeyModel> getApiKeyList(Object loginId) {\n\t\t// 先判断是否开启索引\n\t\tif(! getIsRecordIndex()) {\n\t\t\tSaManager.getLog().warn(\"当前 API Key 模块未开启索引记录功能，无法执行 getApiKeyList 操作\");\n\t\t\treturn new ArrayList<>();\n\t\t}\n\n\t\t// 先查 RawSession\n\t\tList<ApiKeyModel> apiKeyModelList = new ArrayList<>();\n\t\tSaSession session = rawSessionDelegator.getSessionById(loginId, false);\n\t\tif(session == null) {\n\t\t\treturn apiKeyModelList;\n\t\t}\n\n\t\t// 从 RawSession 遍历查询\n\t\tArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);\n\t\tfor (String apikey : apiKeyList) {\n\t\t\tApiKeyModel ak = getApiKeyModelFromCache(apikey);\n\t\t\tif(ak == null || ak.timeExpired()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tapiKeyModelList.add(ak);\n\t\t}\n\t\treturn apiKeyModelList;\n\t}\n\n\n\t// ------------------- 请求查询\n\n\t/**\n\t * 数据读取：从请求对象中读取 ApiKey，获取不到返回 null\n\t */\n\tpublic String readApiKeyValue(SaRequest request) {\n\n\t\t// 优先从请求参数中获取\n\t\tString apiKey = request.getParam(namespace);\n\t\tif(SaFoxUtil.isNotEmpty(apiKey)) {\n\t\t\treturn apiKey;\n\t\t}\n\n\t\t// 然后请求头\n\t\tapiKey = request.getHeader(namespace);\n\t\tif(SaFoxUtil.isNotEmpty(apiKey)) {\n\t\t\treturn apiKey;\n\t\t}\n\n\t\t// 最后从 Authorization 中获取\n\t\tapiKey = SaHttpBasicUtil.getAuthorizationValue();\n\t\tif(SaFoxUtil.isNotEmpty(apiKey)) {\n\t\t\tif(apiKey.endsWith(\":\")) {\n\t\t\t\tapiKey = apiKey.substring(0, apiKey.length() - 1);\n\t\t\t}\n\t\t\treturn apiKey;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * 数据读取：从请求对象中读取 ApiKey，并查询到 ApiKeyModel 信息\n\t */\n\tpublic ApiKeyModel currentApiKey() {\n\t\tString readApiKeyValue = readApiKeyValue(SaHolder.getRequest());\n\t\treturn checkApiKey(readApiKeyValue);\n\t}\n\n\n\t// ------------------- 拼接key\n\n\t/**\n\t * 拼接key：ApiKey 持久化\n\t * @param apiKey ApiKey\n\t * @return key\n\t */\n\tpublic String splicingApiKeySaveKey(String apiKey) {\n\t\treturn getSaTokenConfig().getTokenName() + \":\" + namespace + \":\" + apiKey;\n\t}\n\n\n\t// -------- bean 对象代理\n\n\t/**\n\t * 获取使用的 getSaTokenDao 实例\n\t *\n\t * @return /\n\t */\n\tpublic SaTokenDao getSaTokenDao() {\n\t\treturn SaManager.getSaTokenDao();\n\t}\n\n\t/**\n\t * 获取使用的 SaTokenConfig 实例\n\t *\n\t * @return /\n\t */\n\tpublic SaTokenConfig getSaTokenConfig() {\n\t\treturn SaManager.getConfig();\n\t}\n\n\t/**\n\t * 是否保存索引信息\n\t */\n\tpublic boolean getIsRecordIndex() {\n\t\treturn SaApiKeyManager.getSaApiKeyDataLoader().getIsRecordIndex();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/apikey/template/SaApiKeyUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.apikey.template;\n\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.model.ApiKeyModel;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.session.SaSession;\n\nimport java.util.List;\n\n/**\n * API Key 操作工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaApiKeyUtil {\n\n\t/**\n\t * 获取 ApiKeyModel，无效的 ApiKey 会返回 null\n\t * @param apiKey /\n\t * @return /\n\t */\n\tpublic static ApiKeyModel getApiKey(String apiKey) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().getApiKey(apiKey);\n\t}\n\n\t/**\n\t * 校验 ApiKey，成功返回 ApiKeyModel，失败则抛出异常\n\t * @param apiKey /\n\t * @return /\n\t */\n\tpublic static ApiKeyModel checkApiKey(String apiKey) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().checkApiKey(apiKey);\n\t}\n\n\t/**\n\t * 持久化：ApiKeyModel\n\t * @param ak /\n\t */\n\tpublic static void saveApiKey(ApiKeyModel ak) {\n\t\tSaApiKeyManager.getSaApiKeyTemplate().saveApiKey(ak);\n\t}\n\n\t/**\n\t * 获取 ApiKey 所代表的 LoginId\n\t * @param apiKey ApiKey\n\t * @return LoginId\n\t */\n\tpublic static Object getLoginIdByApiKey(String apiKey) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().getLoginIdByApiKey(apiKey);\n\t}\n\n\t/**\n\t * 删除 ApiKey\n\t * @param apiKey ApiKey\n\t */\n\tpublic static void deleteApiKey(String apiKey) {\n\t\tSaApiKeyManager.getSaApiKeyTemplate().deleteApiKey(apiKey);\n\t}\n\n\t/**\n\t * 删除指定 loginId 的所有 ApiKey\n\t * @param loginId /\n\t */\n\tpublic static void deleteApiKeyByLoginId(Object loginId) {\n\t\tSaApiKeyManager.getSaApiKeyTemplate().deleteApiKeyByLoginId(loginId);\n\t}\n\n\t// ------- 创建\n\n\t/**\n\t * 创建一个 ApiKeyModel 对象\n\t *\n\t * @return /\n\t */\n\tpublic static ApiKeyModel createApiKeyModel() {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel();\n\t}\n\n\t/**\n\t * 创建一个 ApiKeyModel 对象\n\t *\n\t * @return /\n\t */\n\tpublic static ApiKeyModel createApiKeyModel(Object loginId) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel(loginId);\n\t}\n\n\n\t// ------------------- Scope\n\n\t/**\n\t * 判断：指定 ApiKey 是否具有指定 Scope 列表 (AND 模式，需要全部具备)，返回 true 或 false\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static boolean hasApiKeyScope(String apiKey, String... scopes) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScope(apiKey, scopes);\n\t}\n\n\t/**\n\t * 校验：指定 ApiKey 是否具有指定 Scope 列表 (AND 模式，需要全部具备)，如果不具备则抛出异常\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static void checkApiKeyScope(String apiKey, String... scopes) {\n\t\tSaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScope(apiKey, scopes);\n\t}\n\n\t/**\n\t * 判断：指定 ApiKey 是否具有指定 Scope 列表 (OR 模式，具备其一即可)，返回 true 或 false\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static boolean hasApiKeyScopeOr(String apiKey, String... scopes) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScopeOr(apiKey, scopes);\n\t}\n\n\t/**\n\t * 校验：指定 ApiKey 是否具有指定 Scope 列表 (OR 模式，具备其一即可)，如果不具备则抛出异常\n\t * @param apiKey ApiKey\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static void checkApiKeyScopeOr(String apiKey, String... scopes) {\n\t\tSaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScopeOr(apiKey, scopes);\n\t}\n\n\t/**\n\t * 判断：指定 ApiKey 是否属于指定 LoginId，返回 true 或 false\n\t * @param apiKey /\n\t * @param loginId /\n\t */\n\tpublic static boolean isApiKeyLoginId(String apiKey, Object loginId) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().isApiKeyLoginId(apiKey, loginId);\n\t}\n\n\t/**\n\t * 校验：指定 ApiKey 是否属于指定 LoginId，如果不是则抛出异常\n\t *\n\t * @param apiKey /\n\t * @param loginId /\n\t */\n\tpublic static void checkApiKeyLoginId(String apiKey, Object loginId) {\n\t\tSaApiKeyManager.getSaApiKeyTemplate().checkApiKeyLoginId(apiKey, loginId);\n\t}\n\n\n\t// ------------------- 请求查询\n\n\t/**\n\t * 数据读取：从请求对象中读取 ApiKey，获取不到返回 null\n\t */\n\tpublic static String readApiKeyValue(SaRequest request) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().readApiKeyValue(request);\n\t}\n\n\t/**\n\t * 数据读取：从请求对象中读取 ApiKey，并查询到 ApiKeyModel 信息\n\t */\n\tpublic static ApiKeyModel currentApiKey() {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().currentApiKey();\n\t}\n\n\n\t// ------------------- 索引操作\n\n\t/**\n\t * 调整指定 SaSession 的 TTL 值，以保证最小化内存占用\n\t * @param loginId /\n\t * @param session 可填写 null，代表使用 loginId 现场查询\n\t */\n\tpublic static void adjustIndex(Object loginId, SaSession session) {\n\t\tSaApiKeyManager.getSaApiKeyTemplate().adjustIndex(loginId, session);\n\t}\n\n\t/**\n\t * 获取指定 loginId 的 ApiKey 列表记录\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic static List<ApiKeyModel> getApiKeyList(Object loginId) {\n\t\treturn SaApiKeyManager.getSaApiKeyTemplate().getApiKeyList(loginId);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForApiKey.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.apikey.annotation.handle.SaCheckApiKeyHandler;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\n\n/**\n * SaToken 插件安装：API Key 组件\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaTokenPluginForApiKey implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        // 安装 API Key 鉴权注解\n        SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckApiKeyHandler());\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-apikey/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForApiKey"
  },
  {
    "path": "sa-token-plugin/sa-token-caffeine/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-caffeine</name>\n    <artifactId>sa-token-caffeine</artifactId>\n\t<description>sa-token integrate Caffeine</description>\n\n\t<dependencies>\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.github.ben-manes.caffeine</groupId>\n            <artifactId>caffeine</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaMapPackageForCaffeine.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\nimport cn.dev33.satoken.dao.timedcache.SaMapPackage;\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\n\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Map 包装类 (Caffeine 版)\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaMapPackageForCaffeine<V> implements SaMapPackage<V> {\n\n\tpublic Cache<String, V> cache = Caffeine.newBuilder()\n\t\t\t.expireAfterWrite(Long.MAX_VALUE, TimeUnit.SECONDS)\n\t\t\t.maximumSize(Integer.MAX_VALUE)\n\t\t\t.build();\n\n\t@Override\n\tpublic Object getSource() {\n\t\treturn cache;\n\t}\n\n\t/**\n\t * 读\n\t *\n\t * @param key /\n\t * @return /\n\t */\n\t@Override\n\tpublic V get(String key) {\n\t\treturn cache.getIfPresent(key);\n\t}\n\n\t/**\n\t * 写\n\t *\n\t * @param key /\n\t * @param value /\n\t */\n\t@Override\n\tpublic void put(String key, V value) {\n\t\tcache.put(key, value);\n\t}\n\n\t/**\n\t * 删\n\t * @param key /\n\t */\n\t@Override\n\tpublic void remove(String key) {\n\t\tcache.invalidate(key);\n\t}\n\n\t/**\n\t * 所有 key\n\t */\n\t@Override\n\tpublic Set<String> keySet() {\n\t\treturn cache.asMap().keySet();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForCaffeine.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\n\nimport cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject;\nimport cn.dev33.satoken.dao.timedcache.SaTimedCache;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.List;\n\n/**\n * Sa-Token 持久层实现，基于 SaTimedCache - Caffeine （内存缓存，系统重启后数据丢失）\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenDaoForCaffeine implements SaTokenDaoByStringFollowObject, SaTokenDao {\n\n\tpublic SaTimedCache timedCache = new SaTimedCache(\n\t\t\tnew SaMapPackageForCaffeine<>(),\n\t\t\tnew SaMapPackageForCaffeine<>()\n\t);\n\n\t// ------------------------ Object 读写操作\n\n\t@Override\n\tpublic Object getObject(String key) {\n\t\treturn timedCache.getObject(key);\n\t}\n\n\t@Override\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T> T getObject(String key, Class<T> classType){\n\t\treturn (T) getObject(key);\n\t}\n\n\t@Override\n\tpublic void setObject(String key, Object object, long timeout) {\n\t\ttimedCache.setObject(key, object, timeout);\n\t}\n\n\t@Override\n\tpublic void updateObject(String key, Object object) {\n\t\ttimedCache.updateObject(key, object);\n\t}\n\n\t@Override\n\tpublic void deleteObject(String key) {\n\t\ttimedCache.deleteObject(key);\n\t}\n\n\t@Override\n\tpublic long getObjectTimeout(String key) {\n\t\treturn timedCache.getObjectTimeout(key);\n\t}\n\n\t@Override\n\tpublic void updateObjectTimeout(String key, long timeout) {\n\t\ttimedCache.updateObjectTimeout(key, timeout);\n\t}\n\n\n\t// --------- 会话管理\n\n\t@Override\n\tpublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\t\treturn SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType);\n\t}\n\n\n\t// --------- 组件生命周期\n\n\t/**\n\t * 组件被安装时，开始刷新数据线程\n\t */\n\t@Override\n\tpublic void init() {\n\t\ttimedCache.initRefreshThread();\n\t}\n\n\t/**\n\t * 组件被卸载时，结束定时任务，不再定时清理过期数据\n\t */\n\t@Override\n\tpublic void destroy() {\n\t\ttimedCache.endRefreshThread();\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-caffeine/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForCaffeine.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDaoForCaffeine;\n\n/**\n * SaToken 插件安装：DAO 扩展 - Caffeine 版\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForCaffeine implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n\n        SaManager.setSaTokenDao(new SaTokenDaoForCaffeine());\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-caffeine/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForCaffeine"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-dubbo</name>\n    <artifactId>sa-token-dubbo</artifactId>\n\t<description>sa-token-dubbo</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        \n\t\t<!-- dubbo -->\n\t    <dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- <dependency>\n\t\t\t<groupId>org.apache.dubbo</groupId>\n\t\t\t<artifactId>dubbo-spring-boot-starter</artifactId>\n\t\t\t<version>2.7.11</version>\n\t\t</dependency> -->\n        \n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboConsumerFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo.filter;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.SaTokenContextDefaultImpl;\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.*;\n\n/**\n * Sa-Token 整合 Dubbo Consumer 端（调用端）过滤器\n * \n * @author click33\n * @since 1.34.0\n */\n@Activate(group = {CommonConstants.CONSUMER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER)\npublic class SaTokenDubboConsumerFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {\n\t\t\n\t\t// 追加 Same-Token 参数\n\t\tif(SaManager.getConfig().getCheckSameToken()) {\n\t\t\tRpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken()); \n\t\t}\n\n\t\t// 无上下文时只做简单调用，不传递会话 token\n\t\tif( ! SaHolder.getContext().isValid()) {\n\t\t\treturn invoker.invoke(invocation);\n\t\t}\n\n\t\t// 1、调用前，向下传递会话Token\n\t\tif(SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) {\n\t\t\tRpcContext.getContext().setAttachment(SaTokenConsts.JUST_CREATED, StpUtil.getTokenValueNotCut()); \n\t\t}\n\n\t\t// 2、开始调用\n\t\tResult invoke = invoker.invoke(invocation);\n\t\t\n\t\t// 3、调用后，解析回传的Token值\n\t\tStpUtil.setTokenValue(invoke.getAttachment(SaTokenConsts.JUST_CREATED_NOT_PREFIX));\n\t\t\n\t\t// note\n\t\treturn invoke;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboContextFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo.filter;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.dubbo.util.SaTokenContextDubboUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.*;\n\n/**\n * Sa-Token 整合 Dubbo 上下文初始化过滤器\n * \n * @author click33\n * @since 1.42.0\n */\n@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_CONTEXT_FILTER_ORDER)\npublic class SaTokenDubboContextFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) {\n\t\tif(SaHolder.getContext().isValid()) {\n\t\t\treturn invoker.invoke(invocation);\n\t\t}\n\t\ttry {\n\t\t\tSaTokenContextDubboUtil.setContext(RpcContext.getContext());\n\t\t\treturn invoker.invoke(invocation);\n\t\t} finally {\n\t\t\tSaTokenContextDubboUtil.clearContext();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/filter/SaTokenDubboProviderFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo.filter;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.Filter;\nimport org.apache.dubbo.rpc.Invocation;\nimport org.apache.dubbo.rpc.Invoker;\nimport org.apache.dubbo.rpc.Result;\n\n/**\n * Sa-Token 整合 Dubbo Provider端（被调用端）过滤器\n * \n * @author click33\n * @since 1.34.0\n */\n@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER)\npublic class SaTokenDubboProviderFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) {\n\t\t// RPC 调用鉴权 \n\t\tif(SaManager.getConfig().getCheckSameToken()) {\n\t\t\tString idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN);\n\n\t\t\t// dubbo部分协议会将参数变为小写，此处需要额外处理一下，详细参考：https://gitee.com/dromara/sa-token/issues/I4WXQG\n\t\t\tif(idToken == null) {\n\t\t\t\tidToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN.toLowerCase());\n\t\t\t}\n\t\t\tSaSameUtil.checkToken(idToken);\n\t\t}\n\t\t\n\t\t// 开始调用\n\t\treturn invoker.invoke(invocation);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaRequestForDubbo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo.model;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport org.apache.dubbo.rpc.RpcContext;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * 对 SaRequest 包装类的实现（Dubbo 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaRequestForDubbo implements SaRequest {\n\n\t/**\n\t * 底层对象 \n\t */\n\tprotected RpcContext rpcContext;\n\t\n\t/**\n\t * 实例化\n\t * @param rpcContext rpcContext对象 \n\t */\n\tpublic SaRequestForDubbo(RpcContext rpcContext) {\n\t\tthis.rpcContext = rpcContext;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn rpcContext;\n\t}\n\n\t/**\n\t * 在 [请求体] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getParam(String name) {\n\t\t// 不传播 url 参数 \n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数名称\n\t * @return 参数名称列表\n\t */\n\t@Override\n\tpublic Collection<String> getParamNames(){\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数\n\t * @return 参数列表\n\t */\n\t@Override\n\tpublic Map<String, String> getParamMap(){\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [请求头] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getHeader(String name) {\n\t\t// 不传播 header 参数 \n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [Cookie作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getCookieValue(String name) {\n\t\t// 不传播 cookie 参数 \n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n\t */\n\t@Override\n\tpublic String getCookieFirstValue(String name){\n\t\t// 不传播 cookie 参数\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\t@Override\n\tpublic String getCookieLastValue(String name){\n\t\t// 不传播 cookie 参数\n\t\treturn null;\n\t}\n\n\t/**\n\t * 返回当前请求path (不包括上下文名称)  \n\t */\n\t@Override\n\tpublic String getRequestPath() {\n\t\t// 不传播 requestPath \n\t\treturn null;\n\t}\n\n\t/**\n\t * 返回当前请求的url，例：http://xxx.com/test\n\t * @return see note\n\t */\n\tpublic String getUrl() {\n\t\t// 不传播 url \n\t\treturn null;\n\t}\n\t\n\t/**\n\t * 返回当前请求的类型 \n\t */\n\t@Override\n\tpublic String getMethod() {\n\t\t// 不传播 method \n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic String getHost() {\n\t\treturn null;\n\t}\n\n\t/**\n\t * 转发请求 \n\t */\n\t@Override\n\tpublic Object forward(String path) {\n\t\t// 不传播 forward 动作 \n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaResponseForDubbo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo.model;\n\nimport cn.dev33.satoken.context.model.SaResponse;\nimport org.apache.dubbo.rpc.RpcContext;\n\n/**\n * 对 SaResponse 包装类的实现（Dubbo 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaResponseForDubbo implements SaResponse {\n\n\t/**\n\t * 底层Request对象 \n\t */\n\tprotected RpcContext rpcContext;\n\t\n\t/**\n\t * 实例化\n\t * @param rpcContext rpcContext对象 \n\t */\n\tpublic SaResponseForDubbo(RpcContext rpcContext) {\n\t\tthis.rpcContext = rpcContext;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn rpcContext;\n\t}\n\n\t/**\n\t * 设置响应状态码 \n\t */\n\t@Override\n\tpublic SaResponse setStatus(int sc) {\n\t\t// 不回传 status 状态 \n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 在响应头里写入一个值 \n\t */\n\t@Override\n\tpublic SaResponse setHeader(String name, String value) {\n\t\t// 不回传 header响应头 \n\t\treturn this;\n\t}\n\n\t/**\n\t * 在响应头里添加一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tpublic SaResponse addHeader(String name, String value) {\n\t\t// 不回传 header响应头 \n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 重定向 \n\t */\n\t@Override\n\tpublic Object redirect(String url) {\n\t\t// 不回传 重定向 动作 \n\t\treturn null;\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/model/SaStorageForDubbo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo.model;\n\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.rpc.RpcContext;\n\n/**\n * 对 SaStorage 包装类的实现（Dubbo 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaStorageForDubbo implements SaStorage {\n\n\t/**\n\t * 底层对象 \n\t */\n\tprotected RpcContext rpcContext;\n\t\n\t/**\n\t * 实例化\n\t * @param rpcContext rpcContext对象 \n\t */\n\tpublic SaStorageForDubbo(RpcContext rpcContext) {\n\t\tthis.rpcContext = rpcContext;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn rpcContext;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里写入一个值 \n\t */\n\t@Override\n\tpublic SaStorageForDubbo set(String key, Object value) {\n\t\trpcContext.setObjectAttachment(key, value);\n\t\t// 如果是token写入，则回传到Consumer端  \n\t\tif(key.equals(SaTokenConsts.JUST_CREATED_NOT_PREFIX)) {\n\t\t\tRpcContext.getServerContext().setAttachment(key, value);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn rpcContext.getObjectAttachment(key);\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里删除一个值 \n\t */\n\t@Override\n\tpublic SaStorageForDubbo delete(String key) {\n\t\trpcContext.removeAttachment(key);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/java/cn/dev33/satoken/context/dubbo/util/SaTokenContextDubboUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo.util;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.dubbo.model.SaRequestForDubbo;\nimport cn.dev33.satoken.context.dubbo.model.SaResponseForDubbo;\nimport cn.dev33.satoken.context.dubbo.model.SaStorageForDubbo;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport org.apache.dubbo.rpc.RpcContext;\n\n\n/**\n * SaTokenContext 上下文读写工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextDubboUtil {\n\n\t/**\n\t * 写入当前上下文\n\t * @param rpcContext /\n\t */\n\tpublic static void setContext(RpcContext rpcContext) {\n\t\tSaRequest saRequest = new SaRequestForDubbo(RpcContext.getContext());\n\t\tSaResponse saResponse = new SaResponseForDubbo(RpcContext.getContext());\n\t\tSaStorage saStorage = new SaStorageForDubbo(RpcContext.getContext());\n\t\tSaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage);\n\t}\n\n\t/**\n\t * 清除当前上下文\n\t */\n\tpublic static void clearContext() {\n\t\tSaManager.getSaTokenContext().clearContext();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter",
    "content": "saTokenDubboConsumerFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboConsumerFilter\nsaTokenDubboProviderFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboProviderFilter\nsaTokenDubboContextFilter=cn.dev33.satoken.context.dubbo.filter.SaTokenDubboContextFilter"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\t\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\t\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <name>sa-token-dubbo3</name>\n    <artifactId>sa-token-dubbo3</artifactId>\n    <description>sa-token-dubbo3</description>\n\n\n    <properties>\n        <dubbo3.version>3.2.2</dubbo3.version>\n        <maven.compiler.source>8</maven.compiler.source>\n        <maven.compiler.target>8</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <!-- sa-token-core -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\n        <!-- dubbo3 -->\n        <dependency>\n            <groupId>org.apache.dubbo</groupId>\n            <artifactId>dubbo</artifactId>\n            <version>${dubbo3.version}</version>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ConsumerFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo3.filter;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.SaTokenContextDefaultImpl;\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.*;\n\n/**\n * Sa-Token 整合 Dubbo3 Consumer 端（调用端）过滤器\n *\n * @author click33\n * @since 1.34.0\n */\n@Activate(group = {CommonConstants.CONSUMER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER)\npublic class SaTokenDubbo3ConsumerFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) {\n\t\t\n\t\t// 追加 Same-Token 参数 \n\t\tif(SaManager.getConfig().getCheckSameToken()) {\n\t\t\tRpcContext.getServiceContext().setAttachment(SaSameUtil.SAME_TOKEN,SaSameUtil.getToken());\n\t\t}\n\n\t\t// 无上下文时只做简单调用，不传递会话 token\n\t\tif( ! SaHolder.getContext().isValid()) {\n\t\t\treturn invoker.invoke(invocation);\n\t\t}\n\t\t\n\t\t// 1. 调用前，向下传递会话Token\n\t\tif(SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) {\n\t\t\tRpcContext.getServiceContext().setAttachment(SaTokenConsts.JUST_CREATED, StpUtil.getTokenValueNotCut());\n\t\t}\n\n\t\t// 2. 开始调用 \n\t\tResult invoke = invoker.invoke(invocation);\n\t\t\n\t\t// 3. 调用后，解析回传的Token值 \n\t\tStpUtil.setTokenValue(invoke.getAttachment(SaTokenConsts.JUST_CREATED_NOT_PREFIX));\n\t\t\n\t\t// note \n\t\treturn invoke;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ContextFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo3.filter;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.dubbo3.util.SaTokenContextDubbo3Util;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.*;\n\n/**\n * Sa-Token 整合 Dubbo3 上下文初始化过滤器\n * \n * @author click33\n * @since 1.42.0\n */\n@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_CONTEXT_FILTER_ORDER)\npublic class SaTokenDubbo3ContextFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) {\n\t\tif(SaHolder.getContext().isValid()) {\n\t\t\treturn invoker.invoke(invocation);\n\t\t}\n\t\ttry {\n\t\t\tSaTokenContextDubbo3Util.setContext(RpcContext.getServiceContext());\n\t\t\treturn invoker.invoke(invocation);\n\t\t} finally {\n\t\t\tSaTokenContextDubbo3Util.clearContext();\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/filter/SaTokenDubbo3ProviderFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo3.filter;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.common.constants.CommonConstants;\nimport org.apache.dubbo.common.extension.Activate;\nimport org.apache.dubbo.rpc.Filter;\nimport org.apache.dubbo.rpc.Invocation;\nimport org.apache.dubbo.rpc.Invoker;\nimport org.apache.dubbo.rpc.Result;\n\n/**\n * Sa-Token 整合 Dubbo3 Provider端（被调用端）过滤器\n *\n * @author click33\n * @since 1.34.0\n */\n@Activate(group = {CommonConstants.PROVIDER}, order = SaTokenConsts.RPC_PERMISSION_FILTER_ORDER)\npublic class SaTokenDubbo3ProviderFilter implements Filter {\n\n\t@Override\n\tpublic Result invoke(Invoker<?> invoker, Invocation invocation) {\n\t\t\n\t\t// RPC 调用鉴权 \n\t\tif(SaManager.getConfig().getCheckSameToken()) {\n\t\t\tString idToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN);\n\t\t\t// dubbo部分协议会将参数变为小写，详细参考：https://gitee.com/dromara/sa-token/issues/I4WXQG\n\t\t\tif(idToken == null) {\n\t\t\t\tidToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN.toLowerCase());\n\t\t\t}\n\t\t\tSaSameUtil.checkToken(idToken);\n\t\t}\n\t\t\n\t\t// 开始调用\n\t\treturn invoker.invoke(invocation);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaRequestForDubbo3.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo3.model;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport org.apache.dubbo.rpc.RpcContext;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * 对 SaRequest 包装类的实现（Dubbo3 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaRequestForDubbo3 implements SaRequest {\n\n\t/**\n\t * 底层对象 \n\t */\n\tprotected RpcContext rpcContext;\n\t\n\t/**\n\t * 实例化\n\t * @param rpcContext rpcContext对象 \n\t */\n\tpublic SaRequestForDubbo3(RpcContext rpcContext) {\n\t\tthis.rpcContext = rpcContext;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn rpcContext;\n\t}\n\n\t/**\n\t * 在 [请求体] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getParam(String name) {\n\t\t// 不传播 url 参数 \n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数名称\n\t * @return 参数名称列表\n\t */\n\t@Override\n\tpublic Collection<String> getParamNames(){\n\t\treturn null;\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数\n\t * @return 参数列表\n\t */\n\t@Override\n\tpublic Map<String, String> getParamMap(){\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [请求头] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getHeader(String name) {\n\t\t// 不传播 header 参数 \n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [Cookie作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getCookieValue(String name) {\n\t\t// 不传播 cookie 参数 \n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n\t */\n\t@Override\n\tpublic String getCookieFirstValue(String name){\n\t\t// 不传播 cookie 参数\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\t@Override\n\tpublic String getCookieLastValue(String name){\n\t\t// 不传播 cookie 参数\n\t\treturn null;\n\t}\n\n\t/**\n\t * 返回当前请求path (不包括上下文名称)  \n\t */\n\t@Override\n\tpublic String getRequestPath() {\n\t\t// 不传播 requestPath \n\t\treturn null;\n\t}\n\n\t/**\n\t * 返回当前请求的url，例：http://xxx.com/test\n\t * @return see note\n\t */\n\tpublic String getUrl() {\n\t\t// 不传播 url \n\t\treturn null;\n\t}\n\t\n\t/**\n\t * 返回当前请求的类型 \n\t */\n\t@Override\n\tpublic String getMethod() {\n\t\t// 不传播 method \n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic String getHost() {\n\t\treturn null;\n\t}\n\n\t/**\n\t * 转发请求 \n\t */\n\t@Override\n\tpublic Object forward(String path) {\n\t\t// 不传播 forward 动作 \n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaResponseForDubbo3.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo3.model;\n\n\nimport cn.dev33.satoken.context.model.SaResponse;\nimport org.apache.dubbo.rpc.RpcContext;\n\n/**\n * 对 SaResponse 包装类的实现（Dubbo3 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaResponseForDubbo3 implements SaResponse {\n\n\t/**\n\t * 底层Request对象 \n\t */\n\tprotected RpcContext rpcContext;\n\t\n\t/**\n\t * 实例化\n\t * @param rpcContext rpcContext对象 \n\t */\n\tpublic SaResponseForDubbo3(RpcContext rpcContext) {\n\t\tthis.rpcContext = rpcContext;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn rpcContext;\n\t}\n\n\t/**\n\t * 设置响应状态码 \n\t */\n\t@Override\n\tpublic SaResponse setStatus(int sc) {\n\t\t// 不回传 status 状态 \n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 在响应头里写入一个值 \n\t */\n\t@Override\n\tpublic SaResponse setHeader(String name, String value) {\n\t\t// 不回传 header响应头 \n\t\treturn this;\n\t}\n\n\t/**\n\t * 在响应头里添加一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tpublic SaResponse addHeader(String name, String value) {\n\t\t// 不回传 header响应头 \n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 重定向 \n\t */\n\t@Override\n\tpublic Object redirect(String url) {\n\t\t// 不回传 重定向 动作 \n\t\treturn null;\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/model/SaStorageForDubbo3.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo3.model;\n\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.apache.dubbo.rpc.RpcContext;\n\n/**\n * 对 SaStorage 包装类的实现（Dubbo3 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaStorageForDubbo3 implements SaStorage {\n\n\t/**\n\t * 底层对象 \n\t */\n\tprotected RpcContext rpcContext;\n\t\n\t/**\n\t * 实例化\n\t * @param rpcContext rpcContext对象 \n\t */\n\tpublic SaStorageForDubbo3(RpcContext rpcContext) {\n\t\tthis.rpcContext = rpcContext;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn rpcContext;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里写入一个值 \n\t */\n\t@Override\n\tpublic SaStorageForDubbo3 set(String key, Object value) {\n\t\trpcContext.setObjectAttachment(key, value);\n\t\t// 如果是token写入，则回传到Consumer端  \n\t\tif(key.equals(SaTokenConsts.JUST_CREATED_NOT_PREFIX)) {\n\t\t\tRpcContext.getServerContext().setAttachment(key, value);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn rpcContext.getObjectAttachment(key);\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里删除一个值 \n\t */\n\t@Override\n\tpublic SaStorageForDubbo3 delete(String key) {\n\t\trpcContext.removeAttachment(key);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/java/cn/dev33/satoken/context/dubbo3/util/SaTokenContextDubbo3Util.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.dubbo3.util;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.dubbo3.model.SaRequestForDubbo3;\nimport cn.dev33.satoken.context.dubbo3.model.SaResponseForDubbo3;\nimport cn.dev33.satoken.context.dubbo3.model.SaStorageForDubbo3;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport org.apache.dubbo.rpc.RpcContext;\n\n\n/**\n * SaTokenContext 上下文读写工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextDubbo3Util {\n\n\t/**\n\t * 写入当前上下文\n\t * @param rpcContext /\n\t */\n\tpublic static void setContext(RpcContext rpcContext) {\n\t\tSaRequest saRequest = new SaRequestForDubbo3(RpcContext.getServiceContext());\n\t\tSaResponse saResponse = new SaResponseForDubbo3(RpcContext.getServiceContext());\n\t\tSaStorage saStorage = new SaStorageForDubbo3(RpcContext.getServiceContext());\n\t\tSaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage);\n\t}\n\n\t/**\n\t * 清除当前上下文\n\t */\n\tpublic static void clearContext() {\n\t\tSaManager.getSaTokenContext().clearContext();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-dubbo3/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter",
    "content": "saTokenDubbo3ConsumerFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ConsumerFilter\nsaTokenDubbo3ProviderFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ProviderFilter\nsaTokenDubbo3ContextFilter=cn.dev33.satoken.context.dubbo3.filter.SaTokenDubbo3ContextFilter"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>sa-token-fastjson</name>\n    <artifactId>sa-token-fastjson</artifactId>\n    <description>sa-token integrate Fastjson</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>fastjson</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.alibaba.fastjson.JSON;\n\n/**\n * JSON 转换器， Fastjson 版实现\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaJsonTemplateForFastjson implements SaJsonTemplate {\n\n\t/**\n\t * 序列化：对象 -> json 字符串\n\t */\n\t@Override\n\tpublic String objectToJson(Object obj) {\n\t\tif(SaFoxUtil.isEmpty(obj)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn JSON.toJSONString(obj);\n\t}\n\n\t/**\n\t * 反序列化：json 字符串 → 对象\n\t */\n\t@Override\n\tpublic<T> T jsonToObject(String jsonStr, Class<T> type) {\n\t\tif(SaFoxUtil.isEmpty(jsonStr)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn JSON.parseObject(jsonStr, type);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.json.SaJsonTemplateForFastjson;\nimport cn.dev33.satoken.session.SaSessionForFastjsonCustomized;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\n/**\n * SaToken 插件安装：JSON 转换器 - Fastjson 版\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForFastjson implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n\n        // 设置JSON转换器：Fastjson 版\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson());\n\n        // 重写 SaSession 生成策略\n        SaStrategy.instance.createSession = SaSessionForFastjsonCustomized::new;\n\n        // 指定 SaSession 类型\n        SaStrategy.instance.sessionClassType = SaSessionForFastjsonCustomized.class;\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson/src/main/java/cn/dev33/satoken/session/SaSessionForFastjsonCustomized.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.JSONObject;\n\n/**\n * Fastjson 定制版 SaSession，重写类型转换API\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaSessionForFastjsonCustomized extends SaSession {\n\n\tprivate static final long serialVersionUID = -7600983549653130681L;\n\n\t/**\n\t * 构建一个 SaSession 对象\n\t */\n\tpublic SaSessionForFastjsonCustomized() {\n\t\tsuper();\n\t}\n\n\t/**\n\t * 构建一个 SaSession 对象\n\t * @param id Session 的 id\n\t */\n\tpublic SaSessionForFastjsonCustomized(String id) {\n\t\tsuper(id);\n\t}\n\n\t/**\n\t * 取值 (指定转换类型)\n\t * @param <T> 泛型\n\t * @param key key \n\t * @param cs 指定转换类型 \n\t * @return 值 \n\t */\n\t@Override\n\tpublic <T> T getModel(String key, Class<T> cs) {\n\t\t// 如果是想取出为基础类型\n\t\tObject value = get(key);\n\t\tif(SaFoxUtil.isBasicType(cs)) {\n\t\t\treturn SaFoxUtil.getValueByType(value, cs);\n\t\t}\n\t\t// 为空提前返回\n\t\tif(valueIsNull(value)) {\n\t\t\treturn null;\n\t\t}\n\t\t// 如果是 JSONObject 类型直接转，否则先转为 String 再转\n\t\tif(value instanceof JSONObject) {\n\t\t\tJSONObject jo = (JSONObject) value;\n\t\t\treturn jo.toJavaObject(cs);\n\t\t} else {\n\t\t\treturn JSON.parseObject(value.toString(), cs);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForFastjson"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson2/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>sa-token-fastjson2</name>\n    <artifactId>sa-token-fastjson2</artifactId>\n    <description>sa-token integrate Fastjson2</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n\t\t\t<groupId>com.alibaba.fastjson2</groupId>\n\t\t\t<artifactId>fastjson2</artifactId>\n\t\t</dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForFastjson2.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.alibaba.fastjson2.JSON;\n\n/**\n * JSON 转换器， Fastjson2 版实现\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaJsonTemplateForFastjson2 implements SaJsonTemplate {\n\n\t/**\n\t * 序列化：对象 -> json 字符串\n\t */\n\t@Override\n\tpublic String objectToJson(Object obj) {\n\t\tif(SaFoxUtil.isEmpty(obj)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn JSON.toJSONString(obj);\n\t}\n\n\t/**\n\t * 反序列化：json 字符串 → 对象\n\t */\n\t@Override\n\tpublic <T>T jsonToObject(String jsonStr, Class<T> type) {\n\t\tif(SaFoxUtil.isEmpty(jsonStr)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn JSON.parseObject(jsonStr, type);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForFastjson2.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.json.SaJsonTemplateForFastjson2;\nimport cn.dev33.satoken.session.SaSessionForFastjson2Customized;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\n/**\n * SaToken 插件安装：JSON 转换器 - Fastjson2 版\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForFastjson2 implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n\n        // 设置 JSON 转换器：Fastjson2 版\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson2());\n\n        // 重写 SaSession 生成策略\n        SaStrategy.instance.createSession = SaSessionForFastjson2Customized::new;\n\n        // 指定 SaSession 类型\n        SaStrategy.instance.sessionClassType = SaSessionForFastjson2Customized.class;\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson2/src/main/java/cn/dev33/satoken/session/SaSessionForFastjson2Customized.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.alibaba.fastjson2.JSON;\nimport com.alibaba.fastjson2.JSONObject;\n\n/**\n * Fastjson2 定制版 SaSession，重写类型转换API\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaSessionForFastjson2Customized extends SaSession {\n\n\tprivate static final long serialVersionUID = -7600983549653130681L;\n\n\t/**\n\t * 构建一个 SaSession 对象\n\t */\n\tpublic SaSessionForFastjson2Customized() {\n\t\tsuper();\n\t}\n\n\t/**\n\t * 构建一个 SaSession 对象\n\t * @param id Session 的 id\n\t */\n\tpublic SaSessionForFastjson2Customized(String id) {\n\t\tsuper(id);\n\t}\n\n\t/**\n\t * 取值 (指定转换类型)\n\t * @param <T> 泛型\n\t * @param key key \n\t * @param cs 指定转换类型 \n\t * @return 值\n\t */\n\t@Override\n\tpublic <T> T getModel(String key, Class<T> cs) {\n\t\t// 如果是想取出为基础类型\n\t\tObject value = get(key);\n\t\tif(SaFoxUtil.isBasicType(cs)) {\n\t\t\treturn SaFoxUtil.getValueByType(value, cs);\n\t\t}\n\t\t// 为空提前返回\n\t\tif(valueIsNull(value)) {\n\t\t\treturn null;\n\t\t}\n\t\t// 如果是 JSONObject 类型直接转，否则先转为 String 再转\n\t\tif(value instanceof JSONObject) {\n\t\t\tJSONObject jo = (JSONObject) value;\n\t\t\treturn jo.to(cs);\n\t\t} else {\n\t\t\treturn JSON.parseObject(value.toString(), cs);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-fastjson2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForFastjson2"
  },
  {
    "path": "sa-token-plugin/sa-token-forest/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>sa-token-forest</name>\n    <artifactId>sa-token-forest</artifactId>\n    <description>sa-token integrate Forest</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.dtflys.forest</groupId>\n            <artifactId>forest-core</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForForest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.http;\n\nimport cn.dev33.satoken.SaManager;\nimport com.dtflys.forest.Forest;\n\nimport java.util.Map;\n\n/**\n * Http 请求处理器， Forest 版实现\n * \n * @author click33\n * @since 1.43.0\n */\npublic class SaHttpTemplateForForest implements SaHttpTemplate {\n\n\t@Override\n\tpublic String get(String url) {\n\t\tSaManager.log.debug(\"发起请求，GET：{}\", url);\n\t\tString res = Forest.get(url).executeAsString();\n\t\tSaManager.log.debug(\"返回结果：{}\", res);\n\t\treturn res;\n\t}\n\n\t@Override\n\tpublic String postByFormData(String url, Map<String, Object> params) {\n\t\tSaManager.log.debug(\"发起请求，POST：{}\\t参数：{}\", url, params);\n\t\tString res = Forest.post(url).addBody(params).executeAsString();\n\t\tSaManager.log.debug(\"返回结果：{}\", res);\n\t\treturn res;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-forest/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForForest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.http.SaHttpTemplateForForest;\nimport com.dtflys.forest.config.ForestConfiguration;\n\n/**\n * SaToken 插件安装：Http 请求处理器 - Forest 版\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaTokenPluginForForest implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        // 关闭 Forest 默认日志打印\n        ForestConfiguration.getDefaultConfiguration().setLogEnabled(false);\n\n        // 设置 Forest 作为 Http 请求处理器\n        SaManager.setSaHttpTemplate(new SaHttpTemplateForForest());\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-forest/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForForest"
  },
  {
    "path": "sa-token-plugin/sa-token-freemarker/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-freemarker</name>\n    <artifactId>sa-token-freemarker</artifactId>\n\t<description>sa-token-freemarker</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- freemarker -->\n\t\t<dependency>\n\t\t\t<groupId>org.freemarker</groupId>\n\t\t\t<artifactId>freemarker</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateDirectiveModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.freemarker.dialect;\n\nimport freemarker.core.Environment;\nimport freemarker.template.TemplateDirectiveBody;\nimport freemarker.template.TemplateDirectiveModel;\nimport freemarker.template.TemplateException;\nimport freemarker.template.TemplateModel;\n\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.function.Function;\n\n/**\n * Sa-Token Freemarker 标签模板指令模型\n *\n * @author click33\n * @since 1.40.0\n */\npublic class SaTokenTemplateDirectiveModel implements TemplateDirectiveModel {\n\n    /*\n     * 参考资料：\n     *  - https://blog.csdn.net/m0_64210833/article/details/135994864\n     *  - https://blog.csdn.net/qq_35752835/article/details/111321893\n     */\n\n    /**\n     * 使用标签指令模板时，指定值的属性名\n     */\n    String attrName;\n\n    /**\n     * 断言函数，返回 true 时标签内容显示，返回 false 时标签内容不显示\n     */\n    Function <String, Boolean> fun;\n\n    public SaTokenTemplateDirectiveModel(String attrName, Function <String, Boolean> fun) {\n        this.attrName = attrName;\n        this.fun = fun;\n    }\n\n    @Override\n    public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody)\n            throws TemplateException, IOException {\n\n        // 获取 value\n        Object obj = map.get(attrName);\n        String value = obj == null ? null : obj.toString();\n\n        // 使用断言函数判断是否显示标签内容\n        if(this.fun.apply(value)) {\n            templateDirectiveBody.render(environment.getOut());\n        }\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-freemarker/src/main/java/cn/dev33/satoken/freemarker/dialect/SaTokenTemplateModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.freemarker.dialect;\n\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport freemarker.template.SimpleHash;\n\nimport java.util.List;\n\n/**\n * Sa-Token Freemarker 标签模板模型\n *\n * @author click33\n * @since 1.40.0\n */\npublic class SaTokenTemplateModel extends SimpleHash {\n\n    /**\n     * 默认值属性名\n     */\n    public static final String DEFAULT_ATTR_NAME = \"value\";\n\n    /**\n     * 底层使用的 StpLogic\n     */\n    public StpLogic stpLogic;\n\n    /**\n     * 使用默认参数注册标签模板模型\n     */\n    public SaTokenTemplateModel() {\n        this(DEFAULT_ATTR_NAME, StpUtil.stpLogic);\n    }\n\n    /**\n     * 构造标签模板模型，使用自定义参数\n     *\n     * @param stpLogic 使用的 StpLogic 对象\n     */\n    public SaTokenTemplateModel(StpLogic stpLogic) {\n        this(DEFAULT_ATTR_NAME, stpLogic);\n    }\n\n    /**\n     * 构造标签模板模型，使用自定义参数\n     *\n     * @param attrName 属性名\n     * @param stpLogic 使用的 StpLogic 对象\n     */\n    public SaTokenTemplateModel(String attrName, StpLogic stpLogic) {\n        this.stpLogic = stpLogic;\n\n        // 登录判断\n        put(\"login\", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.isLogin()));\n        put(\"notLogin\", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.isLogin()));\n\n        // 角色判断\n        put(\"hasRole\", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRole(value)));\n        put(\"hasRoleAnd\", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleAnd(toArray(value))));\n        put(\"hasRoleOr\", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasRoleOr(toArray(value))));\n        put(\"notRole\", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value)));\n        put(\"lackRole\", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasRole(value)));\n\n        // 权限判断\n        put(\"hasPermission\", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermission(value)));\n        put(\"hasPermissionAnd\", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionAnd(toArray(value))));\n        put(\"hasPermissionOr\", new SaTokenTemplateDirectiveModel(attrName, value -> stpLogic.hasPermissionOr(toArray(value))));\n        put(\"notPermission\", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value)));\n        put(\"lackPermission\", new SaTokenTemplateDirectiveModel(attrName, value -> ! stpLogic.hasPermission(value)));\n\n    }\n\n    /**\n     * String 转 Array\n     * @param str 字符串\n     * @return 数组\n     */\n    public String[] toArray(String str) {\n        List<String> list = SaFoxUtil.convertStringToList(str);\n        return list.toArray(new String[0]);\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n    \n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n    <name>sa-token-grpc</name>\n    <artifactId>sa-token-grpc</artifactId>\n    <description>sa-token-grpc</description>\n\n    <properties>\n        <maven.compiler.source>8</maven.compiler.source>\n        <maven.compiler.target>8</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>net.devh</groupId>\n            <artifactId>grpc-spring-boot-starter</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/constants/GrpcContextConstants.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.constants;\n\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport io.grpc.Metadata;\n\n/**\n * 常量 \n * \n * @author lym\n * @since 1.34.0\n */\npublic class GrpcContextConstants {\n    public static final Metadata.Key<String> SA_SAME_TOKEN =\n            Metadata.Key.of(SaSameUtil.SAME_TOKEN, Metadata.ASCII_STRING_MARSHALLER);\n\n    public static final Metadata.Key<String> SA_JUST_CREATED_NOT_PREFIX =\n            Metadata.Key.of(SaTokenConsts.JUST_CREATED_NOT_PREFIX, Metadata.ASCII_STRING_MARSHALLER);\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/context/SaTokenGrpcContext.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.context;\n\nimport io.grpc.*;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * @author lym\n * @since 1.34.0\n **/\npublic class SaTokenGrpcContext {\n    /**\n     * grpc请求上下文。请求完成后会由grpc自动清空\n     *\n     * @see Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler)\n     */\n    private static final Context.Key<Map<String, Object>> SA_TOKEN_CONTEXT_KEY =\n            Context.key(\"sa-token-context\");\n\n    public static Object get(String key) {\n        return SA_TOKEN_CONTEXT_KEY.get().get(key);\n    }\n\n    public static void set(String key, Object value) {\n        SA_TOKEN_CONTEXT_KEY.get().put(key, value);\n    }\n\n    public static void removeKey(String key) {\n        SA_TOKEN_CONTEXT_KEY.get().remove(key);\n    }\n\n    public static Map<String, Object> getContext() {\n        return SA_TOKEN_CONTEXT_KEY.get();\n    }\n\n    public static boolean isNotNull() {\n        return SA_TOKEN_CONTEXT_KEY.get() != null;\n    }\n\n    public static Context create() {\n        return Context.current().withValue(SaTokenGrpcContext.SA_TOKEN_CONTEXT_KEY, new HashMap<>());\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenContextGrpcServerInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.interceptor;\n\nimport cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext;\nimport io.grpc.*;\nimport net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor;\nimport org.springframework.core.Ordered;\n\n/**\n * 处理请求前，创建上下文\n * \n * @author lym\n * @since 1.34.0\n */\n@GrpcGlobalServerInterceptor\npublic class SaTokenContextGrpcServerInterceptor implements ServerInterceptor, Ordered {\n    @Override\n    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {\n        Context ctx = SaTokenGrpcContext.create();\n        return Contexts.interceptCall(ctx, call, headers, next);\n    }\n\n    /**\n     * 必须最先创建上下文，后面的拦截器才能获取到上下文\n     */\n    @Override\n    public int getOrder() {\n        return HIGHEST_PRECEDENCE;\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcClientInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.interceptor;\n\nimport org.springframework.core.Ordered;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaTokenContextDefaultImpl;\nimport cn.dev33.satoken.context.grpc.constants.GrpcContextConstants;\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport io.grpc.CallOptions;\nimport io.grpc.Channel;\nimport io.grpc.ClientCall;\nimport io.grpc.ClientInterceptor;\nimport io.grpc.ForwardingClientCall;\nimport io.grpc.ForwardingClientCallListener;\nimport io.grpc.Metadata;\nimport io.grpc.MethodDescriptor;\nimport io.grpc.Status;\nimport net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor;\n\n\n/**\n * 客户端请求的时候，带上token\n * \n * @author lym\n * @since 1.34.0\n */\n@GrpcGlobalClientInterceptor\npublic class SaTokenGrpcClientInterceptor implements ClientInterceptor, Ordered {\n    @Override\n    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,\n                                                               CallOptions callOptions, Channel next) {\n        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {\n            @Override\n            public void start(Listener<RespT> responseListener, Metadata headers) {\n\n                // 追加 Same-Token 参数\n                if (SaManager.getConfig().getCheckSameToken()) {\n                    headers.put(GrpcContextConstants.SA_SAME_TOKEN, SaSameUtil.getToken());\n                }\n\n                // 调用前，传递会话Token\n                String tokenValue = StpUtil.getTokenValue();\n                if (SaFoxUtil.isNotEmpty(tokenValue)\n                        && SaManager.getSaTokenContext() != SaTokenContextDefaultImpl.defaultContext) {\n                    headers.put(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX, tokenValue);\n                }\n\n                super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>(responseListener) {\n                    /**\n                     * 服务端结束响应后，解析回传的Token值\n                     */\n                    @Override\n                    public void onClose(Status status, Metadata responseHeader) {\n                        StpUtil.setTokenValue(responseHeader.get(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX));\n                        super.onClose(status, responseHeader);\n                    }\n                }, headers);\n            }\n        };\n    }\n\n\n    @Override\n    public int getOrder() {\n        return HIGHEST_PRECEDENCE;\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/interceptor/SaTokenGrpcServerInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.interceptor;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.grpc.constants.GrpcContextConstants;\nimport cn.dev33.satoken.context.grpc.util.SaTokenContextGrpcUtil;\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport io.grpc.*;\nimport net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor;\n\n/**\n * 鉴权，设置token\n * \n * @author lym\n * @since 1.34.0\n **/\n@GrpcGlobalServerInterceptor\npublic class SaTokenGrpcServerInterceptor implements ServerInterceptor {\n    @Override\n    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {\n        try{\n            // 初始化上下文\n            SaTokenContextGrpcUtil.setContext();\n\n            // RPC 调用鉴权\n            if (SaManager.getConfig().getCheckSameToken()) {\n                String sameToken = headers.get(GrpcContextConstants.SA_SAME_TOKEN);\n                SaSameUtil.checkToken(sameToken);\n            }\n            String tokenFromClient = headers.get(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX);\n            StpUtil.setTokenValue(tokenFromClient);\n\n            return next.startCall(new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {\n                /**\n                 * 结束响应时，若本服务生成了新token，将其传回客户端\n                 */\n                @Override\n                public void close(Status status, Metadata responseHeaders) {\n                    String justCreateToken = StpUtil.getTokenValue();\n                    if (!SaFoxUtil.equals(justCreateToken, tokenFromClient) && SaFoxUtil.isNotEmpty(justCreateToken)) {\n                        responseHeaders.put(GrpcContextConstants.SA_JUST_CREATED_NOT_PREFIX, justCreateToken);\n                    }\n                    super.close(status, responseHeaders);\n                }\n            }, headers);\n        }finally {\n            // 清除上下文\n            SaTokenContextGrpcUtil.clearContext();\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaRequestForGrpc.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.model;\n\nimport cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext;\nimport cn.dev33.satoken.context.model.SaRequest;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * Request for grpc\n *\n * @author lym\n * @since 1.34.0\n */\npublic class SaRequestForGrpc implements SaRequest {\n\n    /**\n     * 获取底层源对象\n     */\n    @Override\n    public Object getSource() {\n        return SaTokenGrpcContext.getContext();\n    }\n\n    /**\n     * 在 [请求体] 里获取一个值\n     */\n    @Override\n    public String getParam(String name) {\n        // 不传播 url 参数\n        return null;\n    }\n\n    /**\n     * 获取 [请求体] 里提交的所有参数名称\n     * @return 参数名称列表\n     */\n    @Override\n    public Collection<String> getParamNames(){\n        return null;\n    }\n\n    /**\n     * 获取 [请求体] 里提交的所有参数\n     * @return 参数列表\n     */\n    @Override\n    public Map<String, String> getParamMap(){\n        return null;\n    }\n\n    /**\n     * 在 [请求头] 里获取一个值\n     */\n    @Override\n    public String getHeader(String name) {\n        // 不传播 header 参数\n        return null;\n    }\n\n    /**\n     * 在 [Cookie作用域] 里获取一个值\n     */\n    @Override\n    public String getCookieValue(String name) {\n        // 不传播 cookie 参数\n        return null;\n    }\n\n    /**\n     * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n     */\n    @Override\n    public String getCookieFirstValue(String name){\n        // 不传播 cookie 参数\n        return null;\n    }\n\n    /**\n     * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n     * @param name 键\n     * @return 值\n     */\n    @Override\n    public String getCookieLastValue(String name){\n        // 不传播 cookie 参数\n        return null;\n    }\n\n    /**\n     * 返回当前请求path (不包括上下文名称)\n     */\n    @Override\n    public String getRequestPath() {\n        // 不传播 requestPath\n        return null;\n    }\n\n    /**\n     * 返回当前请求的url，例：http://xxx.com/test\n     *\n     * @return see note\n     */\n    public String getUrl() {\n        // 不传播 url\n        return null;\n    }\n\n    /**\n     * 返回当前请求的类型\n     */\n    @Override\n    public String getMethod() {\n        // 不传播 method\n        return null;\n    }\n\n    @Override\n    public String getHost() {\n        return null;\n    }\n\n    /**\n     * 转发请求\n     */\n    @Override\n    public Object forward(String path) {\n        // 不传播 forward 动作\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaResponseForGrpc.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.model;\n\nimport cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext;\nimport cn.dev33.satoken.context.model.SaResponse;\n\n/**\n * Response for grpc\n *\n * @author lym\n * @since 1.34.0\n */\npublic class SaResponseForGrpc implements SaResponse {\n    /**\n     * 获取底层源对象\n     */\n    @Override\n    public Object getSource() {\n        return SaTokenGrpcContext.getContext();\n    }\n\n    /**\n     * 设置响应状态码\n     */\n    @Override\n    public SaResponse setStatus(int sc) {\n        // 不回传 status 状态\n        return this;\n    }\n\n    /**\n     * 在响应头里写入一个值\n     */\n    @Override\n    public SaResponse setHeader(String name, String value) {\n        // 不回传 header响应头\n        return this;\n    }\n\n    /**\n     * 在响应头里添加一个值\n     *\n     * @param name  名字\n     * @param value 值\n     * @return 对象自身\n     */\n    public SaResponse addHeader(String name, String value) {\n        // 不回传 header响应头\n        return this;\n    }\n\n    /**\n     * 重定向\n     */\n    @Override\n    public Object redirect(String url) {\n        // 不回传 重定向 动作\n        return null;\n    }\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/model/SaStorageForGrpc.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.model;\n\nimport cn.dev33.satoken.context.grpc.context.SaTokenGrpcContext;\nimport cn.dev33.satoken.context.model.SaStorage;\n\n/**\n * Storage for grpc\n *\n * @author lym\n * @since 1.34.0\n */\npublic class SaStorageForGrpc implements SaStorage {\n\n    /**\n     * 获取底层源对象\n     */\n    @Override\n    public Object getSource() {\n        return SaTokenGrpcContext.getContext();\n    }\n\n    /**\n     * 在 [Request作用域] 里写入一个值\n     */\n    @Override\n    public SaStorage set(String key, Object value) {\n        SaTokenGrpcContext.set(key, value);\n        return this;\n    }\n\n    /**\n     * 在 [Request作用域] 里获取一个值\n     */\n    @Override\n    public Object get(String key) {\n        return SaTokenGrpcContext.get(key);\n    }\n\n    /**\n     * 在 [Request作用域] 里删除一个值\n     */\n    @Override\n    public SaStorage delete(String key) {\n        SaTokenGrpcContext.removeKey(key);\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/java/cn/dev33/satoken/context/grpc/util/SaTokenContextGrpcUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.context.grpc.util;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.grpc.model.SaRequestForGrpc;\nimport cn.dev33.satoken.context.grpc.model.SaResponseForGrpc;\nimport cn.dev33.satoken.context.grpc.model.SaStorageForGrpc;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\n\n\n/**\n * SaTokenContext 上下文读写工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextGrpcUtil {\n\n\t/**\n\t * 写入当前上下文\n\t */\n\tpublic static void setContext() {\n\t\tSaRequest saRequest = new SaRequestForGrpc();\n\t\tSaResponse saResponse = new SaResponseForGrpc();\n\t\tSaStorage saStorage = new SaStorageForGrpc();\n\t\tSaManager.getSaTokenContext().setContext(saRequest, saResponse, saStorage);\n\t}\n\n\t/**\n\t * 清除当前上下文\n\t */\n\tpublic static void clearContext() {\n\t\tSaManager.getSaTokenContext().clearContext();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcClientInterceptor\ncn.dev33.satoken.context.grpc.interceptor.SaTokenContextGrpcServerInterceptor\ncn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcServerInterceptor\ncn.dev33.satoken.context.grpc.SaTokenSecondContextCreatorForGrpc"
  },
  {
    "path": "sa-token-plugin/sa-token-grpc/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\n  cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcClientInterceptor,\\\n  cn.dev33.satoken.context.grpc.interceptor.SaTokenContextGrpcServerInterceptor,\\\n  cn.dev33.satoken.context.grpc.interceptor.SaTokenGrpcServerInterceptor,\\\n  cn.dev33.satoken.context.grpc.SaTokenSecondContextCreatorForGrpc"
  },
  {
    "path": "sa-token-plugin/sa-token-hutool-timed-cache/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-hutool-timed-cache</name>\n    <artifactId>sa-token-hutool-timed-cache</artifactId>\n\t<description>sa-token integrate hutool-TimedCache</description>\n\n\t<dependencies>\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>cn.hutool</groupId>\n            <artifactId>hutool-cache</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForHutoolTimedCache.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.auto.SaTokenDaoByStringFollowObject;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.hutool.cache.CacheUtil;\nimport cn.hutool.cache.impl.CacheObj;\nimport cn.hutool.cache.impl.TimedCache;\n\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * Sa-Token 持久层接口（基于 Hutool-TimedCache，系统重启后数据丢失）\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaTokenDaoForHutoolTimedCache implements SaTokenDaoByStringFollowObject {\n\n\t//\n\t/**\n\t * 底层缓存对象：\n\t * 参数填1000，代表默认ttl为1000毫秒，实际上此参数意义不大，因为后续每个值都会单独设置自己的ttl值\n\t */\n\tpublic TimedCache<String, Object> timedCache = CacheUtil.newTimedCache(1000);\n\n\n\t// ------------------------ Object 读写操作\n\n\t@Override\n\tpublic Object getObject(String key) {\n\t\t// 第二个参数代表：是否刷新最后访问时间\n\t\t// 设置为false，因为我们不需要刷新最后访问时间，只需要取值即可\n\t\treturn timedCache.get(key, false);\n\t}\n\n\t@Override\n\tpublic <T> T getObject(String key, Class<T> classType) {\n\t\treturn (T) getObject(key);\n\t}\n\n\t@Override\n\tpublic void setObject(String key, Object object, long timeout) {\n\t\tif(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {\n\t\t\treturn;\n\t\t}\n\t\t// 如果为永不过期\n\t\t// \t\t在 sa-token 中，-1 代表永不过期\n\t\t// \t\t在 hutool-TimedCache 中，0 代表永不过期\n\t\t// \t\t为了适应 hutool-TimedCache 规范，这里将 -1 转换为 0\n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\ttimedCache.put(key, object, 0);\n\t\t\treturn;\n\t\t}\n\t\t// 正常情况\n\t\ttimedCache.put(key, object, timeout * 1000);\n\t}\n\n\t@Override\n\tpublic void updateObject(String key, Object object) {\n\t\tlong expire = getObjectTimeout(key);\n\t\t// -2 = 无此键\n\t\tif(expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn;\n\t\t}\n\t\tthis.setObject(key, object, expire);\n\t}\n\n\t@Override\n\tpublic void deleteObject(String key) {\n\t\ttimedCache.remove(key);\n\t}\n\n\t@Override\n\tpublic long getObjectTimeout(String key) {\n\t\treturn getKeyTimeout(key);\n\t}\n\n\t@Override\n\tpublic void updateObjectTimeout(String key, long timeout) {\n\t\t// $$待优化：对一个不存在的key进行修改timeout操作时，可能会造成一些意外数据，待进一步测试\n\t\tthis.setObject(key, this.getObject(key), timeout);\n\t}\n\n\n\t// ------------------------ Session 读写操作\n\t// 使用接口默认实现\n\n\n\t// --------- 会话管理\n\n\t@Override\n\tpublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\t\treturn SaFoxUtil.searchList(timedCache.keySet(), prefix, keyword, start, size, sortType);\n\t}\n\n\n\n\t// --------- 过期时间相关操作\n\n\t/**\n\t * 获取指定 key 的剩余存活时间 （单位：秒）\n\t * @param key 指定 key\n\t * @return 这个 key 的剩余存活时间，返回-1=永不过期，返回-2=无此键\n\t */\n\tlong getKeyTimeout(String key) {\n\t\tfinal Iterator<CacheObj<String, Object>> values = timedCache.cacheObjIterator();\n\t\tCacheObj<String, Object> co;\n\t\twhile (values.hasNext()) {\n\t\t\tco = values.next();\n\t\t\tif(co.getKey().equals(key)) {\n\t\t\t\tlong ttl = co.getTtl();\n\t\t\t\t// 在 Hutool-TimedCache 中，ttl=0 (或<0) 代表永不过期，统一返回 Sa-Token 可以理解的 -1\n\t\t\t\tif(ttl <= 0) {\n\t\t\t\t\treturn NEVER_EXPIRE;\n\t\t\t\t}\n\t\t\t\t// 不为 0，那就计算一下剩余有效期\n\t\t\t\t// 单位：毫秒\n\t\t\t\tlong timeout = ttl - (System.currentTimeMillis() - co.getLastAccess());\n\t\t\t\tif(timeout < 0) {\n\t\t\t\t\ttimeout = 0;\n\t\t\t\t}\n\t\t\t\t// 转秒返回\n\t\t\t\treturn timeout / 1000;\n\t\t\t}\n\t\t}\n\t\t// 代码至此，说明缓存中没有这个值\n\t\treturn NOT_VALUE_EXPIRE;\n\t}\n\n\t// --------- 定时清理过期数据\n\n\t/**\n\t * 组件被安装时，开始刷新数据线程\n\t */\n\t@Override\n\tpublic void init() {\n\t\t// 定时清理间隔\n\t\tint dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod();\n\t\t// 配置为<=0代表不启用定时清理\n\t\tif(dataRefreshPeriod <= 0) {\n\t\t\treturn;\n\t\t}\n\t\t// 启用定时清理（转毫秒）\n\t\ttimedCache.schedulePrune(dataRefreshPeriod * 1000L);\n\t}\n\n\t/**\n\t * 组件被卸载时，结束定时任务，不再定时清理过期数据\n\t */\n\t@Override\n\tpublic void destroy() {\n\t\ttimedCache.cancelPruneSchedule();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-hutool-timed-cache/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForHutoolCache.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDaoForHutoolTimedCache;\n\n/**\n * SaToken 插件安装：DAO 扩展 - Hutool-TimedCache 版\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForHutoolCache implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n\n        SaManager.setSaTokenDao(new SaTokenDaoForHutoolTimedCache());\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-hutool-timed-cache/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForHutoolCache"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-jackson</name>\n    <artifactId>sa-token-jackson</artifactId>\n\t<description>sa-token-jackson</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- jackson-databind -->\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<!-- jackson-datatype-jsr310 -->\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.datatype</groupId>\n\t\t\t<artifactId>jackson-datatype-jsr310</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\nimport cn.dev33.satoken.exception.SaJsonConvertException;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;\nimport com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;\nimport com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;\nimport com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;\nimport com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;\nimport com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;\nimport com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;\n\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Map;\n\n/**\n * JSON 转换器， Jackson 版实现\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaJsonTemplateForJackson implements SaJsonTemplate {\n\n\tpublic static final String DATE_TIME_PATTERN = \"yyyy-MM-dd HH:mm:ss\";\n\tpublic static final String DATE_PATTERN = \"yyyy-MM-dd\";\n\tpublic static final String TIME_PATTERN = \"HH:mm:ss\";\n\tpublic static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);\n\tpublic static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);\n\tpublic static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);\n\n\t/**\n\t * 底层 Mapper 对象\n\t */\n\tpublic ObjectMapper objectMapper = new ObjectMapper();\n\n\tpublic SaJsonTemplateForJackson() {\n\n\t\t// 1、使 objectMapper 序列化时带上类型信息，以便该 json 字符串可以成功反序列化\n\t\t// \t  构建反序列化限制器，此处可以限制只允许指定类型或指定包下的类型才可以反序列化，此处指定所有类型都可以反序列化\n\t\tPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()\n\t\t\t\t// 允许所有子类型反序列化（即反序列化时遇到的类）\n\t\t\t\t.allowIfSubType(Object.class)\n\t\t\t\t// 允许所有基类型反序列化（如 Object、自定义抽象类）\n\t\t\t\t.allowIfBaseType(Object.class)\n\t\t\t\t.build();\n\t\t// \t  启用全局默认类型（嵌入类型信息）\n\t\tobjectMapper.activateDefaultTyping(\n\t\t\t\tptv,\n\t\t\t\t// 对非 final 类嵌入类型信息\n\t\t\t\tObjectMapper.DefaultTyping.NON_FINAL,\n\t\t\t\t// 类型信息以属性形式存在（\"@class\"）\n\t\t\t\tJsonTypeInfo.As.PROPERTY\n\t\t\t\t);\n\n\t\t// 2、使空 bean 在序列化时也能记录类型信息，而不是只序列化成 {}\n\t\tobjectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);\n\n\t\t// 3、配置 [ 忽略未知字段 ]\n\t\tthis.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n\n\t\t// 4、配置 [ 时间类型转换 ]\n\t\tJavaTimeModule timeModule = new JavaTimeModule();\n\t\t// \t\tLocalDateTime序列化与反序列化\n\t\ttimeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));\n\t\ttimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));\n\t\t// \t\tLocalDate序列化与反序列化\n\t\ttimeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));\n\t\ttimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));\n\t\t// \t\tLocalTime序列化与反序列化\n\t\ttimeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));\n\t\ttimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));\n\t\t//\n\t\tthis.objectMapper.registerModule(timeModule);\n\n\t}\n\n\t/**\n\t * 序列化：对象 -> json 字符串\n\t */\n\t@Override\n\tpublic String objectToJson(Object obj) {\n\t\tif(SaFoxUtil.isEmpty(obj)) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tif(obj instanceof Map) {\n\t\t\t\treturn mapObjectMapper.writeValueAsString(obj);\n\t\t\t}\n\t\t\treturn objectMapper.writeValueAsString(obj);\n\t\t} catch (JsonProcessingException e) {\n\t\t\tthrow new SaJsonConvertException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 反序列化：json 字符串 → 对象\n\t */\n\t@Override\n\tpublic <T> T jsonToObject(String jsonStr, Class<T> type) {\n\t\tif(SaFoxUtil.isEmpty(jsonStr)) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n            return objectMapper.readValue(jsonStr, type);\n\t\t} catch (JsonProcessingException e) {\n\t\t\tthrow new SaJsonConvertException(e);\n\t\t}\n\t}\n\n\t/*\n\t * 由于构造方法中的如下代码：\n\t * \t\tObjectMapper.DefaultTyping.NON_FINAL,\n\t * 导致 objectMapper 对所有非 final 类型的反序列化均要求提供 @class 信息。\n\t *\n\t * 例如：\n\t * \t\t一个简单的字符串 {\"name\": \"zhangsan\"} 将无法反序列化为 Map 对象，因为这个字符串上没有提供 @class 信息。\n\t *\n\t * 尝试诸多方案，均未能解决此问题。\n\t *\n\t * 因此，以下代码将为 Map 的反序列化提供一个独立干净的 mapObjectMapper 对象，保证其不受构造方法中关于类型配置的影响。\n\t *\n\t */\n\n\t/**\n\t * 处理 Map 的序列化与反序列化\n\t */\n\tpublic ObjectMapper mapObjectMapper = new ObjectMapper();\n\n\t/**\n\t * 将 json 字符串解析为 Map\n\t */\n\t@Override\n\tpublic Map<String, Object> jsonToMap(String jsonStr) {\n\t\tif(SaFoxUtil.isEmpty(jsonStr)) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tMap<String, Object> map = mapObjectMapper.readValue(jsonStr, Map.class);\n\t\t\treturn map;\n\t\t} catch (JsonProcessingException e) {\n\t\t\tthrow new SaJsonConvertException(e);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.json.SaJsonTemplateDefaultImpl;\nimport cn.dev33.satoken.json.SaJsonTemplateForJackson;\n\n/**\n * SaToken 插件安装：JSON 转换器 (Jackson 版)\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForJackson implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        // 只有在未提供自定义的 json 解析器时才会生效，给于其较弱的优先级\n        if(SaManager.getSaJsonTemplate().getClass() == SaJsonTemplateDefaultImpl.class){\n            SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson());\n        }\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForJackson"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson3/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-jackson3</name>\n    <artifactId>sa-token-jackson3</artifactId>\n\t<description>sa-token-jackson3: Sa-Token 与 Jackson 3 整合</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- jackson-databind 3.x (Java 8 时间支持已内置) -->\n\t\t<dependency>\n\t\t\t<groupId>tools.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-compiler-plugin</artifactId>\n\t\t\t\t<configuration>\n\t\t\t\t\t<source>17</source>\n\t\t\t\t\t<target>17</target>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForJackson3.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\nimport cn.dev33.satoken.exception.SaJsonConvertException;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport tools.jackson.core.JacksonException;\nimport tools.jackson.databind.DeserializationFeature;\nimport tools.jackson.databind.DefaultTyping;\nimport tools.jackson.databind.SerializationFeature;\nimport tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;\nimport tools.jackson.databind.jsontype.PolymorphicTypeValidator;\nimport tools.jackson.databind.json.JsonMapper;\n\nimport java.util.Map;\n\n/**\n * JSON 转换器，Jackson 3 版实现\n *\n * @author click33\n * @since 1.45.0\n */\npublic class SaJsonTemplateForJackson3 implements SaJsonTemplate {\n\n\t/**\n\t * 底层 Mapper 对象（带多态类型信息，用于 Session 等复杂对象序列化）\n\t */\n\tpublic final JsonMapper objectMapper;\n\n\t/**\n\t * 处理 Map 的 Mapper（无多态类型配置，用于简单 JSON 解析）\n\t */\n\tpublic final JsonMapper mapObjectMapper;\n\n\tpublic SaJsonTemplateForJackson3() {\n\t\t// 1、构建反序列化限制器（PolymorphicTypeValidator），限制哪些类型可以被自动多态反序列化\n\t\t//    这里允许所有 Object 类型和其子类型参与多态反序列化，从而支持复杂对象（如 SaSession）的序列化、反序列化\n\t\tPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()\n\t\t\t\t.allowIfSubType(Object.class)\t\t// 允许 Object.class 的子类型被反序列化\n\t\t\t\t.allowIfBaseType(Object.class)\t\t// 允许 Object.class 作为基类型（在启用多态时）\n\t\t\t\t.build();\t\t// 构建 Validator 实例\n\n\t\t// 2、通过 JsonMapper 的 builder 模式创建 objectMapper（用于带多态类型信息的 JSON 处理，Jackson3 默认不可变需用构建器）\n\t\tthis.objectMapper = JsonMapper.builder()\n\t\t\t\t// 启用默认多态类型处理，并以 \"@class\" 作为写入类型信息的属性名（即持久化时包含类型字段），仅对非 final 类型生效\n\t\t\t\t.activateDefaultTypingAsProperty(ptv, DefaultTyping.NON_FINAL, \"@class\")\n\t\t\t\t.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)\t\t// 序列化时如果 bean 没有属性，则不抛出异常\n\t\t\t\t.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\t\t// 反序列化时如果有未知属性，也不抛出异常（兼容性更好）\n\t\t\t\t.build();\t\t// 构建真正的 JsonMapper 实例\n\n\t\t// 3、创建 mapObjectMapper，用于简单类型（如 Map）的序列化和反序列化，和 objectMapper 独立，且不启用多态类型\n\t\tthis.mapObjectMapper = JsonMapper.builder()\n\t\t\t\t.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)\t\t// 序列化时如果 bean 没有属性，则不抛出异常\n\t\t\t\t.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\t\t// 反序列化时如果有未知属性，也不抛出异常，避免出错\n\t\t\t\t.build();\t\t// 构建用于处理简单场景的 JsonMapper 实例\n\t}\n\n\t/**\n\t * 序列化：对象 -> json 字符串\n\t */\n\t@Override\n\tpublic String objectToJson(Object obj) {\n\t\tif (SaFoxUtil.isEmpty(obj)) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tif (obj instanceof Map) {\n\t\t\t\treturn mapObjectMapper.writeValueAsString(obj);\n\t\t\t}\n\t\t\treturn objectMapper.writeValueAsString(obj);\n\t\t} catch (JacksonException e) {\n\t\t\tthrow new SaJsonConvertException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 反序列化：json 字符串 → 对象\n\t */\n\t@Override\n\tpublic <T> T jsonToObject(String jsonStr, Class<T> type) {\n\t\tif (SaFoxUtil.isEmpty(jsonStr)) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\treturn objectMapper.readValue(jsonStr, type);\n\t\t} catch (JacksonException e) {\n\t\t\tthrow new SaJsonConvertException(e);\n\t\t}\n\t}\n\n\t/**\n\t * 将 json 字符串解析为 Map\n\t */\n\t@Override\n\tpublic Map<String, Object> jsonToMap(String jsonStr) {\n\t\tif (SaFoxUtil.isEmpty(jsonStr)) {\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tMap<String, Object> map = mapObjectMapper.readValue(jsonStr, Map.class);\n\t\t\treturn map;\n\t\t} catch (JacksonException e) {\n\t\t\tthrow new SaJsonConvertException(e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForJackson3.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.json.SaJsonTemplateDefaultImpl;\nimport cn.dev33.satoken.json.SaJsonTemplateForJackson3;\n\n/**\n * SaToken 插件安装：JSON 转换器 (Jackson 3 版)\n *\n * @author click33\n * @since 1.45.0\n */\npublic class SaTokenPluginForJackson3 implements SaTokenPlugin {\n\n\t@Override\n\tpublic void install() {\n\t\t// 只有在未提供自定义的 json 解析器时才会生效，给予其较弱的优先级\n\t\tif (SaManager.getSaJsonTemplate().getClass() == SaJsonTemplateDefaultImpl.class) {\n\t\t\tSaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jackson3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForJackson3\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-jwt</name>\n    <artifactId>sa-token-jwt</artifactId>\n\t<description>sa-token-jwt</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- hutool (jwt) -->\n        <dependency>\n\t\t    <groupId>cn.hutool</groupId>\n\t\t    <artifactId>hutool-jwt</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jwt;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.jwt.error.SaJwtErrorCode;\nimport cn.dev33.satoken.jwt.exception.SaJwtException;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.hutool.json.JSONException;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.jwt.JWT;\nimport cn.hutool.jwt.JWTException;\nimport cn.hutool.jwt.signers.JWTSigner;\nimport cn.hutool.jwt.signers.JWTSignerUtil;\n\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * jwt 操作模板方法封装\n *\n * @author click33\n * @since 1.31.0\n */\npublic class SaJwtTemplate {\n\t\n\t/**\n\t * key：账号类型  \n\t */\n\tpublic static final String LOGIN_TYPE = \"loginType\"; \n\t\n\t/**\n\t * key：账号id  \n\t */\n\tpublic static final String LOGIN_ID = \"loginId\"; \n\t\n\t/**\n\t * key：登录设备类型\n\t */\n\tpublic static final String DEVICE_TYPE = \"deviceType\";\n\t\n\t/**\n\t * key：有效截止期 (时间戳) \n\t */\n\tpublic static final String EFF = \"eff\";\n\n\t/**\n\t * key：乱数 （ 混入随机字符串，防止每次生成的 token 都是一样的 ）\n\t */\n\tpublic static final String RN_STR = \"rnStr\";\n\n\t/** \n\t * 当有效期被设为此值时，代表永不过期 \n\t */ \n\tpublic static final long NEVER_EXPIRE = SaTokenDao.NEVER_EXPIRE;\n\n\t/** \n\t * 表示一个值不存在 \n\t */ \n\tpublic static final long NOT_VALUE_EXPIRE = SaTokenDao.NOT_VALUE_EXPIRE;\n\t\n\t// ------ 创建\n\n\t/**\n\t * 创建 jwt （简单方式）\n\t *\n     * @param loginType 登录类型 \n\t * @param loginId 账号id \n\t * @param extraData 扩展数据\n     * @param keyt 秘钥\n\t * @return jwt-token \n\t */\n    public String createToken(String loginType, Object loginId, Map<String, Object> extraData, String keyt) {\n    \t\n    \t// 构建\n    \tJWT jwt = JWT.create()\n\t\t\t\t.setPayload(LOGIN_TYPE, loginType)\n\t\t\t    .setPayload(LOGIN_ID, loginId)\n\t\t\t\t// 塞入一个随机字符串，防止同账号下每次生成的 token 都一样的\n\t\t\t    .setPayload(RN_STR, SaFoxUtil.getRandomString(32))\n\t\t\t\t.addPayloads(extraData)\n\t\t\t    ;\n    \t\n    \t// 返回 \n\t\treturn generateToken(jwt, keyt);\n    }\n\n\t/**\n\t * 创建 jwt （全参数方式）\n\t *\n\t * @param loginType 账号类型\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型\n\t * @param timeout token有效期 (单位 秒)\n\t * @param extraData 扩展数据\n\t * @param keyt 秘钥\n\t * @return jwt-token\n\t */\n\tpublic String createToken(String loginType, Object loginId, String deviceType,\n\t\t\t\t\t\t\t\t\t long timeout, Map<String, Object> extraData, String keyt) {\n\n\t\t// 计算 eff 有效期：\n\t\t// \t\t如果 timeout 指定为 -1，那么 eff 也为 -1，代表永不过期\n\t\t// \t\t如果 timeout 指定为一个具体的值，那么 eff 为 13 位时间戳，代表此 token 到期的时间\n\t\tlong effTime = timeout;\n\t\tif(timeout != NEVER_EXPIRE) {\n\t\t\teffTime = timeout * 1000 + System.currentTimeMillis();\n\t\t}\n\n\t\t// 创建  \n\t\tJWT jwt = JWT.create()\n\t\t\t\t.setPayload(LOGIN_TYPE, loginType)\n\t\t\t\t.setPayload(LOGIN_ID, loginId)\n\t\t\t\t.setPayload(DEVICE_TYPE, deviceType)\n\t\t\t\t.setPayload(EFF, effTime)\n\t\t\t\t// 塞入一个随机字符串，防止同账号同一毫秒下每次生成的 token 都一样的\n\t\t\t    .setPayload(RN_STR, SaFoxUtil.getRandomString(32))\n\t\t\t\t.addPayloads(extraData);\n\n\t\t// 返回\n\t\treturn generateToken(jwt, keyt);\n\t}\n\n\t/**\n\t * 为 JWT 对象和 keyt 秘钥，生成 token 字符串\n\t *\n\t * @param jwt JWT构建对象\n\t * @param keyt 秘钥 \n\t * @return 根据 JWT 对象和 keyt 秘钥，生成的 token 字符串\n\t */\n\tpublic String generateToken (JWT jwt, String keyt) {\n\t\treturn jwt.setSigner(createSigner(keyt)).sign();\n\t}\n\n\t/**\n\t * 返回 jwt 使用的签名算法\n\t *\n\t * @param keyt 秘钥\n\t * @return /\n\t */\n\tpublic JWTSigner createSigner (String keyt) {\n\t\treturn JWTSignerUtil.hs256(keyt.getBytes());\n\t}\n\n\t// ------ 解析 \n\n    /**\n     * jwt 解析\n\t *\n     * @param token Jwt-Token值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @param isCheckTimeout 是否校验 timeout 字段\n     * @return 解析后的jwt 对象 \n     */\n    public JWT parseToken(String token, String loginType, String keyt, boolean isCheckTimeout) {\n\n    \t// 秘钥不可以为空\n    \tif(SaFoxUtil.isEmpty(keyt)) {\n    \t\tthrow new SaJwtException(\"请配置 jwt 秘钥\");\n    \t}\n\n    \t// 如果token为null \n    \tif(token == null) {\n    \t\tthrow new SaJwtException(\"jwt 字符串不可为空\");\n    \t}\n    \t\n    \t// 解析 \n    \tJWT jwt;\n    \ttry {\n    \t\tjwt = JWT.of(token);\n\t\t} catch (JWTException | JSONException e) {\n    \t\tthrow new SaJwtException(\"jwt 解析失败：\" + token, e).setCode(SaJwtErrorCode.CODE_30201);\n\t\t}\n    \tJSONObject payloads = jwt.getPayloads();\n    \t\n    \t// 校验 Token 签名\n\t\tboolean verify = jwt.setSigner(createSigner(keyt)).verify();\n    \tif( ! verify) {\n    \t\tthrow new SaJwtException(\"jwt 签名无效：\" + token).setCode(SaJwtErrorCode.CODE_30202);\n    \t}\n\n    \t// 校验 loginType \n    \tif( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) {\n    \t\tthrow new SaJwtException(\"jwt loginType 无效：\" + token).setCode(SaJwtErrorCode.CODE_30203);\n    \t}\n    \t\n    \t// 校验 Token 有效期\n    \tif(isCheckTimeout) {\n    \t\tLong effTime = payloads.getLong(EFF, 0L);\n        \tif(effTime != NEVER_EXPIRE) {\n        \t\tif(effTime == null || effTime < System.currentTimeMillis()) {\n        \t\t\tthrow new SaJwtException(\"jwt 已过期：\" + token).setCode(SaJwtErrorCode.CODE_30204);\n        \t\t}\n        \t}\n    \t}\n    \t\n        // 返回 \n        return jwt;\n    }\n\n    /**\n     * 获取 jwt 数据载荷 （校验 sign、loginType、timeout） \n     * @param token token值\n     * @param loginType 登录类型 \n     * @param keyt 秘钥 \n     * @return 载荷 \n     */\n    public JSONObject getPayloads(String token, String loginType, String keyt) {\n    \treturn parseToken(token, loginType, keyt, true).getPayloads();\n    }\n\n    /**\n     * 获取 jwt 数据载荷 （校验 sign、loginType，不校验 timeout） \n     * @param token token值\n     * @param loginType 登录类型 \n     * @param keyt 秘钥 \n     * @return 载荷 \n     */\n    public JSONObject getPayloadsNotCheck(String token, String loginType, String keyt) {\n    \treturn parseToken(token, loginType, keyt, false).getPayloads();\n    }\n    \n    /**\n     * 获取 jwt 代表的账号id \n     * @param token Token值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public Object getLoginId(String token, String loginType, String keyt) {\n    \treturn getPayloads(token, loginType, keyt).get(LOGIN_ID);\n    }\n\n    /**\n     * 获取 jwt 代表的账号id (未登录时返回null)\n     * @param token Token值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public Object getLoginIdOrNull(String token, String loginType, String keyt) {\n    \ttry {\n    \t\treturn getPayloads(token, loginType, keyt).get(LOGIN_ID);\n\t\t} catch (SaJwtException e) {\n\t\t\treturn null;\n\t\t}\n    }\n\n    /**\n     * 获取 jwt 剩余有效期 \n     * @param token JwtToken值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public long getTimeout(String token, String loginType, String keyt) {\n    \t\n    \t// 如果token为null \n    \tif(token == null) {\n    \t\treturn NOT_VALUE_EXPIRE;\n    \t}\n    \t\n    \t// 取出数据 \n    \tJWT jwt;\n    \ttry {\n    \t\tjwt = JWT.of(token);\n\t\t} catch (JWTException e) {\n\t\t\t// 解析失败 \n\t\t\treturn NOT_VALUE_EXPIRE;\n\t\t}\n    \tJSONObject payloads = jwt.getPayloads();\n    \t\n    \t// 如果签名无效 \n    \tboolean verify = jwt.setSigner(createSigner(keyt)).verify();\n    \tif( ! verify) {\n    \t\treturn NOT_VALUE_EXPIRE;\n    \t}\n\n    \t// 如果 loginType  无效 \n    \tif( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) {\n    \t\treturn NOT_VALUE_EXPIRE;\n    \t}\n    \t\n    \t// 如果被设置为：永不过期 \n    \tLong effTime = payloads.get(EFF, Long.class);\n    \tif(effTime == NEVER_EXPIRE) {\n    \t\treturn NEVER_EXPIRE;\n    \t}\n    \t// 如果已经超时 \n    \tif(effTime == null || effTime < System.currentTimeMillis()) {\n    \t\treturn NOT_VALUE_EXPIRE;\n    \t}\n    \t\n        // 计算timeout (转化为以秒为单位的有效时间)\n        return (effTime - System.currentTimeMillis()) / 1000;\n    }\n\n\n\n\t// -------------- 其它方法\n\n\t/**\n\t * 创建 jwt （Map 参数方式）\n\t *\n\t * @param map 扩展数据\n\t * @param keyt 秘钥\n\t * @return jwt-token\n\t */\n\tpublic String createToken(Map<String, Object> map, String keyt) {\n\t\t// 创建\n\t\tJWT jwt = JWT.create().addPayloads(map);\n\n\t\t// 返回\n\t\treturn generateToken(jwt, keyt);\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jwt;\n\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.jwt.JWT;\n\nimport java.util.Map;\n\n/**\n * jwt 操作工具类封装\n *\n * @author click33\n * @since 1.27.1\n */\npublic class SaJwtUtil {\n\t\n\t/**\n\t * 底层 saJwtTemplate 对象 \n\t */\n\tpublic static SaJwtTemplate saJwtTemplate = new SaJwtTemplate();\n\n\t/**\n\t * 获取底层 saJwtTemplate 对象 \n\t * @return /\n\t */\n\tpublic static SaJwtTemplate getSaJwtTemplate() {\n\t\treturn saJwtTemplate;\n\t}\n\n\t/**\n\t * 设置底层 saJwtTemplate 对象 \n\t * @param saJwtTemplate / \n\t */\n\tpublic static void setSaJwtTemplate(SaJwtTemplate saJwtTemplate) {\n\t\tSaJwtUtil.saJwtTemplate = saJwtTemplate;\n\t}\n\t\n\t// 常量\n\t\n\n\t/**\n\t * key：账号类型  \n\t */\n\tpublic static final String LOGIN_TYPE = SaJwtTemplate.LOGIN_TYPE; \n\t\n\t/**\n\t * key：账号id  \n\t */\n\tpublic static final String LOGIN_ID = SaJwtTemplate.LOGIN_ID; \n\t\n\t/**\n\t * key：登录设备类型\n\t */\n\tpublic static final String DEVICE_TYPE = SaJwtTemplate.DEVICE_TYPE;\n\t\n\t/**\n\t * key：有效截止期 (时间戳) \n\t */\n\tpublic static final String EFF = SaJwtTemplate.EFF; \n\n\t/**\n\t * key：乱数 （ 混入随机字符串，防止每次生成的 token 都是一样的 ）\n\t */\n\tpublic static final String RN_STR = SaJwtTemplate.RN_STR; \n\n\t/** \n\t * 当有效期被设为此值时，代表永不过期 \n\t */ \n\tpublic static final long NEVER_EXPIRE = SaJwtTemplate.NEVER_EXPIRE; \n\n\t/** \n\t * 表示一个值不存在 \n\t */ \n\tpublic static final long NOT_VALUE_EXPIRE = SaJwtTemplate.NOT_VALUE_EXPIRE; \n\t\n\t// ------ 创建\n\n\t/**\n\t * 创建 jwt （简单方式）\n     * @param loginType 登录类型 \n\t * @param loginId 账号id \n\t * @param extraData 扩展数据\n     * @param keyt 秘钥\n\t * @return jwt-token \n\t */\n    public static String createToken(String loginType, Object loginId, Map<String, Object> extraData, String keyt) {\n    \treturn saJwtTemplate.createToken(loginType, loginId, extraData, keyt);\n    }\n\n\t/**\n\t * 创建 jwt （全参数方式）\n\t * @param loginType 账号类型\n\t * @param loginId 账号id\n\t * @param deviceType 设备类型\n\t * @param timeout token有效期 (单位 秒)\n\t * @param extraData 扩展数据\n\t * @param keyt 秘钥\n\t * @return jwt-token\n\t */\n\tpublic static String createToken(String loginType, Object loginId, String deviceType,\n\t\t\t\t\t\t\t\t\t long timeout, Map<String, Object> extraData, String keyt) {\n\t\treturn saJwtTemplate.createToken(loginType, loginId, deviceType, timeout, extraData, keyt);\n\t}\n\n\t/**\n\t * 为 JWT 对象和 keyt 秘钥，生成 token 字符串 \n\t * @param jwt JWT构建对象\n\t * @param keyt 秘钥 \n\t * @return 根据 JWT 对象和 keyt 秘钥，生成的 token 字符串\n\t */\n\tpublic static String generateToken (JWT jwt, String keyt) {\n\t\treturn saJwtTemplate.generateToken(jwt, keyt);\n\t}\n\t\n\t// ------ 解析 \n\n    /**\n     * jwt 解析 \n     * @param token Jwt-Token值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @param isCheckTimeout 是否校验 timeout 字段\n     * @return 解析后的jwt 对象 \n     */\n    public static JWT parseToken(String token, String loginType, String keyt, boolean isCheckTimeout) {\n\t\treturn saJwtTemplate.parseToken(token, loginType, keyt, isCheckTimeout);\n    }\n\n    /**\n     * 获取 jwt 数据载荷 （校验 sign、loginType、timeout） \n     * @param token token值\n     * @param loginType 登录类型 \n     * @param keyt 秘钥 \n     * @return 载荷 \n     */\n    public static JSONObject getPayloads(String token, String loginType, String keyt) {\n    \treturn saJwtTemplate.getPayloads(token, loginType, keyt);\n    }\n\n    /**\n     * 获取 jwt 数据载荷 （校验 sign、loginType，不校验 timeout） \n     * @param token token值\n     * @param loginType 登录类型 \n     * @param keyt 秘钥 \n     * @return 载荷 \n     */\n    public static JSONObject getPayloadsNotCheck(String token, String loginType, String keyt) {\n    \treturn saJwtTemplate.getPayloadsNotCheck(token, loginType, keyt);\n    }\n    \n    /**\n     * 获取 jwt 代表的账号id \n     * @param token Token值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public static Object getLoginId(String token, String loginType, String keyt) {\n    \treturn saJwtTemplate.getLoginId(token, loginType, keyt);\n    }\n\n    /**\n     * 获取 jwt 代表的账号id (未登录时返回null)\n     * @param token Token值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public static Object getLoginIdOrNull(String token, String loginType, String keyt) {\n    \treturn saJwtTemplate.getLoginIdOrNull(token, loginType, keyt);\n    }\n\n    /**\n     * 获取 jwt 剩余有效期 \n     * @param token JwtToken值 \n     * @param loginType 登录类型 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public static long getTimeout(String token, String loginType, String keyt) {\n    \treturn saJwtTemplate.getTimeout(token, loginType, keyt);\n    }\n\n\n\t// -------------- 其它方法\n\n\t/**\n\t * 创建 jwt （Map 参数方式）\n\t *\n\t * @param map 扩展数据\n\t * @param keyt 秘钥\n\t * @return jwt-token\n\t */\n\tpublic static String createToken(Map<String, Object> map, String keyt) {\n\t\treturn saJwtTemplate.createToken(map, keyt);\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForMixin.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jwt;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.ApiDisabledException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.jwt.error.SaJwtErrorCode;\nimport cn.dev33.satoken.jwt.exception.SaJwtException;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Sa-Token 整合 jwt -- Mixin 混入模式\n *\n * @author click33\n * @since 1.30.0\n */\npublic class StpLogicJwtForMixin extends StpLogic {\n\n\t/**\n\t * Sa-Token 整合 jwt -- Mixin 混入  \n\t */\n\tpublic StpLogicJwtForMixin() {\n\t\tsuper(StpUtil.TYPE);\n\t}\n\n\t/**\n\t * Sa-Token 整合 jwt -- Mixin 混入 \n\t * @param loginType 账号体系标识 \n\t */\n\tpublic StpLogicJwtForMixin(String loginType) {\n\t\tsuper(loginType);\n\t}\n\n\t/**\n\t * 获取jwt秘钥 \n\t * @return / \n\t */\n\tpublic String jwtSecretKey() {\n\t\tString keyt = getConfigOrGlobal().getJwtSecretKey();\n\t\tSaJwtException.throwByNull(keyt, \"请配置jwt秘钥\", SaJwtErrorCode.CODE_30205);\n\t\treturn keyt;\n\t}\n\t\n\t// \n\t// ------ 重写方法 \n\t// \n\n\t// ------------------- 获取token 相关 -------------------  \n\t\n\t/**\n\t * 创建一个TokenValue \n\t */\n\t@Override\n\tpublic String createTokenValue(Object loginId, String deviceType, long timeout, Map<String, Object> extraData) {\n\t\treturn SaJwtUtil.createToken(loginType, loginId, deviceType, timeout, extraData, jwtSecretKey());\n\t}\n\n\t/**\n\t * 获取当前会话的Token信息 \n\t * @return token信息 \n\t */\n\t@Override\n\tpublic SaTokenInfo getTokenInfo() {\n\t\tSaTokenInfo info = new SaTokenInfo();\n\t\tinfo.tokenName = getTokenName();\n\t\tinfo.tokenValue = getTokenValue();\n\t\tinfo.isLogin = isLogin();\n\t\tinfo.loginId = getLoginIdDefaultNull();\n\t\tinfo.loginType = getLoginType();\n\t\tinfo.tokenTimeout = getTokenTimeout();\n\t\tinfo.sessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE;\n\t\tinfo.tokenSessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE;\n\t\tinfo.tokenActiveTimeout = SaTokenDao.NOT_VALUE_EXPIRE;\n\t\tinfo.loginDeviceType = getLoginDeviceType();\n\t\treturn info;\n\t}\n\t\n\t// ------------------- 登录相关操作 -------------------  \n\n\t/**\n\t * 获取指定Token对应的账号id (不做任何特殊处理) \n\t */\n\t@Override\n\tpublic String getLoginIdNotHandle(String tokenValue) {\n\t\ttry {\n\t\t\tObject loginId = SaJwtUtil.getLoginId(tokenValue, loginType, jwtSecretKey());\n\t\t\treturn String.valueOf(loginId);\n\t\t} catch (SaJwtException e) {\n\t\t\t// CODE == 30204 时，代表token已过期，此时返回-3，以便外层更精确的显示异常信息\n\t\t\tif(e.getCode() == SaJwtErrorCode.CODE_30204) {\n\t\t\t\treturn NotLoginException.TOKEN_TIMEOUT;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 会话注销 \n\t */\n\t@Override\n\tpublic void logout() {\n\t\t// ... \n\n \t\t// 从当前 [storage存储器] 里删除 \n \t\tSaHolder.getStorage().delete(splicingKeyJustCreatedSave());\n \t\t\n \t\t// 如果打开了Cookie模式，则把cookie清除掉 \n \t\tif(getConfigOrGlobal().getIsReadCookie()){\n \t\t\tSaHolder.getResponse().deleteCookie(getTokenName());\n\t\t}\n\t}\n\n\t/**\n\t * [work] 注销下线\n\t *\n\t * @param tokenValue 指定 token\n\t * @param logoutParameter 注销参数\n\t */\n\tpublic void _logoutByTokenValue(String tokenValue, SaLogoutParameter logoutParameter) {\n\t\tthrow new ApiDisabledException();\n\t}\n\n\t/**\n\t * [禁用] 会话注销\n\t */\n\t@Override\n\tpublic void _logout(Object loginId, SaLogoutParameter logoutParameter) {\n\t\tthrow new ApiDisabledException(); \n\t}\n\n\t/**\n\t * [禁用] 顶人下线，根据账号id 和 设备类型 \n\t */\n\t@Override\n\tpublic void replaced(Object loginId, String deviceType) {\n\t\tthrow new ApiDisabledException(); \n\t}\n\n\t/**\n\t * 获取当前 Token 的扩展信息 \n\t */\n\t@Override\n\tpublic Object getExtra(String key) {\n\t\treturn getExtra(getTokenValue(), key);\n\t}\n\n\t/**\n\t * 获取指定 Token 的扩展信息 \n\t */\n\t@Override\n\tpublic Object getExtra(String tokenValue, String key) {\n\t\treturn SaJwtUtil.getPayloads(tokenValue, loginType, jwtSecretKey()).get(key);\n\t}\n\n\t/**\n\t * 删除 Token-Id 映射 \n\t */\n\t@Override\n\tpublic void deleteTokenToIdMapping(String tokenValue) {\n\t\t// not action \n\t}\n\t/**\n\t * 更改 Token 指向的 账号Id 值 \n\t */\n\t@Override\n\tpublic void updateTokenToIdMapping(String tokenValue, Object loginId) {\n\t\t// not action \n\t}\n\t/**\n\t * 存储 Token-Id 映射 \n\t */\n\t@Override\n\tpublic void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) {\n\t\t// not action \n\t}\n \t\n \t// ------------------- 过期时间相关 -------------------  \n\n \t/**\n \t * 获取指定 token 剩余有效时间 (单位: 秒)\n \t */\n\t@Override\n \tpublic long getTokenTimeout(String tokenValue) {\n \t\treturn SaJwtUtil.getTimeout(tokenValue, loginType, jwtSecretKey());\n \t}\n\n\n\t// ------------------- Token-Session 相关 -------------------\n\n\t/**\n\t * 获取指定 token 的 Token-Session，如果该 SaSession 尚未创建，isCreate代表是否新建并返回\n\t *\n\t * @param tokenValue token值\n\t * @param isCreate 是否新建\n\t * @return session对象\n\t */\n\tpublic SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) {\n\t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n\t\t\tthrow new SaTokenException(\"Token-Session 获取失败：token 不能为空\");\n\t\t}\n\t\tlong timeout = getTokenTimeout(tokenValue);\n\t\treturn getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, timeout, session -> {\n\t\t\t// 这里是该 Token-Session 首次创建时才会被执行的方法：\n\t\t\t// \t\t设定这个 SaSession 的各种基础信息：类型、账号体系、Token 值\n\t\t\tsession.setType(SaTokenConsts.SESSION_TYPE__TOKEN);\n\t\t\tsession.setLoginType(getLoginType());\n\t\t\tsession.setToken(tokenValue);\n\t\t});\n\t}\n\n\n\t// ------------------- 会话管理 -------------------  \n\n\t/**\n\t * [禁用] 根据条件查询Token \n\t */\n\t@Override\n\tpublic List<String> searchTokenValue(String keyword, int start, int size, boolean sortType) {\n\t\tthrow new ApiDisabledException(); \n\t}\n\t\n\n\t// ------------------- Bean对象代理 -------------------  \n\t\n\t/**\n\t * 返回当前 StpLogic 是否支持 isShare \n\t * @return / \n\t */\n\t@Override\n\tpublic boolean isSupportShareToken() {\n\t\treturn false;\n\t}\n\n\t/**\n\t * 返回全局配置对象的 maxTryTimes 属性\n\t * @return /\n\t */\n\t@Override\n\tpublic int getConfigOfMaxTryTimes(SaLoginParameter loginParameter) {\n\t\treturn -1;\n\t}\n\n\t/**\n\t * 重写返回：支持 extra 扩展参数\n\t */\n\t@Override\n\tpublic boolean isSupportExtra() {\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForSimple.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jwt;\n\nimport java.util.Map;\n\nimport cn.dev33.satoken.jwt.error.SaJwtErrorCode;\nimport cn.dev33.satoken.jwt.exception.SaJwtException;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * Sa-Token 整合 jwt -- Simple 简单模式\n *\n * @author click33\n * @since 1.30.0\n */\npublic class StpLogicJwtForSimple extends StpLogic {\n\n\t/**\n\t * Sa-Token 整合 jwt -- Simple模式 \n\t */\n\tpublic StpLogicJwtForSimple() {\n\t\tsuper(StpUtil.TYPE);\n\t}\n\n\t/**\n\t * Sa-Token 整合 jwt -- Simple模式 \n\t * @param loginType 账号体系标识 \n\t */\n\tpublic StpLogicJwtForSimple(String loginType) {\n\t\tsuper(loginType);\n\t}\n\n\t/**\n\t * 获取jwt秘钥 \n\t * @return / \n\t */\n\tpublic String jwtSecretKey() {\n\t\tString keyt = getConfigOrGlobal().getJwtSecretKey();\n\t\tSaJwtException.throwByNull(keyt, \"请配置jwt秘钥\", SaJwtErrorCode.CODE_30205);\n\t\treturn keyt;\n\t}\n\t\n\t// ------ 重写方法 \n\t\n\t/**\n\t * 创建一个TokenValue\n\t */\n\t@Override\n \tpublic String createTokenValue(Object loginId, String deviceType, long timeout, Map<String, Object> extraData) {\n \t\treturn SaJwtUtil.createToken(loginType, loginId, extraData, jwtSecretKey());\n\t}\n\n\t/**\n\t * 获取当前 Token 的扩展信息 \n\t */\n\t@Override\n\tpublic Object getExtra(String key) {\n\t\treturn getExtra(getTokenValue(), key);\n\t}\n\n\t/**\n\t * 获取指定 Token 的扩展信息 \n\t */\n\t@Override\n\tpublic Object getExtra(String tokenValue, String key) {\n\t\treturn SaJwtUtil.getPayloadsNotCheck(tokenValue, loginType, jwtSecretKey()).get(key);\n\t}\n\n\n\t@Override\n\tpublic boolean isSupportShareToken() {\n\t\t// 为确保 jwt-simple 模式的 token Extra 数据生成不受旧token影响，这里必须让 is-share 恒为 false \n\t\t// 即：在使用 jwt-simple 模式后，即使配置了 is-share=true 也不能复用旧 Token，必须每次创建新 Token \n\t\treturn false;\n\t}\n\n\t/**\n\t * 重写返回：支持 extra 扩展参数\n\t */\n\t@Override\n\tpublic boolean isSupportExtra() {\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/StpLogicJwtForStateless.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jwt;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.ApiDisabledException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.jwt.error.SaJwtErrorCode;\nimport cn.dev33.satoken.jwt.exception.SaJwtException;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.Map;\n\n/**\n * Sa-Token 整合 jwt -- Stateless 无状态模式\n * \n * @author click33\n * @since 1.30.0\n */\npublic class StpLogicJwtForStateless extends StpLogic {\n\n\t/**\n\t * Sa-Token 整合 jwt -- Stateless 无状态 \n\t */\n\tpublic StpLogicJwtForStateless() {\n\t\tsuper(StpUtil.TYPE);\n\t}\n\n\t/**\n\t * Sa-Token 整合 jwt -- Stateless 无状态 \n\t * @param loginType 账号体系标识 \n\t */\n\tpublic StpLogicJwtForStateless(String loginType) {\n\t\tsuper(loginType);\n\t}\n\n\t/**\n\t * 获取jwt秘钥 \n\t * @return / \n\t */\n\tpublic String jwtSecretKey() {\n\t\tString keyt = getConfigOrGlobal().getJwtSecretKey();\n\t\tSaJwtException.throwByNull(keyt, \"请配置jwt秘钥\", SaJwtErrorCode.CODE_30205);\n\t\treturn keyt;\n\t}\n\t\n\t// \n\t// ------ 重写方法 \n\t// \n\n\t// ------------------- 获取token 相关 -------------------  \n\t\n\t/**\n\t * 创建一个TokenValue \n\t */\n\t@Override\n\tpublic String createTokenValue(Object loginId, String deviceType, long timeout, Map<String, Object> extraData) {\n\t\treturn SaJwtUtil.createToken(loginType, loginId, deviceType, timeout, extraData, jwtSecretKey());\n\t}\n\n\t/**\n\t * 获取当前会话的Token信息 \n\t * @return token信息 \n\t */\n\t@Override\n\tpublic SaTokenInfo getTokenInfo() {\n\t\tSaTokenInfo info = new SaTokenInfo();\n\t\tinfo.tokenName = getTokenName();\n\t\tinfo.tokenValue = getTokenValue();\n\t\tinfo.isLogin = isLogin();\n\t\tinfo.loginId = getLoginIdDefaultNull();\n\t\tinfo.loginType = getLoginType();\n\t\tinfo.tokenTimeout = getTokenTimeout();\n\t\tinfo.sessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE;\n\t\tinfo.tokenSessionTimeout = SaTokenDao.NOT_VALUE_EXPIRE;\n\t\tinfo.tokenActiveTimeout = SaTokenDao.NOT_VALUE_EXPIRE;\n\t\tinfo.loginDeviceType = getLoginDeviceType();\n\t\treturn info;\n\t}\n\t\n\t// ------------------- 登录相关操作 -------------------  \n\n\t/**\n\t * 创建指定账号id的登录会话\n\t * @param id 登录id，建议的类型：（long | int | String）\n\t * @param loginParameter 此次登录的参数Model \n\t * @return 返回会话令牌 \n\t */\n\t@Override\n\tpublic String createLoginSession(Object id, SaLoginParameter loginParameter) {\n\n\t\t// 1、先检查一下，传入的参数是否有效\n\t\tcheckLoginArgs(id, loginParameter);\n\n\t\t// 3、生成一个token\n\t\tString tokenValue = createTokenValue(id, loginParameter.getDeviceType(), loginParameter.getTimeout(), loginParameter.getExtraData());\n\t\t\n\t\t// 4、$$ 发布事件：账号xxx 登录成功\n\t\tSaTokenEventCenter.doLogin(loginType, id, tokenValue, loginParameter);\n\n\t\t// 5、返回\n\t\treturn tokenValue;\n\t}\n\n\t/**\n\t * 获取指定Token对应的账号id (不做任何特殊处理) \n\t */\n\t@Override\n\tpublic String getLoginIdNotHandle(String tokenValue) {\n\t\ttry {\n\t\t\tObject loginId = SaJwtUtil.getLoginId(tokenValue, loginType, jwtSecretKey());\n\t\t\treturn String.valueOf(loginId);\n\t\t} catch (SaJwtException e) {\n\t\t\t// CODE == 30204 时，代表token已过期，此时返回-3，以便外层更精确的显示异常信息\n\t\t\tif(e.getCode() == SaJwtErrorCode.CODE_30204) {\n\t\t\t\treturn NotLoginException.TOKEN_TIMEOUT;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 会话注销 \n\t */\n\t@Override\n\tpublic void logout() {\n\t\t// 如果连token都没有，那么无需执行任何操作 \n\t\tString tokenValue = getTokenValue();\n \t\tif(SaFoxUtil.isEmpty(tokenValue)) {\n \t\t\treturn;\n \t\t}\n\n \t\t// 从当前 [storage存储器] 里删除 \n \t\tSaHolder.getStorage().delete(splicingKeyJustCreatedSave());\n \t\t\n \t\t// 如果打开了Cookie模式，则把cookie清除掉 \n \t\tif(getConfigOrGlobal().getIsReadCookie()){\n \t\t\tSaHolder.getResponse().deleteCookie(getTokenName());\n\t\t}\n\t}\n\n\t/**\n\t * 获取当前 Token 的扩展信息 \n\t */\n\t@Override\n\tpublic Object getExtra(String key) {\n\t\treturn getExtra(getTokenValue(), key);\n\t}\n\n\t/**\n\t * 获取指定 Token 的扩展信息 \n\t */\n\t@Override\n\tpublic Object getExtra(String tokenValue, String key) {\n\t\treturn SaJwtUtil.getPayloads(tokenValue, loginType, jwtSecretKey()).get(key);\n\t}\n\n \t\n \t// ------------------- 过期时间相关 -------------------  \n\n \t/**\n \t * 获取指定 token 剩余有效时间 (单位: 秒)\n \t */\n\t@Override\n \tpublic long getTokenTimeout(String tokenValue) {\n \t\treturn SaJwtUtil.getTimeout(getTokenValue(), loginType, jwtSecretKey());\n \t}\n \t\n \t\n \t// ------------------- id 反查 token 相关操作 -------------------  \n\n\t/**\n\t * 返回当前会话的登录设备类型 \n\t * @return 当前令牌的登录设备类型\n\t */\n\t@Override\n\tpublic String getLoginDeviceType() {\n\t\t// 如果没有token，直接返回 null \n\t\tString tokenValue = getTokenValue();\n\t\tif(tokenValue == null) {\n\t\t\treturn null;\n\t\t}\n\t\t// 如果还未登录，直接返回 null \n\t\tif(!isLogin()) {\n\t\t\treturn null;\n\t\t}\n\t\t// 获取\n\t\treturn SaJwtUtil.getPayloadsNotCheck(tokenValue, loginType, jwtSecretKey()).getStr(SaJwtUtil.DEVICE_TYPE);\n\t}\n\n\t\n\t// ------------------- Bean对象代理 -------------------  \n\t\n\t/**\n\t * [禁用] 返回持久化对象 \n\t */\n\t@Override\n\tpublic SaTokenDao getSaTokenDao() {\n\t\tthrow new ApiDisabledException();\n\t}\n\n\t/**\n\t * 重写返回：支持 extra 扩展参数\n\t */\n\t@Override\n\tpublic boolean isSupportExtra() {\n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/error/SaJwtErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jwt.error;\n\n/**\n * 定义 sa-token-jwt 所有异常细分状态码 \n * \n * @author click33\n * @since 1.33.0\n */\npublic interface SaJwtErrorCode {\n\n\t/** 对 jwt 字符串解析失败 */\n\tint CODE_30201 = 30201;\n\n\t/** 此 jwt 的签名无效 */\n\tint CODE_30202 = 30202;\n\n\t/** 此 jwt 的 loginType 字段不符合预期 */\n\tint CODE_30203 = 30203;\n\n\t/** 此 jwt 已超时 */\n\tint CODE_30204 = 30204;\n\n\t/** 没有配置jwt秘钥 */\n\tint CODE_30205 = 30205;\n\n\t/** 登录时提供的账号id为空 */\n\tint CODE_30206 = 30206;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/exception/SaJwtException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jwt.exception;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n\n/**\n * 一个异常：代表 jwt 模块相关错误\n * \n * @author click33\n * @since 1.33.0\n */\npublic class SaJwtException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129555290130114L;\n\t\n\t/**\n\t * jwt 解析错误 \n\t * @param message 异常描述 \n\t */\n\tpublic SaJwtException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * jwt 解析错误\n\t * @param message 异常描述 \n\t * @param cause 异常对象 \n\t */\n\tpublic SaJwtException(String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t}\n\n\t/**\n\t * 写入异常细分状态码 \n\t * @param code 异常细分状态码\n\t * @return 对象自身 \n\t */\n\tpublic SaJwtException setCode(int code) {\n\t\tsuper.setCode(code);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 如果flag==true，则抛出message异常 \n\t * @param flag 标记\n\t * @param message 异常信息 \n\t */\n\tpublic static void throwBy(boolean flag, String message) {\n\t\tif(flag) {\n\t\t\tthrow new SaJwtException(message);\n\t\t}\n\t}\n\n\t/**\n\t * 如果value==null或者isEmpty，则抛出message异常 \n\t * @param value 值 \n\t * @param message 异常信息 \n\t * @param code 异常细分状态码 \n\t */\n\tpublic static void throwByNull(Object value, String message, int code) {\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\tthrow new SaJwtException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-oauth2</name>\n    <artifactId>sa-token-oauth2</artifactId>\n\t<description>sa-token realization oauth2.0</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <!-- sa-token-jwt 签发 OIDC id_token 令牌 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jwt</artifactId>\n            <optional>true</optional>\n        </dependency>\n        <!-- sa-token-sign 校验 nonce -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sign</artifactId>\n        </dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2;\n\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.dao.SaOAuth2Dao;\nimport cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter;\nimport cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverterDefaultImpl;\nimport cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;\nimport cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerateDefaultImpl;\nimport cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;\nimport cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoaderDefaultImpl;\nimport cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver;\nimport cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolverDefaultImpl;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Template;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * Sa-Token-OAuth2 模块 总控类\n * \n * @author click33\n * @since 1.19.0\n */\npublic class SaOAuth2Manager {\n\n\t/**\n\t * OAuth2 配置 Bean \n\t */\n\tprivate static volatile SaOAuth2ServerConfig serverConfig;\n\tpublic static SaOAuth2ServerConfig getServerConfig() {\n\t\tif (serverConfig == null) {\n\t\t\t// 初始化默认值\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (serverConfig == null) {\n\t\t\t\t\tsetServerConfig(new SaOAuth2ServerConfig());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn serverConfig;\n\t}\n\tpublic static void setServerConfig(SaOAuth2ServerConfig serverConfig) {\n\t\tSaOAuth2Manager.serverConfig = serverConfig;\n\t}\n\n\t/**\n\t * OAuth2 数据加载器 Bean\n\t */\n\tprivate static volatile SaOAuth2DataLoader dataLoader;\n\tpublic static SaOAuth2DataLoader getDataLoader() {\n\t\tif (dataLoader == null) {\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (dataLoader == null) {\n\t\t\t\t\tsetDataLoader(new SaOAuth2DataLoaderDefaultImpl());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn dataLoader;\n\t}\n\tpublic static void setDataLoader(SaOAuth2DataLoader dataLoader) {\n\t\tSaOAuth2Manager.dataLoader = dataLoader;\n\t}\n\n\t/**\n\t * OAuth2 数据解析器 Bean\n\t */\n\tprivate static volatile SaOAuth2DataResolver dataResolver;\n\tpublic static SaOAuth2DataResolver getDataResolver() {\n\t\tif (dataResolver == null) {\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (dataResolver == null) {\n\t\t\t\t\tsetDataResolver(new SaOAuth2DataResolverDefaultImpl());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn dataResolver;\n\t}\n\tpublic static void setDataResolver(SaOAuth2DataResolver dataResolver) {\n\t\tSaOAuth2Manager.dataResolver = dataResolver;\n\t}\n\n\t/**\n\t * OAuth2 数据格式转换器 Bean\n\t */\n\tprivate static volatile SaOAuth2DataConverter dataConverter;\n\tpublic static SaOAuth2DataConverter getDataConverter() {\n\t\tif (dataConverter == null) {\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (dataConverter == null) {\n\t\t\t\t\tsetDataConverter(new SaOAuth2DataConverterDefaultImpl());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn dataConverter;\n\t}\n\tpublic static void setDataConverter(SaOAuth2DataConverter dataConverter) {\n\t\tSaOAuth2Manager.dataConverter = dataConverter;\n\t}\n\n\t/**\n\t * OAuth2 数据构建器 Bean\n\t */\n\tprivate static volatile SaOAuth2DataGenerate dataGenerate;\n\tpublic static SaOAuth2DataGenerate getDataGenerate() {\n\t\tif (dataGenerate == null) {\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (dataGenerate == null) {\n\t\t\t\t\tsetDataGenerate(new SaOAuth2DataGenerateDefaultImpl());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn dataGenerate;\n\t}\n\tpublic static void setDataGenerate(SaOAuth2DataGenerate dataGenerate) {\n\t\tSaOAuth2Manager.dataGenerate = dataGenerate;\n\t}\n\n\t/**\n\t * OAuth2 数据持久 Bean\n\t */\n\tprivate static volatile SaOAuth2Dao dao;\n\tpublic static SaOAuth2Dao getDao() {\n\t\tif (dao == null) {\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (dao == null) {\n\t\t\t\t\tsetDao(new SaOAuth2Dao());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn dao;\n\t}\n\tpublic static void setDao(SaOAuth2Dao dao) {\n\t\tSaOAuth2Manager.dao = dao;\n\t}\n\n\t/**\n\t * OAuth2 模板方法 Bean\n\t */\n\tprivate static volatile SaOAuth2Template template;\n\tpublic static SaOAuth2Template getTemplate() {\n\t\tif (template == null) {\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (template == null) {\n\t\t\t\t\tsetTemplate(new SaOAuth2Template());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn template;\n\t}\n\tpublic static void setTemplate(SaOAuth2Template template) {\n\t\tSaOAuth2Manager.template = template;\n\t}\n\n\t/**\n\t * OAuth2 StpLogic\n\t */\n\tprivate static volatile StpLogic stpLogic;\n\tpublic static StpLogic getStpLogic() {\n\t\tif (stpLogic == null) {\n\t\t\tsynchronized (SaOAuth2Manager.class) {\n\t\t\t\tif (stpLogic == null) {\n\t\t\t\t\tsetStpLogic(StpUtil.stpLogic);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn stpLogic;\n\t}\n\tpublic static void setStpLogic(StpLogic stpLogic) {\n\t\tSaOAuth2Manager.stpLogic = stpLogic;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckAccessToken.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Access-Token 校验：指定请求中必须包含有效的 access_token ，并且包含指定的 scope\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.39.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface SaCheckAccessToken {\n\n\t/**\n\t * 需要校验的 scope [ 数组 ]\n\t *\n\t * @return /\n\t */\n\tString [] scope() default {};\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientIdSecret.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * ClientSecret 校验：指定请求中必须包含有效的 client_id 和 client_secret 信息\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.39.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface SaCheckClientIdSecret {\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/SaCheckClientToken.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Client-Token 校验：指定请求中必须包含有效的 client_token ，并且包含指定的 scope\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.39.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface SaCheckClientToken {\n\n\t/**\n\t * 需要校验的 scope [ 数组 ]\n\t *\n\t * @return /\n\t */\n\tString [] scope() default {};\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckAccessTokenHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.annotation.SaCheckAccessToken;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckAccessToken 的处理器\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaCheckAccessTokenHandler implements SaAnnotationHandlerInterface<SaCheckAccessToken> {\n\n    @Override\n    public Class<SaCheckAccessToken> getHandlerAnnotationClass() {\n        return SaCheckAccessToken.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckAccessToken at, AnnotatedElement element) {\n        _checkMethod(at.scope());\n    }\n\n    public static void _checkMethod(String[] scope) {\n        String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());\n        SaOAuth2Manager.getTemplate().checkAccessTokenScope(accessToken, scope);\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientIdSecretHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.oauth2.annotation.SaCheckClientIdSecret;\nimport cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckClientSecret 的处理器\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaCheckClientIdSecretHandler implements SaAnnotationHandlerInterface<SaCheckClientIdSecret> {\n\n    @Override\n    public Class<SaCheckClientIdSecret> getHandlerAnnotationClass() {\n        return SaCheckClientIdSecret.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckClientIdSecret at, AnnotatedElement element) {\n        _checkMethod();\n    }\n\n    public static void _checkMethod() {\n        SaOAuth2ServerProcessor.instance.checkCurrClientSecret();\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/annotation/handler/SaCheckClientTokenHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.annotation.handler;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.annotation.SaCheckClientToken;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckAccessToken 的处理器\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaCheckClientTokenHandler implements SaAnnotationHandlerInterface<SaCheckClientToken> {\n\n    @Override\n    public Class<SaCheckClientToken> getHandlerAnnotationClass() {\n        return SaCheckClientToken.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckClientToken at, AnnotatedElement element) {\n        _checkMethod(at.scope());\n    }\n\n    public static void _checkMethod(String[] scope) {\n        String clientToken = SaOAuth2Manager.getDataResolver().readClientToken(SaHolder.getRequest());\n        SaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scope);\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.config;\n\nimport java.io.Serializable;\n\n/**\n * Sa-Token OAuth2 Server 端 Oidc 配置类 Model\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2OidcConfig implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/** iss 值，如不配置则自动计算 */\n\tpublic String iss;\n\n\t/** idToken 有效期（单位秒） 默认十分钟 */\n\tpublic long idTokenTimeout = 60 * 10;\n\n\n\t/**\n\t * 获取 iss 值，如不配置则自动计算\n\t *\n\t * @return /\n\t */\n\tpublic String getIss() {\n\t\treturn this.iss;\n\t}\n\n\t/**\n\t * 设置 iss 值，如不配置则自动计算\n\t *\n\t * @param iss /\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2OidcConfig setIss(String iss) {\n\t\tthis.iss = iss;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 idToken 有效期（单位秒） 默认十分钟\n\t *\n\t * @return /\n\t */\n\tpublic long getIdTokenTimeout() {\n\t\treturn this.idTokenTimeout;\n\t}\n\n\t/**\n\t * 设置 idToken 有效期（单位秒） 默认十分钟\n\t *\n\t * @param idTokenTimeout /\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2OidcConfig setIdTokenTimeout(long idTokenTimeout) {\n\t\tthis.idTokenTimeout = idTokenTimeout;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaOAuth2OidcConfig{\" +\n\t\t\t\t\"iss='\" + iss + '\\'' +\n\t\t\t\t\", idTokenTimeout=\" + idTokenTimeout +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.config;\n\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token OAuth2 Server 端 配置类 Model\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaOAuth2ServerConfig implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/** 是否打开模式：授权码（Authorization Code） */\n\tpublic Boolean enableAuthorizationCode = true;\n\n\t/** 是否打开模式：隐藏式（Implicit） */\n\tpublic Boolean enableImplicit = true;\n\n\t/** 是否打开模式：密码式（Password） */\n\tpublic Boolean enablePassword = true;\n\n\t/** 是否打开模式：凭证式（Client Credentials） */\n\tpublic Boolean enableClientCredentials = true;\n\n\t/** Code授权码 保存的时间(单位：秒) 默认五分钟 */\n\tpublic long codeTimeout = 60 * 5;\n\n\t/** 全局默认配置所有应用：Access-Token 保存的时间(单位：秒) 默认两个小时 */\n\tpublic long accessTokenTimeout = 60 * 60 * 2;\n\n\t/** 全局默认配置所有应用：Refresh-Token 保存的时间(单位：秒) 默认30 天 */\n\tpublic long refreshTokenTimeout = 60 * 60 * 24 * 30;\n\n\t/** 全局默认配置所有应用：Client-Token 保存的时间(单位：秒) 默认两个小时 */\n\tpublic long clientTokenTimeout = 60 * 60 * 2;\n\n\t/** 全局默认配置所有应用：单个应用单个用户最多同时存在的 Access-Token 数量 */\n\tpublic int maxAccessTokenCount = 12;\n\n\t/** 全局默认配置所有应用：单个应用单个用户最多同时存在的 Refresh-Token 数量 */\n\tpublic int maxRefreshTokenCount = 12;\n\n\t/** 全局默认配置所有应用：单个应用最多同时存在的 Client-Token 数量 */\n\tpublic int maxClientTokenCount = 12;\n\n\t/** 全局默认配置所有应用：是否在每次 Refresh-Token 刷新 Access-Token 时，产生一个新的 Refresh-Token */\n\tpublic Boolean isNewRefresh = false;\n\n\t/** 默认 openid 生成算法中使用的摘要前缀 */\n\tpublic String openidDigestPrefix = SaOAuth2Consts.OPENID_DEFAULT_DIGEST_PREFIX;\n\n\t/** 默认 unionid 生成算法中使用的摘要前缀 */\n\tpublic String unionidDigestPrefix = SaOAuth2Consts.UNIONID_DEFAULT_DIGEST_PREFIX;\n\n\t/** 指定高级权限，多个用逗号隔开 */\n\tpublic String higherScope;\n\n\t/** 指定低级权限，多个用逗号隔开 */\n\tpublic String lowerScope;\n\n\t/** 模式4是否返回 AccessToken 字段，以使其更符合 OAuth2 RFC 规范 */\n\tpublic Boolean mode4ReturnAccessToken = false;\n\n\t/** 是否在返回值中隐藏默认的状态字段 (code、msg、data) */\n\tpublic Boolean hideStatusField = false;\n\n\t/**\n\t * oidc 相关配置\n\t */\n\tSaOAuth2OidcConfig oidc = new SaOAuth2OidcConfig();\n\n\t/** client 列表 */\n\tpublic Map<String, SaClientModel> clients = new LinkedHashMap<>();\n\n\t// 额外方法\n\n\t/**\n\t * 注册 client\n\t * @return /\n\t */\n\tpublic SaOAuth2ServerConfig addClient(SaClientModel client) {\n\t\tif(this.clients == null) {\n\t\t\tthis.clients = new LinkedHashMap<>();\n\t\t}\n\t\tthis.clients.put(client.getClientId(), client);\n\t\treturn this;\n\t}\n\n\n\t// get set\n\n\t/**\n\t * 是否打开模式：授权码（Authorization Code）\n\t * @return enableAuthorizationCode\n\t */\n\tpublic Boolean getEnableAuthorizationCode() {\n\t\treturn enableAuthorizationCode;\n\t}\n\n\t/**\n\t * 设置是否打开模式：授权码（Authorization Code）\n\t * @param enableAuthorizationCode 是否开启\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setEnableAuthorizationCode(Boolean enableAuthorizationCode) {\n\t\tthis.enableAuthorizationCode = enableAuthorizationCode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否打开模式：隐藏式（Implicit）\n\t * @return enableImplicit\n\t */\n\tpublic Boolean getEnableImplicit() {\n\t\treturn enableImplicit;\n\t}\n\n\t/**\n\t * 设置是否打开模式：隐藏式（Implicit）\n\t * @param enableImplicit 是否开启\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setEnableImplicit(Boolean enableImplicit) {\n\t\tthis.enableImplicit = enableImplicit;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否打开模式：密码式（Password）\n\t * @return enablePassword\n\t */\n\tpublic Boolean getEnablePassword() {\n\t\treturn enablePassword;\n\t}\n\n\t/**\n\t * 设置是否打开模式：密码式（Password）\n\t * @param enablePassword 是否开启\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setEnablePassword(Boolean enablePassword) {\n\t\tthis.enablePassword = enablePassword;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否打开模式：凭证式（Client Credentials）\n\t * @return enableClientCredentials\n\t */\n\tpublic Boolean getEnableClientCredentials() {\n\t\treturn enableClientCredentials;\n\t}\n\n\t/**\n\t * 设置是否打开模式：凭证式（Client Credentials）\n\t * @param enableClientCredentials 是否开启\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setEnableClientCredentials(Boolean enableClientCredentials) {\n\t\tthis.enableClientCredentials = enableClientCredentials;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：是否在每次 Refresh-Token 刷新 Access-Token 时，产生一个新的 Refresh-Token\n\t * @return isNewRefresh\n\t */\n\tpublic Boolean getIsNewRefresh() {\n\t\treturn isNewRefresh;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：设置是否在每次 Refresh-Token 刷新 Access-Token 时，产生一个新的 Refresh-Token\n\t * @param isNewRefresh 是否开启\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setIsNewRefresh(Boolean isNewRefresh) {\n\t\tthis.isNewRefresh = isNewRefresh;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Code授权码 保存的时间(单位：秒) 默认五分钟\n\t * @return codeTimeout\n\t */\n\tpublic long getCodeTimeout() {\n\t\treturn codeTimeout;\n\t}\n\n\t/**\n\t * 设置Code授权码保存的时间(单位：秒)\n\t * @param codeTimeout 保存时间(秒)\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setCodeTimeout(long codeTimeout) {\n\t\tthis.codeTimeout = codeTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：Access-Token 保存的时间(单位：秒) 默认两个小时\n\t * @return accessTokenTimeout\n\t */\n\tpublic long getAccessTokenTimeout() {\n\t\treturn accessTokenTimeout;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：设置Access-Token保存的时间(单位：秒)\n\t * @param accessTokenTimeout 保存时间(秒)\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setAccessTokenTimeout(long accessTokenTimeout) {\n\t\tthis.accessTokenTimeout = accessTokenTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：Refresh-Token 保存的时间(单位：秒) 默认30天\n\t * @return refreshTokenTimeout\n\t */\n\tpublic long getRefreshTokenTimeout() {\n\t\treturn refreshTokenTimeout;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：设置Refresh-Token保存的时间(单位：秒)\n\t * @param refreshTokenTimeout 保存时间(秒)\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setRefreshTokenTimeout(long refreshTokenTimeout) {\n\t\tthis.refreshTokenTimeout = refreshTokenTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：Client-Token 保存的时间(单位：秒) 默认两个小时\n\t * @return clientTokenTimeout\n\t */\n\tpublic long getClientTokenTimeout() {\n\t\treturn clientTokenTimeout;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：设置Client-Token保存的时间(单位：秒)\n\t * @param clientTokenTimeout 保存时间(秒)\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setClientTokenTimeout(long clientTokenTimeout) {\n\t\tthis.clientTokenTimeout = clientTokenTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：单个应用单个用户最多同时存在的 Access-Token 数量\n\t * @return maxAccessTokenCount\n\t */\n\tpublic int getMaxAccessTokenCount() {\n\t\treturn maxAccessTokenCount;\n\t}\n\n\t/**\n\t * 设置单个应用单个用户最多同时存在的 Access-Token 数量\n\t * @param maxAccessTokenCount 最大数量\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setMaxAccessTokenCount(int maxAccessTokenCount) {\n\t\tthis.maxAccessTokenCount = maxAccessTokenCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：单个应用单个用户最多同时存在的 Refresh-Token 数量\n\t * @return maxRefreshTokenCount\n\t */\n\tpublic int getMaxRefreshTokenCount() {\n\t\treturn maxRefreshTokenCount;\n\t}\n\n\t/**\n\t * 设置单个应用单个用户最多同时存在的 Refresh-Token 数量\n\t * @param maxRefreshTokenCount 最大数量\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setMaxRefreshTokenCount(int maxRefreshTokenCount) {\n\t\tthis.maxRefreshTokenCount = maxRefreshTokenCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 全局默认配置所有应用：单个应用最多同时存在的 Client-Token 数量\n\t * @return maxClientTokenCount\n\t */\n\tpublic int getMaxClientTokenCount() {\n\t\treturn maxClientTokenCount;\n\t}\n\n\t/**\n\t * 设置单个应用最多同时存在的 Client-Token 数量\n\t * @param maxClientTokenCount 最大数量\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setMaxClientTokenCount(int maxClientTokenCount) {\n\t\tthis.maxClientTokenCount = maxClientTokenCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 默认 openid 生成算法中使用的摘要前缀\n\t * @return openidDigestPrefix\n\t */\n\tpublic String getOpenidDigestPrefix() {\n\t\treturn openidDigestPrefix;\n\t}\n\n\t/**\n\t * 设置默认 openid 生成算法中使用的摘要前缀\n\t * @param openidDigestPrefix 摘要前缀\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setOpenidDigestPrefix(String openidDigestPrefix) {\n\t\tthis.openidDigestPrefix = openidDigestPrefix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 默认 unionid 生成算法中使用的摘要前缀\n\t * @return unionidDigestPrefix\n\t */\n\tpublic String getUnionidDigestPrefix() {\n\t\treturn unionidDigestPrefix;\n\t}\n\n\t/**\n\t * 设置默认 unionid 生成算法中使用的摘要前缀\n\t * @param unionidDigestPrefix 摘要前缀\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setUnionidDigestPrefix(String unionidDigestPrefix) {\n\t\tthis.unionidDigestPrefix = unionidDigestPrefix;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 指定高级权限，多个用逗号隔开\n\t * @return higherScope\n\t */\n\tpublic String getHigherScope() {\n\t\treturn higherScope;\n\t}\n\n\t/**\n\t * 设置高级权限，多个用逗号隔开\n\t * @param higherScope 权限字符串\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setHigherScope(String higherScope) {\n\t\tthis.higherScope = higherScope;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 指定低级权限，多个用逗号隔开\n\t * @return lowerScope\n\t */\n\tpublic String getLowerScope() {\n\t\treturn lowerScope;\n\t}\n\n\t/**\n\t * 设置低级权限，多个用逗号隔开\n\t * @param lowerScope 权限字符串\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setLowerScope(String lowerScope) {\n\t\tthis.lowerScope = lowerScope;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 模式4是否返回 AccessToken 字段，以使其更符合 OAuth2 RFC 规范\n\t * @return mode4ReturnAccessToken\n\t */\n\tpublic Boolean getMode4ReturnAccessToken() {\n\t\treturn mode4ReturnAccessToken;\n\t}\n\n\t/**\n\t * 设置模式4是否返回 AccessToken 字段，以使其更符合 OAuth2 RFC 规范\n\t * @param mode4ReturnAccessToken 是否返回\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setMode4ReturnAccessToken(Boolean mode4ReturnAccessToken) {\n\t\tthis.mode4ReturnAccessToken = mode4ReturnAccessToken;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 是否在返回值中隐藏默认的状态字段 (code、msg、data)\n\t * @return hideStatusField\n\t */\n\tpublic Boolean getHideStatusField() {\n\t\treturn hideStatusField;\n\t}\n\n\t/**\n\t * 设置是否在返回值中隐藏默认的状态字段 (code、msg、data)\n\t * @param hideStatusField 是否隐藏\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setHideStatusField(Boolean hideStatusField) {\n\t\tthis.hideStatusField = hideStatusField;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取oidc相关配置\n\t * @return oidc配置对象\n\t */\n\tpublic SaOAuth2OidcConfig getOidc() {\n\t\treturn oidc;\n\t}\n\n\t/**\n\t * 设置oidc相关配置\n\t * @param oidc oidc配置对象\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setOidc(SaOAuth2OidcConfig oidc) {\n\t\tthis.oidc = oidc;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取client列表\n\t * @return client列表\n\t */\n\tpublic Map<String, SaClientModel> getClients() {\n\t\treturn clients;\n\t}\n\n\t/**\n\t * 设置client列表\n\t * @param clients client列表\n\t * @return 对象自身\n\t */\n\tpublic SaOAuth2ServerConfig setClients(Map<String, SaClientModel> clients) {\n\t\tthis.clients = clients;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaOAuth2ServerConfig {\" +\n\t\t\t\t\"enableAuthorizationCode=\" + enableAuthorizationCode +\n\t\t\t\t\", enableImplicit=\" + enableImplicit +\n\t\t\t\t\", enablePassword=\" + enablePassword +\n\t\t\t\t\", enableClientCredentials=\" + enableClientCredentials +\n\t\t\t\t\", isNewRefresh=\" + isNewRefresh +\n\t\t\t\t\", codeTimeout=\" + codeTimeout +\n\t\t\t\t\", accessTokenTimeout=\" + accessTokenTimeout +\n\t\t\t\t\", refreshTokenTimeout=\" + refreshTokenTimeout +\n\t\t\t\t\", clientTokenTimeout=\" + clientTokenTimeout +\n\t\t\t\t\", maxAccessTokenCount=\" + maxAccessTokenCount +\n\t\t\t\t\", maxRefreshTokenCount=\" + maxRefreshTokenCount +\n\t\t\t\t\", maxClientTokenCount=\" + maxClientTokenCount +\n\t\t\t\t\", openidDigestPrefix=\" + openidDigestPrefix +\n\t\t\t\t\", unionidDigestPrefix=\" + unionidDigestPrefix +\n\t\t\t\t\", higherScope=\" + higherScope +\n\t\t\t\t\", lowerScope=\" + lowerScope +\n\t\t\t\t\", mode4ReturnAccessToken=\" + mode4ReturnAccessToken +\n\t\t\t\t\", hideStatusField=\" + hideStatusField +\n\t\t\t\t\", oidc=\" + oidc +\n\t\t\t\t\", clients=\" + clients +\n\t\t\t\t'}';\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/GrantType.java",
    "content": "package cn.dev33.satoken.oauth2.consts;\n\n/**\n * 所有授权类型\n */\npublic final class GrantType {\n    public static String authorization_code = \"authorization_code\";\n    public static String refresh_token = \"refresh_token\";\n    public static String password = \"password\";\n    public static String client_credentials = \"client_credentials\";\n    public static String implicit = \"implicit\";\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.consts;\n\n/**\n * Sa-Token OAuth2 所有常量\n *\n * @author click33\n * @since 1.23.0\n */\npublic class SaOAuth2Consts {\n\n\t/**\n\t * 所有API接口 \n\t * @author click33 \n\t */\n\tpublic static final class Api {\n\t\tpublic static String authorize = \"/oauth2/authorize\";\n\t\tpublic static String token = \"/oauth2/token\";\n\t\tpublic static String refresh = \"/oauth2/refresh\";\n\t\tpublic static String revoke = \"/oauth2/revoke\";\n\t\tpublic static String client_token = \"/oauth2/client_token\";\n\t\tpublic static String doLogin = \"/oauth2/doLogin\";\n\t\tpublic static String doConfirm = \"/oauth2/doConfirm\";\n\t}\n\t\n\t/**\n\t * 所有参数名称 \n\t * @author click33 \n\t */\n\tpublic static final class Param {\n\t\tpublic static String response_type = \"response_type\";\n\t\tpublic static String client_id = \"client_id\";\n\t\tpublic static String client_secret = \"client_secret\";\n\t\tpublic static String redirect_uri = \"redirect_uri\";\n\t\tpublic static String scope = \"scope\";\n\t\tpublic static String state = \"state\";\n\t\tpublic static String code = \"code\";\n\t\tpublic static String token = \"token\";\n\t\tpublic static String access_token = \"access_token\";\n\t\tpublic static String refresh_token = \"refresh_token\";\n\t\tpublic static String client_token = \"client_token\";\n\t\tpublic static String grant_type = \"grant_type\";\n\t\tpublic static String username = \"username\";\n\t\tpublic static String password = \"password\";\n\t\tpublic static String name = \"name\";\n\t\tpublic static String pwd = \"pwd\";\n\t\tpublic static String build_redirect_uri = \"build_redirect_uri\";\n\t\tpublic static String Authorization = \"Authorization\";\n\t\tpublic static String nonce = \"nonce\";\n\t}\n\n\t/**\n\t * 所有返回类型 \n\t */\n\tpublic static final class ResponseType {\n\t\tpublic static String code = \"code\";\n\t\tpublic static String token = \"token\";\n\t}\n\n\t/**\n\t * 所有 token 类型\n\t */\n\tpublic static final class TokenType {\n\t\t// 全小写\n\t\tpublic static String basic = \"basic\";\n\t\tpublic static String digest = \"digest\";\n\t\tpublic static String bearer = \"bearer\";\n\n\t\t// 首字母大写\n\t\tpublic static String Basic = \"Basic\";\n\t\tpublic static String Digest = \"Digest\";\n\t\tpublic static String Bearer = \"Bearer\";\n\t}\n\n\t/**\n\t * 扩展字段\n\t */\n\tpublic static final class ExtraField {\n\t\tpublic static String unionid = \"unionid\";\n\t\tpublic static String openid = \"openid\";\n\t\tpublic static String userid = \"userid\";\n\t\tpublic static String id_token = \"id_token\";\n\t}\n\n\n\t/** 默认 openid 生成算法中使用的前缀 */\n\tpublic static final String OPENID_DEFAULT_DIGEST_PREFIX = \"openid_default_digest_prefix\";\n\n\t/** 默认 unionid 生成算法中使用的前缀 */\n\tpublic static final String UNIONID_DEFAULT_DIGEST_PREFIX = \"unionid_default_digest_prefix\";\n\n\t/** 表示OK的返回结果 */\n\tpublic static final String OK = \"ok\";\n\n\t/** 表示请求没有得到任何有效处理 {msg: \"not handle\"} */\n\tpublic static final String NOT_HANDLE = \"{\\\"msg\\\": \\\"not handle\\\"}\";\n\n\t/**\n\t * 最终权限处理器标识符：在所有权限处理器执行之后，执行此 scope 标识符代表的权限处理器\n\t */\n\tpublic static final String _FINALLY_WORK_SCOPE = \"_FINALLY_WORK_SCOPE\";\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.dao;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.RefreshTokenModel;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.raw.SaRawSessionDelegator;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaTtlMethods;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\nimport static cn.dev33.satoken.oauth2.template.SaOAuth2Util.checkClientModel;\n\n/**\n * Sa-Token OAuth2 数据持久层 (在 SaTokenDao 之上再封装一层，方便 OAuth2 模块整体的数据读写操作)\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2Dao implements SaTtlMethods {\n\n\t// ------------------- 索引操作公共代码\n\n\t/**\n\t * Raw Session 读写委托 (存储 Access-Token、Refresh-Token、Client-Token 索引)\n\t */\n\tpublic SaRawSessionDelegator oauth2RSD = new SaRawSessionDelegator(\"oauth2\");\n\n\t/**\n\t * 在 raw-session 中的保存 Access-Token 索引列表使用的 key\n\t */\n\tpublic static final String ACCESS_TOKEN_MAP = \"__HD_ACCESS_TOKEN_MAP\";\n\n\t/**\n\t * 在 raw-session 中的保存 Refresh-Token 索引列表使用的 key\n\t */\n\tpublic static final String REFRESH_TOKEN_MAP = \"__HD_REFRESH_TOKEN_MAP\";\n\n\t/**\n\t * 在 raw-session 中的保存 Client-Token 索引列表使用的 key\n\t */\n\tpublic static final String CLIENT_TOKEN_MAP = \"__HD_CLIENT_TOKEN_MAP\";\n\n\t/**\n\t * 获取：保存 Access-Token 索引时使用的 RawSession\n\t *\n\t * @param clientId 应用 id\n\t * @param loginId 账号 id\n\t * @param isCreate 如果尚未创建，是否立即创建\n\t * @return /\n\t */\n\tprotected SaSession getRawSessionByAccessToken(String clientId, Object loginId, boolean isCreate) {\n\t\tString value = splicingAccessTokenRSDValue(clientId, loginId);\n\t\treturn oauth2RSD.getSessionById(value, isCreate);\n\t}\n\n\t/**\n\t * 获取：保存 Refresh-Token 索引时使用的 RawSession\n\t *\n\t * @param clientId 应用 id\n\t * @param loginId 账号 id\n\t * @param isCreate 如果尚未创建，是否立即创建\n\t * @return /\n\t */\n\tprotected SaSession getRawSessionByRefreshToken(String clientId, Object loginId, boolean isCreate) {\n\t\tString value = splicingRefreshTokenRSDValue(clientId, loginId);\n\t\treturn oauth2RSD.getSessionById(value, isCreate);\n\t}\n\n\t/**\n\t * 获取：保存 Client-Token 索引时使用的 RawSession\n\t *\n\t * @param clientId 应用 id\n\t * @param isCreate 如果尚未创建，是否立即创建\n\t * @return /\n\t */\n\tprotected SaSession getRawSessionByClientToken(String clientId, boolean isCreate) {\n\t\tString value = splicingClientTokenRSDValue(clientId);\n\t\treturn oauth2RSD.getSessionById(value, isCreate);\n\t}\n\n\t/**\n\t * 在 RawSession 上添加 token 索引，并完整调整索引列表\n\t *\n\t * @param session 待操作的 RawSession\n\t * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key\n\t * @param token 待添加的 token\n\t * @param timeout 添加的 token 其过期时间\n\t * @param maxTokenCount 允许的最多 token 数量，超出的将被删除 (-1=不限制)\n\t * @param removeFun 执行删除 token 的函数\n\t */\n\tprotected void addTokenIndex_AndAdjust(SaSession session, String tokenIndexMapSaveKey, String token, long timeout, int maxTokenCount, SaParamFunction<String> removeFun) {\n\t\tMap<String, Long> tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap);\n\t\tif(! tokenIndexMap.containsKey(token)) {\n\t\t\t// 添加\n\t\t\ttokenIndexMap.put(token, ttlToExpireTime(timeout));\n\t\t\t// 剔除过期的\n\t\t\ttokenIndexMap = _removeExpiredIndex(tokenIndexMap);\n\t\t\t// 删掉溢出的\n\t\t\ttokenIndexMap = _removeOverflowIndex(tokenIndexMap, maxTokenCount, removeFun);\n\t\t\t// 保存\n\t\t\tsession.set(tokenIndexMapSaveKey, tokenIndexMap);\n\t\t\t// 更新 TTL\n\t\t\tlong maxTtl = getMaxTtlByExpireTime(tokenIndexMap.values());\n\t\t\tif(maxTtl != 0) {\n\t\t\t\tsession.updateTimeout(maxTtl);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 在 RawSession 上删除 token 索引，并尝试注销 RawSession\n\t *\n\t * @param session 待操作的 RawSession\n\t * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key\n\t * @param token 待删除的 token\n\t */\n\tprotected void deleteTokenIndex_AndTryLogout(SaSession session, String tokenIndexMapSaveKey, String token) {\n\t\tMap<String, Long> tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap);\n\t\ttokenIndexMap.remove(token);\n\t\t// 如果删除后还有记录，就再次保存\n\t\tif( ! tokenIndexMap.isEmpty()) {\n\t\t\tsession.set(tokenIndexMapSaveKey, tokenIndexMap);\n\t\t} else {\n\t\t\t// 没有的话就直接注销此 RawSession\n\t\t\tsession.logout();\n\t\t}\n\t}\n\n\t/**\n\t * 剔除已过期的 token 索引\n\t *\n\t * @param tokenIndexMap token 索引列表\n\t * @return 调整后的索引列表\n\t */\n\tprotected Map<String, Long> _removeExpiredIndex(Map<String, Long> tokenIndexMap) {\n\t\tMap<String, Long> newTokenList = newTokenIndexMap();\n\t\tfor (Map.Entry<String, Long> entry : tokenIndexMap.entrySet()) {\n\t\t\tlong ttl = expireTimeToTtl(entry.getValue());\n\t\t\tif(ttl != SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\t\tnewTokenList.put(entry.getKey(), entry.getValue());\n\t\t\t}\n\t\t}\n\t\treturn newTokenList;\n\t}\n\n\t/**\n\t * 将 token 索引列表中溢出的部分删除（按照插入顺序先进先出，不考虑每个剩余 token 剩余有效期）\n\t *\n\t * @param tokenIndexMap token 索引列表(key=token, value=token过期时间)（传入的 Map 必须是有序的）\n\t * @param maxTokenCount 允许的最多 token 数量，超出的将被删除 (-1=不限制)\n\t * @param removeFun 执行删除 token 的函数\n\t * @return 调整后的索引列表\n\t */\n\tprotected Map<String, Long> _removeOverflowIndex(Map<String, Long> tokenIndexMap, int maxTokenCount, SaParamFunction<String> removeFun) {\n\n\t\t// 如果当前数量未超过限制，直接返回\n\t\tif (tokenIndexMap.size() <= maxTokenCount || maxTokenCount == SaTokenDao.NEVER_EXPIRE) {\n\t\t\treturn tokenIndexMap;\n\t\t}\n\n\t\t// 创建新的索引 Map 副本\n\t\tMap<String, Long> newTokenIndexMap = newTokenIndexMap();\n\n\t\t// 溢出数量\n\t\tint overflowCount = tokenIndexMap.size() - maxTokenCount;\n\n\t\t// 已删除 Token 数量\n\t\tint removedCount = 0;\n\n\t\t// 遍历原 Map 的所有条目\n\t\tfor (Map.Entry<String, Long> entry : tokenIndexMap.entrySet()) {\n\t\t\tString token = entry.getKey();\n\t\t\tif (removedCount < overflowCount) {\n\t\t\t\t// 溢出部分：执行删除回调，但不添加到新 Map\n\t\t\t\tremoveFun.run(token);\n\t\t\t\tremovedCount++;\n\t\t\t} else {\n\t\t\t\t// 未溢出部分：添加到新 Map 副本\n\t\t\t\tnewTokenIndexMap.put(token, entry.getValue());\n\t\t\t}\n\t\t}\n\n\t\t// 返回索引 Map 副本\n\t\treturn newTokenIndexMap;\n\t}\n\n\t/**\n\t * 从 RawSession 获取 Token 索引列表（获取之前会完整调整索引列表，保证获取的都是有效 token）\n\t *\n\t * @param session 待操作的 RawSession\n\t * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key\n\t * @return /\n\t */\n\tprotected Map<String, Long> getTokenIndexMap_FromAdjustAfter(SaSession session, String tokenIndexMapSaveKey) {\n\t\tif(session == null) {\n\t\t\treturn newTokenIndexMap();\n\t\t}\n\n\t\t// 根据 ttl 值过滤一遍\n\t\tMap<String, Long> tokenIndexMap = session.get(tokenIndexMapSaveKey, this::newTokenIndexMap);\n\t\tMap<String, Long> newTokenIndexMap = _removeExpiredIndex(tokenIndexMap);\n\n\t\t// 如果调整后集合长度归零了，说明 token 已全部过期，直接注销此 RawSession\n\t\tif(newTokenIndexMap.isEmpty()) {\n\t\t\tsession.logout();\n\t\t\treturn newTokenIndexMap();\n\t\t}\n\n\t\t// 没有归零，但是长度变小了，说明有过期的 token，需要重写写入一遍\n\t\tif(tokenIndexMap.size() > newTokenIndexMap.size()) {\n\t\t\tsession.set(tokenIndexMapSaveKey, newTokenIndexMap);\n\t\t}\n\n\t\t// 转 List 返回\n\t\treturn newTokenIndexMap;\n\t}\n\n\t/**\n\t * 从 RawSession 获取 Token 列表（获取之前会完整调整索引列表，保证获取的都是有效 token）\n\t *\n\t * @param session 待操作的 RawSession\n\t * @param tokenIndexMapSaveKey 在 session 上保存 token 索引列表使用的 key\n\t * @return /\n\t */\n\tprotected List<String> getTokenValueList_FromAdjustAfter(SaSession session, String tokenIndexMapSaveKey) {\n\t\treturn new ArrayList<>(getTokenIndexMap_FromAdjustAfter(session, tokenIndexMapSaveKey).keySet());\n\t}\n\n\n\t// ------------------- code 操作\n\n\t/**\n\t * 保存：CodeModel\n\t * @param c / \n\t */\n\tpublic void saveCode(CodeModel c) {\n\t\tif(c == null) {\n\t\t\treturn;\n\t\t}\n\t\tgetSaTokenDao().setObject(splicingCodeSaveKey(c.code), c, SaOAuth2Manager.getServerConfig().getCodeTimeout());\n\t}\n\n\t/**\n\t * 删除：CodeModel\n\t * @param code /\n\t */\n\tpublic void deleteCode(String code) {\n\t\tif(code != null) {\n\t\t\tgetSaTokenDao().deleteObject(splicingCodeSaveKey(code));\n\t\t}\n\t}\n\n\t/**\n\t * 获取：CodeModel\n\t * @param code /\n\t * @return /\n\t */\n\tpublic CodeModel getCode(String code) {\n\t\tif(code == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn (CodeModel)getSaTokenDao().getObject(splicingCodeSaveKey(code));\n\t}\n\n\n\t// ------------------- code 索引\n\n\t/**\n\t * 保存：Code 索引\n\t * @param c /\n\t */\n\tpublic void saveCodeIndex(CodeModel c) {\n\t\tif(c == null) {\n\t\t\treturn;\n\t\t}\n\t\tgetSaTokenDao().set(splicingCodeIndexKey(c.clientId, c.loginId), c.code, SaOAuth2Manager.getServerConfig().getCodeTimeout());\n\t}\n\n\t/**\n\t * 删除：Code 索引\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t */\n\tpublic void deleteCodeIndex(String clientId, Object loginId) {\n\t\tgetSaTokenDao().delete(splicingCodeIndexKey(clientId, loginId));\n\t}\n\n\t/**\n\t * 获取：Code Value\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic String getCodeValue(String clientId, Object loginId) {\n\t\treturn getSaTokenDao().get(splicingCodeIndexKey(clientId, loginId));\n\t}\n\n\n\t// ------------------- Access-Token Model\n\n\t/**\n\t * 保存：AccessTokenModel\n\t * @param at /\n\t */\n\tpublic void saveAccessToken(AccessTokenModel at) {\n\t\tif(at == null) {\n\t\t\treturn;\n\t\t}\n\t\tgetSaTokenDao().setObject(splicingAccessTokenSaveKey(at.accessToken), at, at.getExpiresIn());\n\t}\n\n\t/**\n\t * 删除：AccessTokenModel\n\t * @param accessToken 值\n\t */\n\tpublic void deleteAccessToken(String accessToken) {\n\t\tif(accessToken != null) {\n\t\t\tgetSaTokenDao().deleteObject(splicingAccessTokenSaveKey(accessToken));\n\t\t}\n\t}\n\n\t/**\n\t * 获取：AccessTokenModel\n\t * @param accessToken /\n\t * @return /\n\t */\n\tpublic AccessTokenModel getAccessToken(String accessToken) {\n\t\tif(accessToken == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn (AccessTokenModel)getSaTokenDao().getObject(splicingAccessTokenSaveKey(accessToken));\n\t}\n\n\n\t// ------------------- Access-Token 索引\n\n\t/**\n\t * 保存：Access-Token 索引，并完整调整索引列表\n\t *\n\t * @param at /\n\t * @param maxAccessTokenCount 允许的最多 Access-Token 数量，超出的将被删除 (-1=不限制)\n\t */\n\tpublic void saveAccessTokenIndex_AndAdjust(AccessTokenModel at, int maxAccessTokenCount) {\n\t\tif(at == null) {\n\t\t\treturn;\n\t\t}\n\t\tSaSession session = getRawSessionByAccessToken(at.clientId, at.loginId, true);\n\t\taddTokenIndex_AndAdjust(session, ACCESS_TOKEN_MAP, at.accessToken, at.getExpiresIn(), maxAccessTokenCount, this::deleteAccessToken);\n\t}\n\n\t/**\n\t * 删除：Access-Token 在 RawSession 上的单个索引数据\n\t *\n\t * @param clientId 应用 id\n\t * @param loginId 账号id\n\t * @param accessToken 值\n\t */\n\tpublic void deleteAccessTokenIndex_BySingleData(String clientId, Object loginId, String accessToken) {\n\t\tSaSession session = getRawSessionByAccessToken(clientId, loginId, false);\n\t\tif(session == null) {\n\t\t\treturn;\n\t\t}\n\t\tdeleteTokenIndex_AndTryLogout(session, ACCESS_TOKEN_MAP, accessToken);\n\t}\n\n\t/**\n\t * 删除：Access-Token 索引整体\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t */\n\tpublic void deleteAccessTokenIndex(String clientId, Object loginId) {\n\t\toauth2RSD.deleteSessionById(splicingAccessTokenRSDValue(clientId, loginId));\n\t}\n\n\t/**\n\t * 获取 Access-Token 索引列表（获取之前会完整调整索引列表，保证获取的都是有效 AccessToken 索引）\n\t *\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic Map<String, Long> getAccessTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) {\n\t\tSaSession session = getRawSessionByAccessToken(clientId, loginId, false);\n\t\treturn getTokenIndexMap_FromAdjustAfter(session, ACCESS_TOKEN_MAP);\n\t}\n\n\t/**\n\t * 获取 Access-Token 列表（获取之前会完整调整索引列表，保证获取的都是有效 AccessToken）\n\t *\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic List<String> getAccessTokenValueList_FromAdjustAfter(String clientId, Object loginId) {\n\t\tSaSession session = getRawSessionByAccessToken(clientId, loginId, false);\n\t\treturn getTokenValueList_FromAdjustAfter(session, ACCESS_TOKEN_MAP);\n\t}\n\n\n\t// ------------------- Refresh-Token Model\n\n\t/**\n\t * 保存：RefreshTokenModel\n\t * @param rt .\n\t */\n\tpublic void saveRefreshToken(RefreshTokenModel rt) {\n\t\tif(rt == null) {\n\t\t\treturn;\n\t\t}\n\t\tgetSaTokenDao().setObject(splicingRefreshTokenSaveKey(rt.refreshToken), rt, rt.getExpiresIn());\n\t}\n\n\t/**\n\t * 删除：RefreshTokenModel\n\t * @param refreshToken 值\n\t */\n\tpublic void deleteRefreshToken(String refreshToken) {\n\t\tif(refreshToken != null) {\n\t\t\tgetSaTokenDao().deleteObject(splicingRefreshTokenSaveKey(refreshToken));\n\t\t}\n\t}\n\n\t/**\n\t * 获取：RefreshTokenModel\n\t * @param refreshToken /\n\t * @return /\n\t */\n\tpublic RefreshTokenModel getRefreshToken(String refreshToken) {\n\t\tif(refreshToken == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn (RefreshTokenModel)getSaTokenDao().getObject(splicingRefreshTokenSaveKey(refreshToken));\n\t}\n\n\n\t// ------------------- Refresh-Token 索引\n\n\t/**\n\t * 保存：Refresh-Token 索引\n\t *\n\t * @param rt /\n\t * @param maxRefreshTokenCount 允许的最多 Refresh-Token 数量，超出的将被删除 (-1=不限制)\n\t */\n\tpublic void saveRefreshTokenIndex_AndAdjust(RefreshTokenModel rt, int maxRefreshTokenCount) {\n\t\tif(rt == null) {\n\t\t\treturn;\n\t\t}\n\t\tSaSession session = getRawSessionByRefreshToken(rt.clientId, rt.loginId, true);\n\t\taddTokenIndex_AndAdjust(session, REFRESH_TOKEN_MAP, rt.refreshToken, rt.getExpiresIn(), maxRefreshTokenCount, this::deleteRefreshToken);\n\t}\n\n\t/**\n\t * 删除：Refresh-Token 在 RawSession 上的单个索引数据\n\t *\n\t * @param clientId 应用 id\n\t * @param loginId 账号id\n\t * @param refreshToken 值\n\t */\n\tpublic void deleteRefreshTokenIndex_BySingleData(String clientId, Object loginId, String refreshToken) {\n\t\tSaSession session = getRawSessionByRefreshToken(clientId, loginId, false);\n\t\tif(session == null) {\n\t\t\treturn;\n\t\t}\n\t\tdeleteTokenIndex_AndTryLogout(session, REFRESH_TOKEN_MAP, refreshToken);\n\t}\n\n\t/**\n\t * 删除：Refresh-Token 索引整体\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t */\n\tpublic void deleteRefreshTokenIndex(String clientId, Object loginId) {\n\t\toauth2RSD.deleteSessionById(splicingRefreshTokenRSDValue(clientId, loginId));\n\t}\n\n\t/**\n\t * 获取 Refresh-Token 索引列表（获取之前会完整调整索引列表，保证获取的都是有效 RefreshToken 索引）\n\t *\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic Map<String, Long> getRefreshTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) {\n\t\tSaSession session = getRawSessionByRefreshToken(clientId, loginId, false);\n\t\treturn getTokenIndexMap_FromAdjustAfter(session, REFRESH_TOKEN_MAP);\n\t}\n\n\t/**\n\t * 获取 Refresh-Token 列表（获取之前会完整调整索引列表，保证获取的都是有效 RefreshToken）\n\t *\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic List<String> getRefreshTokenValueList_FromAdjustAfter(String clientId, Object loginId) {\n\t\tSaSession session = getRawSessionByRefreshToken(clientId, loginId, false);\n\t\treturn getTokenValueList_FromAdjustAfter(session, REFRESH_TOKEN_MAP);\n\t}\n\n\n\t// ------------------- Client-Token Model\n\n\t/**\n\t * 保存：ClientTokenModel\n\t * @param ct .\n\t */\n\tpublic void saveClientToken(ClientTokenModel ct) {\n\t\tif(ct == null) {\n\t\t\treturn;\n\t\t}\n\t\tgetSaTokenDao().setObject(splicingClientTokenSaveKey(ct.clientToken), ct, ct.getExpiresIn());\n\t}\n\n\t/**\n\t * 删除：ClientTokenModel\n\t * @param clientToken 值\n\t */\n\tpublic void deleteClientToken(String clientToken) {\n\t\tif(clientToken != null) {\n\t\t\tgetSaTokenDao().deleteObject(splicingClientTokenSaveKey(clientToken));\n\t\t}\n\t}\n\n\t/**\n\t * 获取：ClientTokenModel\n\t * @param clientToken /\n\t * @return /\n\t */\n\tpublic ClientTokenModel getClientToken(String clientToken) {\n\t\tif(clientToken == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getSaTokenDao().getObject(splicingClientTokenSaveKey(clientToken), ClientTokenModel.class);\n\t}\n\n\n\t// ------------------- Client-Token 索引\n\n\t/**\n\t * 保存：Client-Token 索引\n\t *\n\t * @param ct /\n\t * @param maxClientTokenCount 允许的最多 Client-Token 数量，超出的将被删除 (-1=不限制)\n\t */\n\tpublic void saveClientTokenIndex_AndAdjust(ClientTokenModel ct, int maxClientTokenCount) {\n\t\tif(ct == null) {\n\t\t\treturn;\n\t\t}\n\t\tSaSession session = getRawSessionByClientToken(ct.clientId, true);\n\t\taddTokenIndex_AndAdjust(session, CLIENT_TOKEN_MAP, ct.clientToken, ct.getExpiresIn(), maxClientTokenCount, this::deleteClientToken);\n\t}\n\n\t/**\n\t * 删除：Client-Token 在 RawSession 上的单个索引数据\n\t * @param clientId 应用 id\n\t * @param clientToken 值\n\t */\n\tpublic void deleteClientTokenIndex_BySingleData(String clientId, String clientToken) {\n\t\tSaSession session = getRawSessionByClientToken(clientId, false);\n\t\tif(session == null) {\n\t\t\treturn;\n\t\t}\n\t\tdeleteTokenIndex_AndTryLogout(session, CLIENT_TOKEN_MAP, clientToken);\n\t}\n\n\t/**\n\t * 删除：Client-Token 索引整体\n\t *\n\t * @param clientId 应用id\n\t */\n\tpublic void deleteClientTokenIndex(String clientId) {\n\t\toauth2RSD.deleteSessionById(splicingClientTokenRSDValue(clientId));\n\t}\n\n\t/**\n\t * 获取 Client-Token 索引列表（获取之前会完整调整索引列表，保证获取的都是有效 ClientToken 索引）\n\t *\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return /\n\t */\n\tpublic Map<String, Long> getClientTokenIndexMap_FromAdjustAfter(String clientId, Object loginId) {\n\t\tSaSession session = getRawSessionByClientToken(clientId, false);\n\t\treturn getTokenIndexMap_FromAdjustAfter(session, CLIENT_TOKEN_MAP);\n\t}\n\n\t/**\n\t * 获取 Client-Token 列表（获取之前会完整调整索引列表，保证获取的都是有效 ClientToken）\n\t *\n\t * @param clientId 应用id\n\t * @return /\n\t */\n\tpublic List<String> getClientTokenValueList_FromAdjustAfter(String clientId) {\n\t\tSaSession session = getRawSessionByClientToken(clientId, false);\n\t\treturn getTokenValueList_FromAdjustAfter(session, CLIENT_TOKEN_MAP);\n\t}\n\n\n\t// ------------------- GrantScope\n\n\t/**\n\t * 保存：用户授权记录\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @param scopes 权限列表\n\t */\n\tpublic void saveGrantScope(String clientId, Object loginId, List<String> scopes) {\n\t\tif( ! SaFoxUtil.isEmpty(scopes)) {\n\t\t\tlong ttl = checkClientModel(clientId).getAccessTokenTimeout();\n\t\t\tString value = SaOAuth2Manager.getDataConverter().convertScopeListToString(scopes);\n\t\t\tgetSaTokenDao().set(splicingGrantScopeKey(clientId, loginId), value, ttl);\n\t\t}\n\t}\n\n\t/**\n\t * 删除：用户授权记录\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t */\n\tpublic void deleteGrantScope(String clientId, Object loginId) {\n\t\tgetSaTokenDao().delete(splicingGrantScopeKey(clientId, loginId));\n\t}\n\n\t/**\n\t * 获取：用户授权记录\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return 权限\n\t */\n\tpublic List<String> getGrantScope(String clientId, Object loginId) {\n\t\tString value = getSaTokenDao().get(splicingGrantScopeKey(clientId, loginId));\n\t\treturn SaOAuth2Manager.getDataConverter().convertScopeStringToList(value);\n\t}\n\n\n\t// ------------------- State\n\n\t/**\n\t * 保存：state\n\t * @param state /\n\t */\n\tpublic void saveState(String state) {\n\t\tif( ! SaFoxUtil.isEmpty(state)) {\n\t\t\tlong ttl = SaOAuth2Manager.getServerConfig().getCodeTimeout();\n\t\t\tgetSaTokenDao().set(splicingStateSaveKey(state), state, ttl);\n\t\t}\n\t}\n\n\t/**\n\t * 删除：state记录\n\t * @param state /\n\t */\n\tpublic void deleteState(String state) {\n\t\tgetSaTokenDao().delete(splicingStateSaveKey(state));\n\t}\n\n\t/**\n\t * 获取：state\n\t * @param state /\n\t * @return /\n\t */\n\tpublic String getState(String state) {\n\t\tif(SaFoxUtil.isEmpty(state)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getSaTokenDao().get(splicingStateSaveKey(state));\n\t}\n\n\n\t// ------------------- 其它\n\n\t/**\n\t * 保存：nonce-索引\n\t * @param c /\n\t */\n\tpublic void saveCodeNonceIndex(CodeModel c) {\n\t\tif(c == null || SaFoxUtil.isEmpty(c.nonce)) {\n\t\t\treturn;\n\t\t}\n\t\tgetSaTokenDao().set(splicingCodeNonceIndexSaveKey(c.code), c.nonce, SaOAuth2Manager.getServerConfig().getCodeTimeout());\n\t}\n\n\t/**\n\t * 获取：nonce\n\t * @param code /\n\t * @return /\n\t */\n\tpublic String getNonce(String code) {\n\t\tif(SaFoxUtil.isEmpty(code)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn getSaTokenDao().get(splicingCodeNonceIndexSaveKey(code));\n\t}\n\n\n\t// ------------------- 拼接key\n\n\t/**\n\t * 拼接 key：Code 保存\n\t * @param code 授权码\n\t * @return key\n\t */\n\tpublic String splicingCodeSaveKey(String code) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:code:\" + code;\n\t}\n\n\t/**\n\t * 拼接 key：Code 索引\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return key\n\t */\n\tpublic String splicingCodeIndexKey(String clientId, Object loginId) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:code-index:\" + clientId + \":\" + loginId;\n\t}\n\n\t/**\n\t * 拼接 key：Access-Token 保存\n\t * @param accessToken accessToken\n\t * @return key\n\t */\n\tpublic String splicingAccessTokenSaveKey(String accessToken) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:access-token:\" + accessToken;\n\t}\n\n\t/**\n\t * 拼接 key：Access-Token RSD Value\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return key\n\t */\n\tpublic String splicingAccessTokenRSDValue(String clientId, Object loginId) {\n\t\treturn \"access-token:\" + clientId + \":\" + loginId;\n\t}\n\n\t/**\n\t * 拼接 key：Refresh-Token 保存\n\t * @param refreshToken refreshToken\n\t * @return key\n\t */\n\tpublic String splicingRefreshTokenSaveKey(String refreshToken) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:refresh-token:\" + refreshToken;\n\t}\n\n\t/**\n\t * 拼接 key：Refresh-Token RSD Value\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return key\n\t */\n\tpublic String splicingRefreshTokenRSDValue(String clientId, Object loginId) {\n\t\treturn \"refresh-token:\" + clientId + \":\" + loginId;\n\t}\n\n\t/**\n\t * 拼接 key：Client-Token 保存\n\t * @param clientToken clientToken\n\t * @return key\n\t */\n\tpublic String splicingClientTokenSaveKey(String clientToken) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:client-token:\" + clientToken;\n\t}\n\n\t/**\n\t * 拼接 key：Client-Token RSD Value\n\t * @param clientId 应用id\n\t * @return key\n\t */\n\tpublic String splicingClientTokenRSDValue(String clientId) {\n\t\treturn \"client-token:\" + clientId;\n\t}\n\n\t/**\n\t * 拼接 key：用户授权记录\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @return key\n\t */\n\tpublic String splicingGrantScopeKey(String clientId, Object loginId) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:grant-scope:\" + clientId + \":\" + loginId;\n\t}\n\n\t/**\n\t * 拼接 key：state 参数保存\n\t * @param state /\n\t * @return key\n\t */\n\tpublic String splicingStateSaveKey(String state) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:state:\" + state;\n\t}\n\n\t/**\n\t * 拼接 key：code-nonce 索引 参数保存\n\t * @param code 授权码\n\t * @return key\n\t */\n\tpublic String splicingCodeNonceIndexSaveKey(String code) {\n\t\treturn getSaTokenConfig().getTokenName() + \":oauth2:code-nonce-index:\" + code;\n\t}\n\n\n\t// -------- bean 对象代理\n\n\t/**\n\t * 获取使用的 getSaTokenDao 实例\n\t * \n\t * @return /\n\t */\n\tpublic SaTokenDao getSaTokenDao() {\n\t\treturn SaManager.getSaTokenDao();\n\t}\n\n\t/**\n\t * 获取使用的 SaTokenConfig 实例\n\t *\n\t * @return /\n\t */\n\tpublic SaTokenConfig getSaTokenConfig() {\n\t\treturn SaManager.getConfig();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.convert;\n\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.RefreshTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\n\nimport java.util.List;\n\n/**\n * Sa-Token OAuth2 数据格式转换器\n *\n * @author click33\n * @since 1.39.0\n */\npublic interface SaOAuth2DataConverter {\n\n    /**\n     * 转换 scope 数据格式：String -> List\n     * @param scopeString /\n     * @return /\n     */\n    List<String> convertScopeStringToList(String scopeString);\n\n    /**\n     * 转换 scope 数据格式：List -> String\n     * @param scopeList /\n     * @return /\n     */\n    String convertScopeListToString(List<String> scopeList);\n\n    /**\n     * 转换 redirect_uri 数据格式：String -> List\n     * @param redirectUris /\n     * @return /\n     */\n    List<String> convertRedirectUriStringToList(String redirectUris);\n\n    /**\n     * 根据 RequestAuthModel 构建一个 CodeModel\n     * @param ra RequestAuthModel\n     * @return CodeModel 对象\n     */\n    CodeModel convertRequestAuthToCode(RequestAuthModel ra);\n\n    /**\n     * 根据 RequestAuthModel 构建一个 AccessTokenModel\n     * @param ra RequestAuthModel\n     * @param accessTokenTimeout Access-Token 有效期 (单位：秒)\n     * @return AccessTokenModel 对象\n     */\n    AccessTokenModel convertRequestAuthToAccessToken(RequestAuthModel ra, long accessTokenTimeout);\n\n    /**\n     * 根据 Code 构建一个 Access-Token\n     * @param cm CodeModel对象\n     * @param accessTokenTimeout Access-Token 有效期 (单位：秒)\n     * @return AccessToken对象\n     */\n    AccessTokenModel convertCodeToAccessToken(CodeModel cm, long accessTokenTimeout);\n\n    /**\n     * 根据 Access-Token 构建一个 Refresh-Token\n     * @param at /\n     * @param refreshTokenTimeout Refresh-Token 有效期 (单位：秒)\n     * @return /\n     */\n    RefreshTokenModel convertAccessTokenToRefreshToken(AccessTokenModel at, long refreshTokenTimeout);\n\n    /**\n     * 根据 Refresh-Token 构建一个 Access-Token\n     * @param rt /\n     * @param accessTokenTimeout Access-Token 有效期 (单位：秒)\n     * @return /\n     */\n    AccessTokenModel convertRefreshTokenToAccessToken(RefreshTokenModel rt, long accessTokenTimeout);\n\n    /**\n     * 根据 Refresh-Token 构建一个新的 Refresh-Token\n     * @param rt /\n     * @param refreshTokenTimeout Refresh-Token 有效期 (单位：秒)\n     * @return /\n     */\n    RefreshTokenModel convertRefreshTokenToRefreshToken(RefreshTokenModel rt, long refreshTokenTimeout);\n\n    /**\n     * 根据 SaClientModel 构建一个 ClientTokenModel\n     * @param clientModel /\n     * @param scopes 权限列表\n     * @return /\n     */\n    ClientTokenModel convertSaClientToClientToken(SaClientModel clientModel, List<String> scopes);\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/convert/SaOAuth2DataConverterDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.convert;\n\nimport cn.dev33.satoken.oauth2.consts.GrantType;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.RefreshTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaTtlMethods;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\n\n/**\n * Sa-Token OAuth2 数据格式转换器，默认实现类\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2DataConverterDefaultImpl implements SaOAuth2DataConverter, SaTtlMethods {\n\n    /**\n     * 转换 scope 数据格式：String -> List\n     */\n    @Override\n    public List<String> convertScopeStringToList(String scopeString) {\n        if(SaFoxUtil.isEmpty(scopeString)) {\n            return new ArrayList<>();\n        }\n        // 兼容以下三种分隔符：空格、逗号、%20、加号\n        scopeString = scopeString.replace(\" \", \",\");\n        scopeString = scopeString.replace(\"%20\", \",\");\n        scopeString = scopeString.replace(\"+\", \",\");\n        return SaFoxUtil.convertStringToList(scopeString);\n    }\n\n    /**\n     * 转换 scope 数据格式：List -> String\n     */\n    @Override\n    public String convertScopeListToString(List<String> scopeList) {\n        return SaFoxUtil.convertListToString(scopeList);\n    }\n\n    /**\n     * 转换 redirect_uri 数据格式：String -> List\n     */\n    @Override\n    public List<String> convertRedirectUriStringToList(String redirectUris) {\n        if(SaFoxUtil.isEmpty(redirectUris)) {\n            return new ArrayList<>();\n        }\n        return SaFoxUtil.convertStringToList(redirectUris);\n    }\n\n    /**\n     * 根据 RequestAuthModel 构建一个 CodeModel\n     * @param ra RequestAuthModel\n     * @return CodeModel 对象\n     */\n    @Override\n    public CodeModel convertRequestAuthToCode(RequestAuthModel ra){\n        String codeValue = SaOAuth2Strategy.instance.createCodeValue.execute(ra.clientId, ra.loginId, ra.scopes);\n        CodeModel cm = new CodeModel();\n        cm.code = codeValue;\n        cm.clientId = ra.clientId;\n        cm.scopes = ra.scopes;\n        cm.loginId = ra.loginId;\n        cm.redirectUri = ra.redirectUri;\n        cm.nonce = ra.getNonce();\n        return cm;\n    }\n\n    /**\n     * 根据 RequestAuthModel 构建一个 AccessTokenModel\n     * @param ra RequestAuthModel\n     * @return AccessTokenModel 对象\n     */\n    @Override\n    public AccessTokenModel convertRequestAuthToAccessToken(RequestAuthModel ra, long accessTokenTimeout) {\n        String newAtValue = SaOAuth2Strategy.instance.createAccessToken.execute(ra.clientId, ra.loginId, ra.scopes);\n        AccessTokenModel at = new AccessTokenModel();\n        at.accessToken = newAtValue;\n        at.clientId = ra.clientId;\n        at.loginId = ra.loginId;\n        at.scopes = ra.scopes;\n        at.tokenType = SaOAuth2Consts.TokenType.Bearer;\n        at.expiresTime = ttlToExpireTime(accessTokenTimeout);\n        at.extraData = new LinkedHashMap<>();\n        return at;\n    }\n\n    /**\n     * 根据 Code 构建一个 Access-Token\n     */\n    @Override\n    public AccessTokenModel convertCodeToAccessToken(CodeModel cm, long accessTokenTimeout) {\n        AccessTokenModel at = new AccessTokenModel();\n        at.accessToken = SaOAuth2Strategy.instance.createAccessToken.execute(cm.clientId, cm.loginId, cm.scopes);\n        at.clientId = cm.clientId;\n        at.loginId = cm.loginId;\n        at.scopes = cm.scopes;\n        at.tokenType = SaOAuth2Consts.TokenType.Bearer;\n        at.grantType = GrantType.authorization_code;\n        at.expiresTime = ttlToExpireTime(accessTokenTimeout);\n        at.extraData = new LinkedHashMap<>();\n        return at;\n    }\n\n    /**\n     * 根据 Access-Token 构建一个 Refresh-Token\n     */\n    @Override\n    public RefreshTokenModel convertAccessTokenToRefreshToken(AccessTokenModel at, long refreshTokenTimeout) {\n        RefreshTokenModel rt = new RefreshTokenModel();\n        rt.refreshToken = SaOAuth2Strategy.instance.createRefreshToken.execute(at.clientId, at.loginId, at.scopes);\n        rt.clientId = at.clientId;\n        rt.loginId = at.loginId;\n        rt.scopes = at.scopes;\n        rt.expiresTime = ttlToExpireTime(refreshTokenTimeout);\n        rt.extraData = new LinkedHashMap<>(at.extraData);\n        return rt;\n    }\n\n    /**\n     * 根据 Refresh-Token 构建一个 Access-Token\n     */\n    @Override\n    public AccessTokenModel convertRefreshTokenToAccessToken(RefreshTokenModel rt, long accessTokenTimeout) {\n        AccessTokenModel at = new AccessTokenModel();\n        at.accessToken = SaOAuth2Strategy.instance.createAccessToken.execute(rt.clientId, rt.loginId, rt.scopes);\n        at.refreshToken = rt.refreshToken;\n        at.clientId = rt.clientId;\n        at.loginId = rt.loginId;\n        at.scopes = rt.scopes;\n        at.tokenType = SaOAuth2Consts.TokenType.Bearer;\n        at.grantType = GrantType.refresh_token;\n        at.extraData = new LinkedHashMap<>(rt.extraData);\n        at.expiresTime = ttlToExpireTime(accessTokenTimeout);\n        at.refreshExpiresTime = rt.expiresTime;\n        return at;\n    }\n\n    /**\n     * 根据 Refresh-Token 构建一个新的 Refresh-Token\n     */\n    @Override\n    public RefreshTokenModel convertRefreshTokenToRefreshToken(RefreshTokenModel rt, long refreshTokenTimeout) {\n        RefreshTokenModel newRt = new RefreshTokenModel();\n        newRt.refreshToken = SaOAuth2Strategy.instance.createRefreshToken.execute(rt.clientId, rt.loginId, rt.scopes);\n        newRt.expiresTime = ttlToExpireTime(refreshTokenTimeout);\n        newRt.clientId = rt.clientId;\n        newRt.scopes = rt.scopes;\n        newRt.loginId = rt.loginId;\n        newRt.extraData = new LinkedHashMap<>(rt.extraData);\n        return newRt;\n    }\n\n    /**\n     * 根据 SaClientModel 构建一个 ClientTokenModel\n     * @param clientModel /\n     * @param scopes 权限列表\n     * @return /\n     */\n    @Override\n    public ClientTokenModel convertSaClientToClientToken(SaClientModel clientModel, List<String> scopes) {\n        String clientTokenValue = SaOAuth2Strategy.instance.createClientToken.execute(clientModel.getClientId(), scopes);\n        ClientTokenModel ct = new ClientTokenModel(clientTokenValue, clientModel.getClientId(), scopes);\n        ct.clientToken = clientTokenValue;\n        ct.clientId = clientModel.getClientId();\n        ct.scopes = scopes;\n        ct.tokenType = SaOAuth2Consts.TokenType.Bearer;\n        ct.expiresTime = ttlToExpireTime(clientModel.getClientTokenTimeout());\n        ct.grantType = GrantType.client_credentials;\n        ct.extraData = new LinkedHashMap<>();\n        return ct;\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.generate;\n\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\n\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * Sa-Token OAuth2 数据构建器，负责相关 Model 数据构建\n *\n * @author click33\n * @since 1.39.0\n */\npublic interface SaOAuth2DataGenerate {\n\n    /**\n     * 构建 Model：Code授权码\n     * @param ra 请求参数Model\n     * @return 授权码Model\n     */\n    CodeModel generateCode(RequestAuthModel ra);\n\n    /**\n     * 构建 Model：Access-Token (根据 code 授权码)\n     * @param code 授权码Model\n     * @return AccessToken Model\n     */\n    AccessTokenModel generateAccessToken(String code);\n\n    /**\n     * 刷新 Model：根据 Refresh-Token 生成一个新的 Access-Token\n     * @param refreshToken Refresh-Token值\n     * @return 新的 Access-Token\n     */\n    AccessTokenModel refreshAccessToken(String refreshToken);\n\n    /**\n     * 构建 Model：Access-Token (根据 RequestAuthModel 构建，用于隐藏式 and 密码式)\n     * @param ra 请求参数Model\n     * @param isCreateRt 是否生成对应的Refresh-Token\n     * @param appendWork 对生成的 AccessTokenModel 进行追加操作\n     * @return Access-Token Model\n     */\n    AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt, Consumer<AccessTokenModel> appendWork);\n\n    /**\n     * 构建 Model：Client-Token\n     * @param clientId 应用id\n     * @param scopes 授权范围\n     * @return Client-Token Model\n     */\n    ClientTokenModel generateClientToken(String clientId, List<String> scopes);\n\n    /**\n     * 构建 URL：下放Code URL (Authorization Code 授权码)\n     * @param redirectUri 下放地址\n     * @param code code参数\n     * @param state state参数\n     * @return 构建完毕的URL\n     */\n    String buildRedirectUri(String redirectUri, String code, String state);\n\n    /**\n     * 构建 URL：下放Access-Token URL （implicit 隐藏式）\n     * @param redirectUri 下放地址\n     * @param token token\n     * @param state state参数\n     * @return 构建完毕的URL\n     */\n    String buildImplicitRedirectUri(String redirectUri, String token, String state);\n\n    /**\n     * 检查 state 是否被重复使用\n     * @param state /\n     */\n    void checkState(String state);\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.generate;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.dao.SaOAuth2Dao;\nimport cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.RefreshTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2AuthorizationCodeException;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2RefreshTokenException;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaTtlMethods;\n\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * Sa-Token OAuth2 数据构建器，默认实现类\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate, SaTtlMethods {\n\n    /**\n     * 构建 Model：Code授权码\n     * @param ra 请求参数Model\n     * @return 授权码Model\n     */\n    @Override\n    public CodeModel generateCode(RequestAuthModel ra) {\n\n        SaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n        // 删除旧 Code\n        dao.deleteCode(dao.getCodeValue(ra.clientId, ra.loginId));\n\n        // 生成新 Code\n        CodeModel cm = SaOAuth2Manager.getDataConverter().convertRequestAuthToCode(ra);\n\n        // 保存新 Code\n        dao.saveCode(cm);\n        dao.saveCodeIndex(cm);\n\n        // 保存 code -> nonce\n        dao.saveCodeNonceIndex(cm);\n\n        // 返回\n        return cm;\n    }\n\n    /**\n     * 构建 Model：Access-Token (根据 code 授权码)\n     * @param code 授权码\n     * @return AccessToken Model\n     */\n    @Override\n    public AccessTokenModel generateAccessToken(String code) {\n\n        SaOAuth2Dao dao = SaOAuth2Manager.getDao();\n        SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter();\n\n        // 1、先校验\n        CodeModel cm = dao.getCode(code);\n        SaOAuth2AuthorizationCodeException.throwBy(cm == null, \"无效 code: \" + code, code, SaOAuth2ErrorCode.CODE_30110);\n\n        // 2、开发者自定义的授权前置检查\n        SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(cm.loginId, cm.clientId);\n\n        // 3、生成新 Access-Token\n        SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(cm.clientId);\n        AccessTokenModel at = dataConverter.convertCodeToAccessToken(cm, clientModel.getAccessTokenTimeout());\n        SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at);\n\n        // 4、生成新 Refresh-Token\n        RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at, clientModel.getRefreshTokenTimeout());\n        at.refreshToken = rt.refreshToken;\n        at.refreshExpiresTime = rt.expiresTime;\n\n        // 5、保存 Access-Token、Refresh-Token\n        dao.saveAccessToken(at);\n        dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount());\n        dao.saveRefreshToken(rt);\n        dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount());\n\n        // 6、删除 Code (一个 code 只可以使用一次)\n        dao.deleteCode(code);\n        dao.deleteCodeIndex(cm.clientId, cm.loginId);\n\n        // 7、返回 Access-Token\n        return at;\n    }\n\n    /**\n     * 刷新 Model：根据 Refresh-Token 生成一个新的 Access-Token\n     * @param refreshToken Refresh-Token值\n     * @return 新的 Access-Token\n     */\n    @Override\n    public AccessTokenModel refreshAccessToken(String refreshToken) {\n\n        SaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n        // 1、获取 Refresh-Token 信息\n        RefreshTokenModel rt = dao.getRefreshToken(refreshToken);\n        SaOAuth2RefreshTokenException.throwBy(rt == null, \"无效 refresh_token: \" + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111);\n\n        // 2、开发者自定义的授权前置检查\n        SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(rt.loginId, rt.clientId);\n\n        // 3、如果配置了 isNewRefresh=true，则生成一个新的 Refresh-Token\n        SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(rt.clientId);\n        if(clientModel.getIsNewRefresh()) {\n            rt = SaOAuth2Manager.getDataConverter().convertRefreshTokenToRefreshToken(rt, clientModel.getRefreshTokenTimeout());\n            dao.saveRefreshToken(rt);\n            dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount());\n        }\n\n        // 4、生成新 Access-Token\n        AccessTokenModel at = SaOAuth2Manager.getDataConverter().convertRefreshTokenToAccessToken(rt, clientModel.getAccessTokenTimeout());\n        SaOAuth2Strategy.instance.refreshAccessTokenWorkByScope.accept(at);\n\n        // 5、保存新 Access-Token\n        dao.saveAccessToken(at);\n        dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount());\n\n        // 6、返回新 Access-Token\n        return at;\n    }\n\n    /**\n     * 构建 Model：Access-Token (根据 RequestAuthModel 构建，用于隐藏式 and 密码式)\n     * @param ra 请求参数Model\n     * @param isCreateRt 是否生成对应的Refresh-Token\n     * @param appendWork 对生成的 AccessTokenModel 进行追加操作\n     *\n     * @return Access-Token Model\n     */\n    @Override\n    public AccessTokenModel generateAccessToken(RequestAuthModel ra, boolean isCreateRt, Consumer<AccessTokenModel> appendWork) {\n\n        SaOAuth2Dao dao = SaOAuth2Manager.getDao();\n        SaOAuth2DataConverter dataConverter = SaOAuth2Manager.getDataConverter();\n\n        // 1、开发者自定义的授权前置检查\n        SaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId);\n\n        // 2、生成 Access-Token\n        SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(ra.clientId);\n        AccessTokenModel at = dataConverter.convertRequestAuthToAccessToken(ra, clientModel.getAccessTokenTimeout());\n        if(appendWork != null) {\n            appendWork.accept(at);\n        }\n        SaOAuth2Strategy.instance.workAccessTokenByScope.accept(at);\n\n        // 3、生成 & 保存 Refresh-Token\n        if(isCreateRt) {\n            RefreshTokenModel rt = dataConverter.convertAccessTokenToRefreshToken(at, clientModel.getRefreshTokenTimeout());\n            at.refreshToken = rt.refreshToken;\n            at.refreshExpiresTime = rt.expiresTime;\n\n            dao.saveRefreshToken(rt);\n            dao.saveRefreshTokenIndex_AndAdjust(rt, clientModel.getMaxRefreshTokenCount());\n        }\n\n        // 4、保存 Access-Token\n        dao.saveAccessToken(at);\n        dao.saveAccessTokenIndex_AndAdjust(at, clientModel.getMaxAccessTokenCount());\n\n        // 5、返回 Access-Token\n        return at;\n    }\n\n    /**\n     * 构建 Model：Client-Token\n     * @param clientId 应用id\n     * @param scopes 授权范围\n     * @return Client-Token Model\n     */\n    @Override\n    public ClientTokenModel generateClientToken(String clientId, List<String> scopes) {\n\n        SaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n        // 1、如果配置了 Lower-Client-Token 的 ttl ，则需要更新一下\n        SaClientModel clientModel = SaOAuth2Manager.getDataLoader().getClientModelNotNull(clientId);\n\n        // 2、生成 Client-Token\n        ClientTokenModel ct = SaOAuth2Manager.getDataConverter().convertSaClientToClientToken(clientModel, scopes);\n        SaOAuth2Strategy.instance.workClientTokenByScope.accept(ct);\n\n        // 3、保存 Client-Token\n        dao.saveClientToken(ct);\n        dao.saveClientTokenIndex_AndAdjust(ct, clientModel.getMaxClientTokenCount());\n\n        // 4、返回\n        return ct;\n    }\n\n    /**\n     * 构建 URL：下放Code URL (Authorization Code 授权码)\n     * @param redirectUri 下放地址\n     * @param code code参数\n     * @param state state参数\n     * @return 构建完毕的URL\n     */\n    @Override\n    public String buildRedirectUri(String redirectUri, String code, String state) {\n        String url = SaFoxUtil.joinParam(redirectUri, SaOAuth2Consts.Param.code, code);\n        if( ! SaFoxUtil.isEmpty(state)) {\n            checkState(state);\n            url = SaFoxUtil.joinParam(url, SaOAuth2Consts.Param.state, state);\n        }\n        return url;\n    }\n\n    /**\n     * 构建 URL：下放Access-Token URL （implicit 隐藏式）\n     * @param redirectUri 下放地址\n     * @param token token\n     * @param state state参数\n     * @return 构建完毕的URL\n     */\n    @Override\n    public String buildImplicitRedirectUri(String redirectUri, String token, String state) {\n        String url = SaFoxUtil.joinSharpParam(redirectUri, SaOAuth2Consts.Param.token, token);\n        if( ! SaFoxUtil.isEmpty(state)) {\n            checkState(state);\n            url = SaFoxUtil.joinSharpParam(url, SaOAuth2Consts.Param.state, state);\n        }\n        return url;\n    }\n\n    /**\n     * 检查 state 是否被重复使用\n     * @param state /\n     */\n    @Override\n    public void checkState(String state) {\n        String value = SaOAuth2Manager.getDao().getState(state);\n        if(SaFoxUtil.isNotEmpty(value)) {\n            throw new SaOAuth2Exception(\"多次请求的 state 不可重复: \" + state).setCode(SaOAuth2ErrorCode.CODE_30127);\n        }\n        SaOAuth2Manager.getDao().saveState(state);\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.loader;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2ClientModelException;\nimport cn.dev33.satoken.secure.SaSecureUtil;\n\nimport java.util.List;\n\n/**\n * Sa-Token OAuth2 数据加载器\n *\n * @author click33\n * @since 1.39.0\n */\npublic interface SaOAuth2DataLoader {\n\n    /**\n     * 根据 id 获取 Client 信息\n     *\n     * @param clientId 应用id\n     * @return ClientModel\n     */\n    default SaClientModel getClientModel(String clientId) {\n        // 默认从内存配置中读取数据\n        return SaOAuth2Manager.getServerConfig().getClients().get(clientId);\n    }\n\n    /**\n     * 根据 id 获取 Client 信息，不允许为 null\n     *\n     * @param clientId 应用id\n     * @return ClientModel\n     */\n    default SaClientModel getClientModelNotNull(String clientId) {\n        SaClientModel clientModel = getClientModel(clientId);\n        if(clientModel == null) {\n            throw new SaOAuth2ClientModelException(\"无效 client_id: \" + clientId)\n                    .setClientId(clientId)\n                    .setCode(SaOAuth2ErrorCode.CODE_30105);\n        }\n        return clientModel;\n    }\n\n    /**\n     * 根据 ClientId 和 LoginId 获取 openid\n     *\n     * @param clientId 应用id\n     * @param loginId 账号id\n     * @return 此账号在此Client下的openid\n     */\n    default String getOpenid(String clientId, Object loginId) {\n        return SaSecureUtil.md5(SaOAuth2Manager.getServerConfig().getOpenidDigestPrefix() + \"_\" + clientId + \"_\" + loginId);\n    }\n\n    /**\n     * 根据 subjectId 和 loginId 获取 unionid\n     *\n     * @param subjectId 应用主体id\n     * @param loginId 账号id\n     * @return 此账号在此主体 Client 下的 unionid\n     */\n    default String getUnionid(String subjectId, Object loginId) {\n        return SaSecureUtil.md5(SaOAuth2Manager.getServerConfig().getUnionidDigestPrefix() + \"_\" + subjectId + \"_\" + loginId);\n    }\n\n    /**\n     * 获取高级权限列表\n     * @return /\n     */\n    default List<String> getHigherScopeList() {\n        String higherScope = SaOAuth2Manager.getServerConfig().getHigherScope();\n        return SaOAuth2Manager.getDataConverter().convertScopeStringToList(higherScope);\n    }\n\n    /**\n     * 获取低级权限列表\n     * @return /\n     */\n    default List<String> getLowerScopeList() {\n        String lowerScope = SaOAuth2Manager.getServerConfig().getLowerScope();\n        return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope);\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoaderDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.loader;\n\n/**\n * Sa-Token OAuth2 数据加载器 默认实现类\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2DataLoaderDefaultImpl implements SaOAuth2DataLoader{\n\n    // be empty of\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/AccessTokenModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Model: Access-Token\n *\n * @author click33\n * @since 1.23.0\n */\npublic class AccessTokenModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * Access-Token 值\n\t */\n\tpublic String accessToken;\n\t\n\t/**\n\t * Refresh-Token 值\n\t */\n\tpublic String refreshToken;\n\t\n\t/**\n\t * Access-Token 到期时间 \n\t */\n\tpublic long expiresTime;\n\n\t/**\n\t * Refresh-Token 到期时间   \n\t */\n\tpublic long refreshExpiresTime;\n\n\t/**\n\t * 应用id \n\t */\n\tpublic String clientId;\n\n\t/**\n\t * 账号id \n\t */\n\tpublic Object loginId;\n\n\t/**\n\t * 授权范围\n\t */\n\tpublic List<String> scopes;\n\n\t/**\n\t * Token 类型\n\t */\n\tpublic String tokenType;\n\n\t/**\n\t * 授权类型\n\t */\n\tpublic String grantType;\n\n\t/**\n\t * 扩展数据\n\t */\n\tpublic Map<String, Object> extraData;\n\n\t/**\n\t * 创建时间，13位时间戳\n\t */\n\tpublic long createTime;\n\n\n\tpublic AccessTokenModel() {\n\t\tthis.createTime = System.currentTimeMillis();\n\t}\n\n\t/**\n\t * 构建一个 \n\t * @param accessToken accessToken\n\t * @param clientId 应用id \n\t * @param scopes 请求授权范围\n\t * @param loginId 对应的账号id \n\t */\n\tpublic AccessTokenModel(String accessToken, String clientId, Object loginId, List<String> scopes) {\n\t\tthis();\n\t\tthis.accessToken = accessToken;\n\t\tthis.clientId = clientId;\n\t\tthis.loginId = loginId;\n\t\tthis.scopes = scopes;\n\t}\n\n\t// 额外追加方法\n\n\t/**\n\t * 获取：此 Access-Token 的剩余有效期（秒）\n\t * @return /\n\t */\n\tpublic long getExpiresIn() {\n\t\tlong s = (expiresTime - System.currentTimeMillis()) / 1000;\n\t\treturn s < 1 ? -2 : s;\n\t}\n\n\t/**\n\t * 获取：此 Refresh-Token 的剩余有效期（秒）\n\t * @return /\n\t */\n\tpublic long getRefreshExpiresIn() {\n\t\tlong s = (refreshExpiresTime - System.currentTimeMillis()) / 1000;\n\t\treturn s < 1 ? -2 : s;\n\t}\n\n\n\t// get set\n\n\tpublic String getAccessToken() {\n\t\treturn accessToken;\n\t}\n\n\tpublic AccessTokenModel setAccessToken(String accessToken) {\n\t\tthis.accessToken = accessToken;\n\t\treturn this;\n\t}\n\n\tpublic String getRefreshToken() {\n\t\treturn refreshToken;\n\t}\n\n\tpublic AccessTokenModel setRefreshToken(String refreshToken) {\n\t\tthis.refreshToken = refreshToken;\n\t\treturn this;\n\t}\n\n\tpublic long getExpiresTime() {\n\t\treturn expiresTime;\n\t}\n\n\tpublic AccessTokenModel setExpiresTime(long expiresTime) {\n\t\tthis.expiresTime = expiresTime;\n\t\treturn this;\n\t}\n\n\tpublic long getRefreshExpiresTime() {\n\t\treturn refreshExpiresTime;\n\t}\n\n\tpublic AccessTokenModel setRefreshExpiresTime(long refreshExpiresTime) {\n\t\tthis.refreshExpiresTime = refreshExpiresTime;\n\t\treturn this;\n\t}\n\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\tpublic AccessTokenModel setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\tpublic Object getLoginId() {\n\t\treturn loginId;\n\t}\n\n\tpublic AccessTokenModel setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\tpublic List<String> getScopes() {\n\t\treturn scopes;\n\t}\n\n\tpublic AccessTokenModel setScopes(List<String> scopes) {\n\t\tthis.scopes = scopes;\n\t\treturn this;\n\t}\n\n\tpublic String getTokenType() {\n\t\treturn tokenType;\n\t}\n\n\tpublic AccessTokenModel setTokenType(String tokenType) {\n\t\tthis.tokenType = tokenType;\n\t\treturn this;\n\t}\n\n\tpublic String getGrantType() {\n\t\treturn grantType;\n\t}\n\n\tpublic AccessTokenModel setGrantType(String grantType) {\n\t\tthis.grantType = grantType;\n\t\treturn this;\n\t}\n\n\tpublic Map<String, Object> getExtraData() {\n\t\treturn extraData;\n\t}\n\n\tpublic AccessTokenModel setExtraData(Map<String, Object> extraData) {\n\t\tthis.extraData = extraData;\n\t\treturn this;\n\t}\n\n\tpublic long getCreateTime() {\n\t\treturn createTime;\n\t}\n\n\tpublic AccessTokenModel setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"AccessTokenModel{\" +\n\t\t\t\t\"accessToken='\" + accessToken +\n\t\t\t\t\", refreshToken='\" + refreshToken +\n\t\t\t\t\", expiresTime=\" + expiresTime +\n\t\t\t\t\", refreshExpiresTime=\" + refreshExpiresTime +\n\t\t\t\t\", clientId='\" + clientId +\n\t\t\t\t\", loginId=\" + loginId +\n\t\t\t\t\", scopes=\" + scopes +\n\t\t\t\t\", tokenType='\" + tokenType +\n\t\t\t\t\", grantType='\" + grantType +\n\t\t\t\t\", extraData=\" + extraData +\n\t\t\t\t\", createTime=\" + createTime +\n\t\t\t\t'}';\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/ClientTokenModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Model: Client-Token\n *\n * @author click33\n * @since 1.23.0\n */\npublic class ClientTokenModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * Client-Token 值\n\t */\n\tpublic String clientToken;\n\t\n\t/**\n\t * Client-Token 到期时间 \n\t */\n\tpublic long expiresTime;\n\n\t/**\n\t * 应用id \n\t */\n\tpublic String clientId;\n\n\t/**\n\t * 授权范围\n\t */\n\tpublic List<String> scopes;\n\n\t/**\n\t * Token 类型\n\t */\n\tpublic String tokenType;\n\n\t/**\n\t * 授权类型\n\t */\n\tpublic String grantType;\n\n\t/**\n\t * 扩展数据\n\t */\n\tpublic Map<String, Object> extraData;\n\n\t/**\n\t * 创建时间，13位时间戳\n\t */\n\tpublic long createTime;\n\n\tpublic ClientTokenModel(){\n\t\tthis.createTime = System.currentTimeMillis();\n\t}\n\n\t/**\n\t * 构建一个 ClientTokenModel\n\t * @param clientToken clientToken\n\t * @param clientId 应用id\n\t * @param scopes 请求授权范围\n\t */\n\tpublic ClientTokenModel(String clientToken, String clientId, List<String> scopes) {\n\t\tthis();\n\t\tthis.clientToken = clientToken;\n\t\tthis.clientId = clientId;\n\t\tthis.scopes = scopes;\n\t}\n\n\t// 额外追加方法\n\n\t/**\n\t * 获取：此 Client-Token 的剩余有效期（秒）\n\t * @return /\n\t */\n\tpublic long getExpiresIn() {\n\t\tlong s = (expiresTime - System.currentTimeMillis()) / 1000;\n\t\treturn s < 1 ? -2 : s;\n\t}\n\n\n\t// get set\n\n\tpublic String getClientToken() {\n\t\treturn clientToken;\n\t}\n\n\tpublic ClientTokenModel setClientToken(String clientToken) {\n\t\tthis.clientToken = clientToken;\n\t\treturn this;\n\t}\n\n\tpublic long getExpiresTime() {\n\t\treturn expiresTime;\n\t}\n\n\tpublic ClientTokenModel setExpiresTime(long expiresTime) {\n\t\tthis.expiresTime = expiresTime;\n\t\treturn this;\n\t}\n\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\tpublic ClientTokenModel setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\tpublic List<String> getScopes() {\n\t\treturn scopes;\n\t}\n\n\tpublic ClientTokenModel setScopes(List<String> scopes) {\n\t\tthis.scopes = scopes;\n\t\treturn this;\n\t}\n\n\tpublic String getTokenType() {\n\t\treturn tokenType;\n\t}\n\n\tpublic ClientTokenModel setTokenType(String tokenType) {\n\t\tthis.tokenType = tokenType;\n\t\treturn this;\n\t}\n\n\tpublic String getGrantType() {\n\t\treturn grantType;\n\t}\n\n\tpublic ClientTokenModel setGrantType(String grantType) {\n\t\tthis.grantType = grantType;\n\t\treturn this;\n\t}\n\n\tpublic Map<String, Object> getExtraData() {\n\t\treturn extraData;\n\t}\n\n\tpublic ClientTokenModel setExtraData(Map<String, Object> extraData) {\n\t\tthis.extraData = extraData;\n\t\treturn this;\n\t}\n\n\tpublic long getCreateTime() {\n\t\treturn createTime;\n\t}\n\n\tpublic ClientTokenModel setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"ClientTokenModel{\" +\n\t\t\t\t\"clientToken='\" + clientToken +\n\t\t\t\t\", expiresTime=\" + expiresTime +\n\t\t\t\t\", clientId='\" + clientId +\n\t\t\t\t\", scopes=\" + scopes +\n\t\t\t\t\", tokenType=\" + tokenType +\n\t\t\t\t\", grantType=\" + grantType +\n\t\t\t\t\", extraData=\" + extraData +\n\t\t\t\t\", createTime=\" + createTime +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/CodeModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * Model: 授权码\n *\n * @author click33\n * @since 1.23.0\n */\npublic class CodeModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/** \n\t * 授权码 \n\t */\n\tpublic String code;\n\t\n\t/**\n\t * 应用id \n\t */\n\tpublic String clientId;\n\t\n\t/**\n\t * 授权范围\n\t */\n\tpublic List<String> scopes;\n\n\t/**\n\t * 对应账号id \n\t */\n\tpublic Object loginId;\n\n\t/**\n\t * 重定向的地址 \n\t */\n\tpublic String redirectUri;\n\n\t/**\n\t * 随机数\n\t */\n\tpublic String nonce;\n\n\t/**\n\t * 创建时间，13位时间戳\n\t */\n\tpublic long createTime;\n\t\n\t/**\n\t * 构建一个 \n\t */\n\tpublic CodeModel() {\n\t\tthis.createTime = System.currentTimeMillis();\n\t}\n\n\t/**\n\t * 构建一个 \n\t * @param code 授权码 \n\t * @param clientId 应用id \n\t * @param scopes 请求授权范围\n\t * @param loginId 对应的账号id \n\t * @param redirectUri 重定向地址\n\t * @param nonce 随机数\n\t */\n\tpublic CodeModel(String code, String clientId, List<String> scopes, Object loginId, String redirectUri, String nonce) {\n\t\tthis();\n\t\tthis.code = code;\n\t\tthis.clientId = clientId;\n\t\tthis.scopes = scopes;\n\t\tthis.loginId = loginId;\n\t\tthis.redirectUri = redirectUri;\n\t\tthis.nonce = nonce;\n\t}\n\n\tpublic String getCode() {\n\t\treturn code;\n\t}\n\n\tpublic CodeModel setCode(String code) {\n\t\tthis.code = code;\n\t\treturn this;\n\t}\n\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\tpublic CodeModel setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\tpublic List<String> getScopes() {\n\t\treturn scopes;\n\t}\n\n\tpublic CodeModel setScopes(List<String> scopes) {\n\t\tthis.scopes = scopes;\n\t\treturn this;\n\t}\n\n\tpublic Object getLoginId() {\n\t\treturn loginId;\n\t}\n\n\tpublic CodeModel setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\tpublic String getRedirectUri() {\n\t\treturn redirectUri;\n\t}\n\n\tpublic CodeModel setRedirectUri(String redirectUri) {\n\t\tthis.redirectUri = redirectUri;\n\t\treturn this;\n\t}\n\n\tpublic String getNonce() {\n\t\treturn nonce;\n\t}\n\n\tpublic CodeModel setNonce(String nonce) {\n\t\tthis.nonce = nonce;\n\t\treturn this;\n\t}\n\n\tpublic long getCreateTime() {\n\t\treturn createTime;\n\t}\n\n\tpublic CodeModel setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"CodeModel{\" +\n\t\t\t\t\"code='\" + code + '\\'' +\n\t\t\t\t\", clientId='\" + clientId + '\\'' +\n\t\t\t\t\", scopes=\" + scopes +\n\t\t\t\t\", loginId=\" + loginId +\n\t\t\t\t\", redirectUri='\" + redirectUri + '\\'' +\n\t\t\t\t\", nonce='\" + nonce + '\\'' +\n\t\t\t\t\", createTime=\" + createTime +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/RefreshTokenModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Model: Refresh-Token\n *\n * @author click33\n * @since 1.23.0\n */\npublic class RefreshTokenModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n \n\t/**\n\t * Refresh-Token 值\n\t */\n\tpublic String refreshToken;\n\n\t/**\n\t * Refresh-Token 到期时间 \n\t */\n\tpublic long expiresTime;\n\t\n\t/**\n\t * 应用id \n\t */\n\tpublic String clientId;\n\n\t/**\n\t * 对应账号id \n\t */\n\tpublic Object loginId;\n\n\t/**\n\t * 授权范围\n\t */\n\tpublic List<String> scopes;\n\n\t/**\n\t * 扩展数据\n\t */\n\tpublic Map<String, Object> extraData;\n\n\t/**\n\t * 创建时间，13位时间戳\n\t */\n\tpublic long createTime;\n\n\tpublic RefreshTokenModel() {\n\t\tthis.createTime = System.currentTimeMillis();\n\t}\n\n\n\t// 额外追加方法\n\n\t/**\n\t * 获取：此 Refresh-Token 的剩余有效期（秒）\n\t * @return /\n\t */\n\tpublic long getExpiresIn() {\n\t\tlong s = (expiresTime - System.currentTimeMillis()) / 1000;\n\t\treturn s < 1 ? -2 : s;\n\t}\n\n\n\t// get set\n\n\tpublic String getRefreshToken() {\n\t\treturn refreshToken;\n\t}\n\n\tpublic RefreshTokenModel setRefreshToken(String refreshToken) {\n\t\tthis.refreshToken = refreshToken;\n\t\treturn this;\n\t}\n\n\tpublic long getExpiresTime() {\n\t\treturn expiresTime;\n\t}\n\n\tpublic RefreshTokenModel setExpiresTime(long expiresTime) {\n\t\tthis.expiresTime = expiresTime;\n\t\treturn this;\n\t}\n\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\tpublic RefreshTokenModel setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\tpublic List<String> getScopes() {\n\t\treturn scopes;\n\t}\n\n\tpublic RefreshTokenModel setScopes(List<String> scopes) {\n\t\tthis.scopes = scopes;\n\t\treturn this;\n\t}\n\n\tpublic Object getLoginId() {\n\t\treturn loginId;\n\t}\n\n\tpublic RefreshTokenModel setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\tpublic Map<String, Object> getExtraData() {\n\t\treturn extraData;\n\t}\n\n\tpublic RefreshTokenModel setExtraData(Map<String, Object> extraData) {\n\t\tthis.extraData = extraData;\n\t\treturn this;\n\t}\n\n\tpublic long getCreateTime() {\n\t\treturn createTime;\n\t}\n\n\tpublic RefreshTokenModel setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"RefreshTokenModel [\" +\n\t\t\t\t\"refreshToken=\" + refreshToken +\n\t\t\t\t\", expiresTime=\" + expiresTime +\n\t\t\t\t\", clientId=\" + clientId +\n\t\t\t\t\", loginId=\" + loginId +\n\t\t\t\t\", scopes=\" + scopes +\n\t\t\t\t\", extraData=\" + extraData +\n\t\t\t\t\", createTime=\" + createTime +\n\t\t\t\t\"]\";\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model.loader;\n\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * Client 应用信息 Model\n *\n * @author click33\n * @since 1.23.0\n */\npublic class SaClientModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * 应用id \n\t */\n\tpublic String clientId;\n\t\n\t/**\n\t * 应用秘钥 \n\t */\n\tpublic String clientSecret;\n\n\t/**\n\t * 应用签约的所有权限\n\t */\n\tpublic List<String> contractScopes = new ArrayList<>();\n\t\n\t/**\n\t * 应用允许授权的所有 redirect_uri\n\t */\n\tpublic List<String> allowRedirectUris = new ArrayList<>();\n\n\t/**\n\t * 应用允许的所有 grant_type\n\t */\n\tpublic List<String> allowGrantTypes = new ArrayList<>();\n\n\t/**\n\t * 主体id\n\t */\n\tpublic String subjectId;\n\n\t/** 此应用 Access-Token 保存的时间(单位秒)  [默认取全局配置] */\n\tpublic long accessTokenTimeout;\n\n\t/** 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置] */\n\tpublic long refreshTokenTimeout;\n\n\t/** 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置] */\n\tpublic long clientTokenTimeout;\n\n\t/** 此应用单个用户最多同时存在的 Access-Token 数量 */\n\tpublic int maxAccessTokenCount;\n\n\t/** 此应用单个用户最多同时存在的 Refresh-Token 数量 */\n\tpublic int maxRefreshTokenCount;\n\n\t/** 此应用最多同时存在的 Client-Token 数量 */\n\tpublic int maxClientTokenCount;\n\n\t/** 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时，产生一个新的 Refresh-Token [默认取全局配置] */\n\tpublic Boolean isNewRefresh;\n\n\t/** 是否允许此应用自动确认授权（高危配置，禁止向不被信任的第三方开启此选项） */\n\tpublic Boolean isAutoConfirm = false;\n\n\t\n\tpublic SaClientModel() {\n\t\tSaOAuth2Strategy.instance.setSaClientModelDefaultFields.run(this);\n\t}\n\tpublic SaClientModel(String clientId, String clientSecret, List<String> contractScopes, List<String> allowRedirectUris) {\n\t\tthis();\n\t\tthis.clientId = clientId;\n\t\tthis.clientSecret = clientSecret;\n\t\tthis.contractScopes = contractScopes;\n\t\tthis.allowRedirectUris = allowRedirectUris;\n\t}\n\n\n\t// 追加方法\n\n\t/**\n\t * @param scopes 添加应用签约的所有权限\n\t * @return 对象自身\n\t */\n\tpublic SaClientModel addContractScopes(String... scopes) {\n\t\tif(this.contractScopes == null) {\n\t\t\tthis.contractScopes = new ArrayList<>();\n\t\t}\n\t\tthis.contractScopes.addAll(Arrays.asList(scopes));\n\t\treturn this;\n\t}\n\n\t/**\n\t * @param redirectUris 添加应用允许授权的所有 redirect_uri\n\t * @return 对象自身\n\t */\n\tpublic SaClientModel addAllowRedirectUris(String... redirectUris) {\n\t\tif(this.allowRedirectUris == null) {\n\t\t\tthis.allowRedirectUris = new ArrayList<>();\n\t\t}\n\t\tthis.allowRedirectUris.addAll(Arrays.asList(redirectUris));\n\t\treturn this;\n\t}\n\n\t/**\n\t * @param grantTypes 应用允许的所有 grant_type\n\t * @return 对象自身\n\t */\n\tpublic SaClientModel addAllowGrantTypes(String... grantTypes) {\n\t\tif(this.allowGrantTypes == null) {\n\t\t\tthis.allowGrantTypes = new ArrayList<>();\n\t\t}\n\t\tthis.allowGrantTypes.addAll(Arrays.asList(grantTypes));\n\t\treturn this;\n\t}\n\n\n\t// get set\n\n\t/**\n\t * @return 应用id\n\t */\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\t/**\n\t * @param clientId 应用id \n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 应用秘钥 \n\t */\n\tpublic String getClientSecret() {\n\t\treturn clientSecret;\n\t}\n\n\t/**\n\t * @param clientSecret 应用秘钥 \n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setClientSecret(String clientSecret) {\n\t\tthis.clientSecret = clientSecret;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 应用签约的所有权限\n\t */\n\tpublic List<String> getContractScopes() {\n\t\treturn contractScopes;\n\t}\n\n\t/**\n\t * @param contractScopes 应用签约的所有权限\n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setContractScopes(List<String> contractScopes) {\n\t\tthis.contractScopes = contractScopes;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 应用允许授权的所有 redirect_uri\n\t */\n\tpublic List<String> getAllowRedirectUris() {\n\t\treturn allowRedirectUris;\n\t}\n\n\t/**\n\t * @param allowRedirectUris 应用允许授权的所有 redirect_uri\n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setAllowRedirectUris(List<String> allowRedirectUris) {\n\t\tthis.allowRedirectUris = allowRedirectUris;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 应用允许的所有 grant_type\n\t */\n\tpublic List<String> getAllowGrantTypes() {\n\t\treturn allowGrantTypes;\n\t}\n\n\t/**\n\t * 应用允许的所有 grant_type\n\t * @param allowGrantTypes /\n\t * @return /\n\t */\n\tpublic SaClientModel setAllowGrantTypes(List<String> allowGrantTypes) {\n\t\tthis.allowGrantTypes = allowGrantTypes;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 主体id\n\t *\n\t * @return subjectId 主体id\n\t */\n\tpublic String getSubjectId() {\n\t\treturn this.subjectId;\n\t}\n\n\t/**\n\t * 设置 主体id\n\t *\n\t * @param subjectId 主体id\n\t */\n\tpublic SaClientModel setSubjectId(String subjectId) {\n\t\tthis.subjectId = subjectId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时，产生一个新的 Refresh-Token [默认取全局配置]\n\t */\n\tpublic Boolean getIsNewRefresh() {\n\t\treturn isNewRefresh;\n\t}\n\t\n\t/**\n\t * @param isNewRefresh 此应用 是否在每次 Refresh-Token 刷新 Access-Token 时，产生一个新的 Refresh-Token [默认取全局配置]\n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setIsNewRefresh(Boolean isNewRefresh) {\n\t\tthis.isNewRefresh = isNewRefresh;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * @return 此应用 Access-Token 保存的时间(单位秒)  [默认取全局配置]\n\t */\n\tpublic long getAccessTokenTimeout() {\n\t\treturn accessTokenTimeout;\n\t}\n\t\n\t/**\n\t * @param accessTokenTimeout 此应用 Access-Token 保存的时间(单位秒)  [默认取全局配置]\n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setAccessTokenTimeout(long accessTokenTimeout) {\n\t\tthis.accessTokenTimeout = accessTokenTimeout;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * @return 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置]\n\t */\n\tpublic long getRefreshTokenTimeout() {\n\t\treturn refreshTokenTimeout;\n\t}\n\t\n\t/**\n\t * @param refreshTokenTimeout 此应用 Refresh-Token 保存的时间(单位秒) [默认取全局配置]\n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setRefreshTokenTimeout(long refreshTokenTimeout) {\n\t\tthis.refreshTokenTimeout = refreshTokenTimeout;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * @return 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置]\n\t */\n\tpublic long getClientTokenTimeout() {\n\t\treturn clientTokenTimeout;\n\t}\n\t\n\t/**\n\t * @param clientTokenTimeout 此应用 Client-Token 保存的时间(单位秒) [默认取全局配置]\n\t * @return 对象自身 \n\t */\n\tpublic SaClientModel setClientTokenTimeout(long clientTokenTimeout) {\n\t\tthis.clientTokenTimeout = clientTokenTimeout;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 是否允许此应用自动确认授权（高危配置，禁止向不被信任的第三方开启此选项）\n\t *\n\t * @return /\n\t */\n\tpublic Boolean getIsAutoConfirm() {\n\t\treturn this.isAutoConfirm;\n\t}\n\n\t/**\n\t * 设置 是否允许此应用自动确认授权（高危配置，禁止向不被信任的第三方开启此选项）\n\t *\n\t * @param isAutoConfirm /\n\t * @return 对象自身\n\t */\n\tpublic SaClientModel setIsAutoConfirm(Boolean isAutoConfirm) {\n\t\tthis.isAutoConfirm = isAutoConfirm;\n\t\treturn this;\n\t}\n\n\t/**\n\t *  此应用单个用户最多同时存在的 Access-Token 数量\n\t * @return /\n\t */\n\tpublic int getMaxAccessTokenCount() {\n\t\treturn maxAccessTokenCount;\n\t}\n\n\t/**\n\t * 设置  此应用单个用户最多同时存在的 Access-Token 数量\n\t * @param maxAccessTokenCount /\n\t * @return 对象自身\n\t */\n\tpublic SaClientModel setMaxAccessTokenCount(int maxAccessTokenCount) {\n\t\tthis.maxAccessTokenCount = maxAccessTokenCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 此应用单个用户最多同时存在的 Refresh-Token 数量\n\t * @return /\n\t */\n\tpublic int getMaxRefreshTokenCount() {\n\t\treturn maxRefreshTokenCount;\n\t}\n\n\t/**\n\t * 此应用单个用户最多同时存在的 Refresh-Token 数量\n\t * @param maxRefreshTokenCount /\n\t * @return 对象自身\n\t */\n\tpublic SaClientModel setMaxRefreshTokenCount(int maxRefreshTokenCount) {\n\t\tthis.maxRefreshTokenCount = maxRefreshTokenCount;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 此应用单个用户最多同时存在的 Client-Token 数量\n\t * @return /\n\t */\n\tpublic int getMaxClientTokenCount() {\n\t\treturn maxClientTokenCount;\n\t}\n\n\t/**\n\t * 此应用单个用户最多同时存在的 Client-Token 数量\n\t * @param maxClientTokenCount /\n\t * @return 对象自身\n\t */\n\tpublic SaClientModel setMaxClientTokenCount(int maxClientTokenCount) {\n\t\tthis.maxClientTokenCount = maxClientTokenCount;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaClientModel{\" +\n\t\t\t\t\"clientId='\" + clientId + '\\'' +\n\t\t\t\t\", clientSecret='\" + clientSecret + '\\'' +\n\t\t\t\t\", contractScopes=\" + contractScopes +\n\t\t\t\t\", allowRedirectUris=\" + allowRedirectUris +\n\t\t\t\t\", allowGrantTypes=\" + allowGrantTypes +\n\t\t\t\t\", subjectId=\" + subjectId +\n\t\t\t\t\", isNewRefresh=\" + isNewRefresh +\n\t\t\t\t\", accessTokenTimeout=\" + accessTokenTimeout +\n\t\t\t\t\", refreshTokenTimeout=\" + refreshTokenTimeout +\n\t\t\t\t\", clientTokenTimeout=\" + clientTokenTimeout +\n\t\t\t\t\", isAutoConfirm=\" + isAutoConfirm +\n\t\t\t\t\", maxAccessTokenCount=\" + maxAccessTokenCount +\n\t\t\t\t\", refreshTokenTimeout=\" + refreshTokenTimeout +\n\t\t\t\t\", maxClientTokenCount=\" + maxClientTokenCount +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model.oidc;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n/**\n * OIDC IdToken Model\n *\n * <br/> 参考：\n * <br/> <a href=\"https://openid.net/specs/openid-connect-core-1_0.html#IDToken\">IDToken</a>\n * <br/> <a href=\"https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\">StandardClaims</a>\n *\n * @author click33\n * @since 1.23.0\n */\npublic class IdTokenModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * 必填：发行者标识符，例如：https://server.example.com\n\t */\n\tpublic String iss;\n\n\t/**\n\t * 必填：用户标识符，用户id，例如：10001\n\t */\n\tpublic Object sub;\n\n\t/**\n\t * 必填：客户端标识符，clientId，例如：s6BhdRkqt3\n\t */\n\tpublic String aud;\n\n\t/**\n\t * 必填：令牌到期时间，10位时间戳，例如：1723341795\n\t */\n\tpublic long exp;\n\n\t/**\n\t * 必填：签发此令牌的时间，10位时间戳，例如：1723339995\n\t */\n\tpublic long iat;\n\n\t/**\n\t * 用户认证时间，10位时间戳，例如：1723339988\n\t */\n\tpublic long authTime;\n\n\t/**\n\t * 随机数，客户端提供，防止重放攻击，例如：e9a3f4d9\n\t */\n\tpublic String nonce;\n\n\t/**\n\t * 身份验证上下文类引用\n\t */\n\tpublic String acr;\n\n\t/**\n\t * 身份验证方法参考\n\t */\n\tpublic String amr;\n\n\t/**\n\t * 授权方 - 签发 ID 令牌的一方，如果存在，它必须包含此方的 OAuth 2.0 客户端 ID。\n\t */\n\tpublic String azp;\n\n\t/**\n\t * 扩展数据\n\t */\n\tpublic Map<String, Object> extraData;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/ClientIdAndSecretModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model.request;\n\nimport java.io.Serializable;\n\n/**\n * Client 的 id 和 secret\n *\n * @author click33\n * @since 1.39.0\n */\npublic class ClientIdAndSecretModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * 应用id\n\t */\n\tpublic String clientId;\n\n\t/**\n\t * 应用秘钥\n\t */\n\tpublic String clientSecret;\n\n\tpublic ClientIdAndSecretModel() {\n\t}\n\tpublic ClientIdAndSecretModel(String clientId, String clientSecret) {\n\t\tsuper();\n\t\tthis.clientId = clientId;\n\t\tthis.clientSecret = clientSecret;\n\t}\n\n\t/**\n\t * @return 应用id\n\t */\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\t/**\n\t * @param clientId 应用id \n\t * @return 对象自身 \n\t */\n\tpublic ClientIdAndSecretModel setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return 应用秘钥 \n\t */\n\tpublic String getClientSecret() {\n\t\treturn clientSecret;\n\t}\n\n\t/**\n\t * @param clientSecret 应用秘钥 \n\t * @return 对象自身 \n\t */\n\tpublic ClientIdAndSecretModel setClientSecret(String clientSecret) {\n\t\tthis.clientSecret = clientSecret;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"ClientIdAndSecretModel{\" +\n\t\t\t\t\"clientId='\" + clientId + '\\'' +\n\t\t\t\t\", clientSecret='\" + clientSecret + '\\'' +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/request/RequestAuthModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.model.request;\n\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * 请求授权参数的 Model\n *\n * @author click33\n * @since 1.23.0\n */\npublic class RequestAuthModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * 应用id \n\t */\n\tpublic String clientId;\n\t \n\t/**\n\t * 授权范围\n\t */\n\tpublic List<String> scopes;\n\t\n\t/**\n\t * 对应的账号id \n\t */\n\tpublic Object loginId;\n\t\n\t/**\n\t * 待重定向URL\n\t */\n\tpublic String redirectUri; \n\t\n\t/**\n\t * 授权类型, 非必填 \n\t */\n\tpublic String responseType;\n\n\t/**\n\t * 状态标识, 可为null \n\t */\n\tpublic String state;\n\n\t/**\n\t * 随机数\n\t */\n\tpublic String nonce;\n\n\t\n\t/**\n\t * @return clientId\n\t */\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\t/**\n\t * @param clientId 要设置的 clientId\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return scopes\n\t */\n\tpublic List<String> getScopes() {\n\t\treturn scopes;\n\t}\n\n\t/**\n\t * @param scopes 要设置的 scopes\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel setScopes(List<String> scopes) {\n\t\tthis.scopes = scopes;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return loginId\n\t */\n\tpublic Object getLoginId() {\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * @param loginId 要设置的 loginId\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return redirectUri\n\t */\n\tpublic String getRedirectUri() {\n\t\treturn redirectUri;\n\t}\n\n\t/**\n\t * @param redirectUri 要设置的 redirectUri\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel setRedirectUri(String redirectUri) {\n\t\tthis.redirectUri = redirectUri;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return responseType\n\t */\n\tpublic String getResponseType() {\n\t\treturn responseType;\n\t}\n\n\t/**\n\t * @param responseType 要设置的 responseType\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel setResponseType(String responseType) {\n\t\tthis.responseType = responseType;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * @return state\n\t */\n\tpublic String getState() {\n\t\treturn state;\n\t}\n\n\t/**\n\t * @param state 要设置的 state\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel setState(String state) {\n\t\tthis.state = state;\n\t\treturn this;\n\t}\n\n\t/**\n\t * @return nonce\n\t */\n\tpublic String getNonce() {\n\t\treturn nonce;\n\t}\n\n\t/**\n\t * @param nonce 要设置的随机数\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel setNonce(String nonce) {\n\t\tthis.nonce = nonce;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 数据自检\n\t * @return 对象自身\n\t */\n\tpublic RequestAuthModel checkModel() {\n\t\tif(SaFoxUtil.isEmpty(clientId)) {\n\t\t\tthrow new SaOAuth2Exception(\"client_id 不可为空\").setCode(SaOAuth2ErrorCode.CODE_30101);\n\t\t}\n\t\tif(SaFoxUtil.isEmpty(scopes)) {\n\t\t\tthrow new SaOAuth2Exception(\"scope 不可为空\").setCode(SaOAuth2ErrorCode.CODE_30102);\n\t\t}\n\t\tif(SaFoxUtil.isEmpty(redirectUri)) {\n\t\t\tthrow new SaOAuth2Exception(\"redirect_uri 不可为空\").setCode(SaOAuth2ErrorCode.CODE_30103);\n\t\t}\n\t\tif(SaFoxUtil.isEmpty(String.valueOf(loginId))) {\n\t\t\tthrow new SaOAuth2Exception(\"LoginId 不可为空\").setCode(SaOAuth2ErrorCode.CODE_30104);\n\t\t}\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"RequestAuthModel{\" +\n\t\t\t\t\"clientId='\" + clientId + '\\'' +\n\t\t\t\t\", scopes=\" + scopes +\n\t\t\t\t\", loginId=\" + loginId +\n\t\t\t\t\", redirectUri='\" + redirectUri + '\\'' +\n\t\t\t\t\", responseType='\" + responseType + '\\'' +\n\t\t\t\t\", state='\" + state + '\\'' +\n\t\t\t\t\", nonce='\" + nonce + '\\'' +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolver.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.resolver;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.Map;\n\n/**\n * Sa-Token OAuth2 数据解析器，负责 Web 交互层面的数据进出：\n *  <p>1、从请求中按照指定格式读取数据</p>\n *  <p>2、构建数据输出格式</p>\n *\n * @author click33\n * @since 1.39.0\n */\npublic interface SaOAuth2DataResolver {\n\n    /**\n     * 数据读取：从请求对象中读取 ClientId、Secret\n     *\n     * @param request /\n     * @return /\n     */\n    ClientIdAndSecretModel readClientIdAndSecret(SaRequest request);\n\n    /**\n     * 数据读取：从请求对象中读取 AccessToken，获取不到返回 null\n     * <br /> 1、请求参数 access_token，2、请求头 Authorization Bearer access_token\n     *\n     * @param request /\n     * @return /\n     */\n    String readAccessToken(SaRequest request);\n\n    /**\n     * 数据读取：从请求对象中读取 ClientToken，获取不到返回 null\n     * <br /> 1、请求参数 client_token，2、请求头 Authorization Bearer client_token\n     *\n     * @param request /\n     * @return /\n     */\n    String readClientToken(SaRequest request);\n\n    /**\n     * 数据读取：从请求对象中构建 RequestAuthModel\n     * @param req SaRequest对象\n     * @param loginId 账号id\n     * @return RequestAuthModel对象\n     */\n    RequestAuthModel readRequestAuthModel(SaRequest req, Object loginId);\n\n    /**\n     * 构建返回值: 获取 token\n     * @param at token信息\n     * @return /\n     */\n    Map<String, Object> buildAccessTokenReturnValue(AccessTokenModel at);\n\n    /**\n     * 构建返回值: RefreshToken 刷新 Access-Token\n     * @param at token信息\n     * @return /\n     */\n    default Map<String, Object> buildRefreshTokenReturnValue(AccessTokenModel at) {\n        return buildAccessTokenReturnValue(at);\n    }\n\n    /**\n     * 构建返回值: 回收 Access-Token\n     * @return /\n     */\n    default Map<String, Object> buildRevokeTokenReturnValue() {\n        return SaResult.ok();\n    }\n\n    /**\n     * 构建返回值: 凭证式 模式认证 获取 token\n     * @param ct token信息\n     */\n    Map<String, Object> buildClientTokenReturnValue(ClientTokenModel ct);\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/resolver/SaOAuth2DataResolverDefaultImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.data.resolver;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Param;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.TokenType;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token OAuth2 数据解析器，负责 Web 交互层面的数据进出：\n *  <p>1、从请求中按照指定格式读取数据</p>\n *  <p>2、构建数据输出格式</p>\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2DataResolverDefaultImpl implements SaOAuth2DataResolver {\n\n    /**\n     * 数据读取：从请求对象中读取 ClientId、Secret，如果获取不到则抛出异常\n     *\n     * @param request /\n     * @return /\n     */\n    @Override\n    public ClientIdAndSecretModel readClientIdAndSecret(SaRequest request) {\n        // 优先从请求参数中获取\n        String clientId = request.getParam(SaOAuth2Consts.Param.client_id);\n        String clientSecret = request.getParam(SaOAuth2Consts.Param.client_secret);\n\n        // 此处必须 clientId 和 clientSecret 都有值才可以采用，fix pr: https://gitee.com/dromara/sa-token/pulls/346\n        if(SaFoxUtil.isNotEmpty(clientId) && SaFoxUtil.isNotEmpty(clientSecret)) {\n            return new ClientIdAndSecretModel(clientId, clientSecret);\n        }\n\n        // 如果请求参数中没有提供 client_id 参数，则尝试从 Authorization 中获取\n        String authorizationValue = SaHttpBasicUtil.getAuthorizationValue();\n        if(SaFoxUtil.isNotEmpty(authorizationValue)) {\n            String[] arr = authorizationValue.split(\":\");\n            clientId = arr[0];\n            if(arr.length > 1) {\n                clientSecret = arr[1];\n            }\n            return new ClientIdAndSecretModel(clientId, clientSecret);\n        }\n\n        // 如果只提供了 clientId 参数，也为其构建一个 ClientIdAndSecretModel 对象，clientSecret 置空\n        if(SaFoxUtil.isNotEmpty(clientId)) {\n            return new ClientIdAndSecretModel(clientId, null);\n        }\n\n        // 如果都没有提供，则抛出异常\n        throw new SaOAuth2Exception(\"请提供 client 信息\").setCode(SaOAuth2ErrorCode.CODE_30191);\n    }\n\n    /**\n     * 数据读取：从请求对象中读取 AccessToken，获取不到返回 null，获取不到返回 null\n     * <br /> 1、请求参数 access_token，2、请求头 Authorization Bearer access_token\n     */\n    @Override\n    public String readAccessToken(SaRequest request) {\n        // 优先从请求参数中获取，可以读取到的话直接返回\n        String accessToken = request.getParam(Param.access_token);\n        if(SaFoxUtil.isNotEmpty(accessToken)) {\n            return accessToken;\n        }\n\n        // 如果请求参数中没有提供 access_token 参数，则尝试从 Authorization 中获取\n        String authorizationValue = request.getHeader(Param.Authorization);\n        if(SaFoxUtil.isEmpty(authorizationValue)) {\n            return null;\n        }\n\n        // 判断前缀，裁剪\n        String prefix = TokenType.Bearer + \" \";\n        if(authorizationValue.startsWith(prefix)) {\n            return authorizationValue.substring(prefix.length());\n        }\n\n        // 前缀不符合，返回 null\n        return null;\n    }\n\n    /**\n     * 数据读取：从请求对象中读取 ClientToken，获取不到返回 null\n     * <br /> 1、请求参数 client_token，2、请求头 Authorization Bearer client_token\n     */\n    @Override\n    public String readClientToken(SaRequest request) {\n        // 优先从请求参数中获取，可以读取到的话直接返回\n        String clientToken = request.getParam(Param.client_token);\n        if(SaFoxUtil.isNotEmpty(clientToken)) {\n            return clientToken;\n        }\n\n        // 如果请求参数中没有提供 client_token 参数，则尝试从 Authorization 中获取\n        String authorizationValue = request.getHeader(Param.Authorization);\n        if(SaFoxUtil.isEmpty(authorizationValue)) {\n            return null;\n        }\n\n        // 判断前缀，裁剪\n        String prefix = TokenType.Bearer + \" \";\n        if(authorizationValue.startsWith(prefix)) {\n            return authorizationValue.substring(prefix.length());\n        }\n\n        // 前缀不符合，返回 null\n        return null;\n    }\n\n    /**\n     * 数据读取：从请求对象中构建 RequestAuthModel\n     */\n    @Override\n    public RequestAuthModel readRequestAuthModel(SaRequest req, Object loginId) {\n        RequestAuthModel ra = new RequestAuthModel();\n        ra.clientId = req.getParamNotNull(Param.client_id);\n        ra.responseType = req.getParamNotNull(Param.response_type);\n        ra.redirectUri = req.getParamNotNull(Param.redirect_uri);\n        ra.state = req.getParam(Param.state);\n        ra.nonce = req.getParam(Param.nonce);\n        ra.scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(Param.scope));\n        ra.loginId = loginId;\n        return ra;\n    }\n\n\n    /**\n     * 构建返回值: 获取 token\n     */\n    @Override\n    public Map<String, Object> buildAccessTokenReturnValue(AccessTokenModel at) {\n        Map<String, Object> map = new LinkedHashMap<>();\n        map.put(\"token_type\", at.tokenType);\n        map.put(\"access_token\", at.accessToken);\n        map.put(\"refresh_token\", at.refreshToken);\n        map.put(\"expires_in\", at.getExpiresIn());\n        map.put(\"refresh_expires_in\", at.getRefreshExpiresIn());\n        map.put(\"client_id\", at.clientId);\n        map.put(\"scope\", SaOAuth2Manager.getDataConverter().convertScopeListToString(at.scopes));\n        map.putAll(at.extraData);\n        SaResult result = SaResult.ok().setMap(map);\n        if(SaOAuth2Manager.getServerConfig().hideStatusField) {\n            result.removeDefaultFields();\n        }\n        return result;\n    }\n\n    /**\n     * 构建返回值: 凭证式 模式认证 获取 token\n     */\n    @Override\n    public Map<String, Object> buildClientTokenReturnValue(ClientTokenModel ct) {\n        Map<String, Object> map = new LinkedHashMap<>();\n        map.put(\"token_type\", ct.tokenType);\n        map.put(\"client_token\", ct.clientToken);\n        if(SaOAuth2Manager.getServerConfig().getMode4ReturnAccessToken()) {\n             map.put(\"access_token\", ct.clientToken);\n        }\n        map.put(\"expires_in\", ct.getExpiresIn());\n        map.put(\"client_id\", ct.clientId);\n        map.put(\"scope\", SaOAuth2Manager.getDataConverter().convertScopeListToString(ct.scopes));\n        map.putAll(ct.extraData);\n\n        SaResult result = SaResult.ok().setMap(map);\n        if(SaOAuth2Manager.getServerConfig().hideStatusField) {\n            result.removeDefaultFields();\n        }\n        return result;\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/error/SaOAuth2ErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.error;\n\n/**\n * 定义 sa-token-oauth2 所有异常细分状态码 \n * \n * @author click33\n * @since 1.33.0\n */\npublic interface SaOAuth2ErrorCode {\n\n\t/** client_id 不可为空 */\n\tint CODE_30101 = 30101;\n\n\t/** scope 不可为空 */\n\tint CODE_30102 = 30102;\n\n\t/** redirect_uri 不可为空 */\n\tint CODE_30103 = 30103;\n\n\t/** LoginId 不可为空 */\n\tint CODE_30104 = 30104;\n\n\t/** 无效 client_id */\n\tint CODE_30105 = 30105;\n\n\t/** 无效 access_token */\n\tint CODE_30106 = 30106;\n\n\t/** 无效 client_token */\n\tint CODE_30107 = 30107;\n\n\t/** Access-Token 不具备指定的 Scope */\n\tint CODE_30108 = 30108;\n\n\t/** Client-Token 不具备指定的 Scope */\n\tint CODE_30109 = 30109;\n\n\t/** 无效 Code 码 */\n\tint CODE_30110 = 30110;\n\n\t/** 无效 Refresh-Token */\n\tint CODE_30111 = 30111;\n\n\t/** 请求的 Scope 暂未签约 */\n\tint CODE_30112 = 30112;\n\n\t/** 无效 redirect_url */\n\tint CODE_30113 = 30113;\n\n\t/** 非法 redirect_url */\n\tint CODE_30114 = 30114;\n\t\n\t/** 无效client_secret */\n\tint CODE_30115 = 30115;\n\n\t/** redirect_uri 不一致 */\n\tint CODE_30120 = 30120;\n\n\t/** client_id 不一致 */\n\tint CODE_30122 = 30122;\n\n\t/** 无效 response_type */\n\tint CODE_30125 = 30125;\n\n\t/** 无效 grant_type */\n\tint CODE_30126 = 30126;\n\n\t/** 无效 state */\n\tint CODE_30127 = 30127;\n\n\t/** 暂未开放授权码模式 */\n\tint CODE_30131 = 30131;\n\t\n\t/** 暂未开放隐藏式模式 */\n\tint CODE_30132 = 30132;\n\t\n\t/** 暂未开放密码式模式 */\n\tint CODE_30133 = 30133;\n\t\n\t/** 暂未开放凭证式模式 */\n\tint CODE_30134 = 30134;\n\n\t/** 系统暂未开放的授权模式 */\n\tint CODE_30141 = 30141;\n\n\t/** 应用暂未开放的授权模式 */\n\tint CODE_30142 = 30142;\n\n\t/** 无效的请求 Method */\n\tint CODE_30151 = 30151;\n\n\t/** Password 模式认证失败 */\n\tint CODE_30161 = 30161;\n\n\t/** 其它异常 */\n\tint CODE_30191 = 30191;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 Access-Token 相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2AccessTokenException extends SaOAuth2Exception {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 Access-Token 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2AccessTokenException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 Access-Token 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2AccessTokenException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 Access-Token 值\n\t */\n\tpublic String accessToken;\n\n\tpublic String getAccessToken() {\n\t\treturn accessToken;\n\t}\n\n\tpublic SaOAuth2AccessTokenException setAccessToken(String accessToken) {\n\t\tthis.accessToken = accessToken;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2AccessTokenException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AccessTokenScopeException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 Access-Token Scope 相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2AccessTokenScopeException extends SaOAuth2AccessTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 Access-Token Scope 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2AccessTokenScopeException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 Access-Token Scope 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2AccessTokenScopeException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 Access-Token 值\n\t */\n\tpublic String accessToken;\n\n\t/**\n\t * 具体引起异常的 scope 值\n\t */\n\tpublic String scope;\n\n\tpublic String getAccessToken() {\n\t\treturn accessToken;\n\t}\n\n\tpublic SaOAuth2AccessTokenScopeException setAccessToken(String accessToken) {\n\t\tthis.accessToken = accessToken;\n\t\treturn this;\n\t}\n\n\tpublic String getScope() {\n\t\treturn scope;\n\t}\n\n\tpublic SaOAuth2AccessTokenScopeException setScope(String scope) {\n\t\tthis.scope = scope;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2AccessTokenScopeException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2AuthorizationCodeException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 Code 授权码相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2AuthorizationCodeException extends SaOAuth2Exception {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 Access-Token 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2AuthorizationCodeException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 Access-Token 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2AuthorizationCodeException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 code 值\n\t */\n\tpublic String authorizationCode;\n\n\tpublic String getAuthorizationCode() {\n\t\treturn authorizationCode;\n\t}\n\n\tpublic SaOAuth2AuthorizationCodeException setAuthorizationCode(String authorizationCode) {\n\t\tthis.authorizationCode = authorizationCode;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param authorizationCode 引入异常的 code 值\n\t * @param code 异常细分码\n\t */\n\tpublic static void throwBy(boolean flag, String message, String authorizationCode, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2AuthorizationCodeException(message).setAuthorizationCode(authorizationCode).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 ClientModel 相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2ClientModelException extends SaOAuth2Exception {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 ClientModel 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2ClientModelException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 ClientModel 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2ClientModelException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n     * 具体引起异常的 ClientId 值\n\t */\n\tpublic String clientId;\n\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\tpublic SaOAuth2ClientModelException setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2ClientModelException(message).setCode(code);\n\t\t}\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息\n\t * @param clientId 应用id\n\t * @param code 异常细分码\n\t */\n\tpublic static void throwBy(boolean flag, String message, String clientId, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2ClientModelException(message).setClientId(clientId).setCode(code);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientModelScopeException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 ClientModel Scope 相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2ClientModelScopeException extends SaOAuth2ClientModelException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 ClientModel Scope 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2ClientModelScopeException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 ClientModel Scope 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2ClientModelScopeException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 ClientId 值\n\t */\n\tpublic String clientId;\n\n\t/**\n\t * 具体引起异常的 scope 值\n\t */\n\tpublic String scope;\n\n\tpublic String getClientId() {\n\t\treturn clientId;\n\t}\n\n\tpublic SaOAuth2ClientModelScopeException setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t\treturn this;\n\t}\n\n\tpublic String getScope() {\n\t\treturn scope;\n\t}\n\n\tpublic SaOAuth2ClientModelScopeException setScope(String scope) {\n\t\tthis.scope = scope;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2ClientModelScopeException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 Client-Token 相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2ClientTokenException extends SaOAuth2Exception {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 Client-Token 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2ClientTokenException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 Client-Token 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2ClientTokenException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 Client-Token 值\n\t */\n\tpublic String clientToken;\n\n\tpublic String getClientToken() {\n\t\treturn clientToken;\n\t}\n\n\tpublic SaOAuth2ClientTokenException setClientToken(String clientToken) {\n\t\tthis.clientToken = clientToken;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2ClientTokenException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2ClientTokenScopeException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 Client-Token Scope 相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2ClientTokenScopeException extends SaOAuth2ClientTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 Client-Token Scope 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2ClientTokenScopeException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 Client-Token Scope 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2ClientTokenScopeException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 Client-Token 值\n\t */\n\tpublic String clientToken;\n\n\t/**\n\t * 具体引起异常的 scope 值\n\t */\n\tpublic String scope;\n\n\tpublic String getClientToken() {\n\t\treturn clientToken;\n\t}\n\n\tpublic SaOAuth2ClientTokenScopeException setClientToken(String clientToken) {\n\t\tthis.clientToken = clientToken;\n\t\treturn this;\n\t}\n\n\tpublic String getScope() {\n\t\treturn scope;\n\t}\n\n\tpublic SaOAuth2ClientTokenScopeException setScope(String scope) {\n\t\tthis.scope = scope;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2ClientTokenScopeException(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\nimport cn.dev33.satoken.exception.SaTokenException;\n\n/**\n * 一个异常：代表 OAuth2 认证流程错误\n * \n * @author click33\n * @since 1.33.0\n */\npublic class SaOAuth2Exception extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 OAuth2 认证流程错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2Exception(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 OAuth2 认证流程错误\n\t * @param message 异常描述 \n\t */\n\tpublic SaOAuth2Exception(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2Exception(message).setCode(code);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2RefreshTokenException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.exception;\n\n/**\n * 一个异常：代表 Refresh-Token 相关错误\n * \n * @author click33\n * @since 1.39.0\n */\npublic class SaOAuth2RefreshTokenException extends SaOAuth2Exception {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\n\t/**\n\t * 一个异常：代表 Refresh-Token 相关错误\n\t * @param cause 根异常原因\n\t */\n\tpublic SaOAuth2RefreshTokenException(Throwable cause) {\n\t\tsuper(cause);\n\t}\n\n\t/**\n\t * 一个异常：代表 Refresh-Token 相关错误\n\t * @param message 异常描述\n\t */\n\tpublic SaOAuth2RefreshTokenException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 具体引起异常的 Refresh-Token 值\n\t */\n\tpublic String refreshToken;\n\n\tpublic String getRefreshToken() {\n\t\treturn refreshToken;\n\t}\n\n\tpublic SaOAuth2RefreshTokenException setRefreshToken(String refreshToken) {\n\t\tthis.refreshToken = refreshToken;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息 \n\t * @param code 异常细分码 \n\t */\n\tpublic static void throwBy(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2RefreshTokenException(message).setCode(code);\n\t\t}\n\t}\n\n\t/**\n\t * 如果 flag==true，则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息\n\t * @param refreshToken refreshToken\n\t * @param code 异常细分码\n\t */\n\tpublic static void throwBy(boolean flag, String message, String refreshToken, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaOAuth2RefreshTokenException(message).setRefreshToken(refreshToken).setCode(code);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2ConfirmViewFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function;\n\n\nimport java.util.List;\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：OAuth-Server端 确认授权时返回的View\n *\n * <p>  参数：无  </p>\n * <p>  返回：view 视图  </p>\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2ConfirmViewFunction extends BiFunction<String, List<String>, Object> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2DoLoginHandleFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function;\n\n\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：登录函数\n *\n * <p>  参数：name, pwd  </p>\n * <p>  返回：认证返回结果  </p>\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2DoLoginHandleFunction extends BiFunction<String, String, Object> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/SaOAuth2NotLoginViewFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function;\n\n\nimport java.util.function.Supplier;\n\n/**\n * 函数式接口：OAuth-Server端 未登录时返回的View\n *\n * <p>  参数：clientId, scope  </p>\n * <p>  返回：view 视图  </p>\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2NotLoginViewFunction extends Supplier<Object> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateAccessTokenValueFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function.strategy;\n\nimport java.util.List;\n\n/**\n * 函数式接口：创建一个 AccessToken value\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2CreateAccessTokenValueFunction {\n\n    /**\n     * 创建一个 AccessToken value\n     * @param clientId 应用id\n     * @param loginId 账号id\n     * @param scopes 权限\n     * @return AccessToken value\n     */\n    String execute(String clientId, Object loginId, List<String> scopes);\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateClientTokenValueFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function.strategy;\n\nimport java.util.List;\n\n/**\n * 函数式接口：创建一个 ClientToken value\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2CreateClientTokenValueFunction {\n\n    /**\n     * 创建一个 ClientToken value\n     * @param clientId 应用id\n     * @param scopes 权限\n     * @return ClientToken value\n     */\n    String execute(String clientId, List<String> scopes);\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateCodeValueFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function.strategy;\n\nimport java.util.List;\n\n/**\n * 函数式接口：创建一个 code value\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2CreateCodeValueFunction {\n\n    /**\n     * 创建一个 code value\n     * @param clientId 应用id\n     * @param loginId 账号id\n     * @param scopes 权限\n     * @return code value\n     */\n    String execute(String clientId, Object loginId, List<String> scopes);\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2CreateRefreshTokenValueFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function.strategy;\n\nimport java.util.List;\n\n/**\n * 函数式接口：创建一个 RefreshToken value\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2CreateRefreshTokenValueFunction {\n\n    /**\n     * 创建一个 RefreshToken value\n     * @param clientId 应用id\n     * @param loginId 账号id\n     * @param scopes 权限\n     * @return RefreshToken value\n     */\n    String execute(String clientId, Object loginId, List<String> scopes);\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2GrantTypeAuthFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function.strategy;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\n\nimport java.util.function.Function;\n\n/**\n * 函数式接口：GrantType 认证\n *\n * <p>  参数：SaRequest、grant_type </p>\n * <p>  返回：处理结果  </p>\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2GrantTypeAuthFunction extends Function<SaRequest, AccessTokenModel> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkAccessTokenFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function.strategy;\n\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\n\nimport java.util.function.Consumer;\n\n/**\n * 函数式接口：AccessTokenModel 加工\n *\n * <p>  参数：AccessTokenModel </p>\n * <p>  返回：无  </p>\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2ScopeWorkAccessTokenFunction extends Consumer<AccessTokenModel> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/function/strategy/SaOAuth2ScopeWorkClientTokenFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.function.strategy;\n\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\n\nimport java.util.function.Consumer;\n\n/**\n * 函数式接口：ClientTokenModel 加工\n *\n * <p>  参数：ClientTokenModel </p>\n * <p>  返回：无  </p>\n *\n * @author click33\n * @since 1.39.0\n */\n@FunctionalInterface\npublic interface SaOAuth2ScopeWorkClientTokenFunction extends Consumer<ClientTokenModel> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/AuthorizationCodeGrantTypeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.granttype.handler;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.GrantType;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel;\n\nimport java.util.List;\n\n/**\n * authorization_code grant_type 处理器\n *\n * @author click33\n * @since 1.39.0\n */\npublic class AuthorizationCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {\n\n    @Override\n    public String getHandlerGrantType() {\n        return GrantType.authorization_code;\n    }\n\n    @Override\n    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {\n        ClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req);\n        String clientSecret = clientIdAndSecret.clientSecret;\n        String code = req.getParamNotNull(SaOAuth2Consts.Param.code);\n        String redirectUri = req.getParam(SaOAuth2Consts.Param.redirect_uri);\n\n        // 校验参数\n        SaOAuth2Manager.getTemplate().checkGainTokenParam(code, clientId, clientSecret, redirectUri);\n\n        // 构建 Access-Token、返回\n        return SaOAuth2Manager.getDataGenerate().generateAccessToken(code);\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.granttype.handler;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.GrantType;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.oauth2.granttype.handler.model.PasswordAuthResult;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.stp.StpUtil;\n\nimport java.util.List;\n\n/**\n * password grant_type 处理器\n *\n * @author click33\n * @since 1.39.0\n */\npublic class PasswordGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {\n\n    @Override\n    public String getHandlerGrantType() {\n        return GrantType.password;\n    }\n\n    @Override\n    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {\n\n        // 1、获取请求参数\n        String username = req.getParamNotNull(SaOAuth2Consts.Param.username);\n        String password = req.getParamNotNull(SaOAuth2Consts.Param.password);\n\n        // 2、调用API 开始登录，如果没能成功登录，则直接退出\n        PasswordAuthResult passwordAuthResult = loginByUsernamePassword(username, password);\n        Object loginId = passwordAuthResult.getLoginId();\n        if(loginId == null) {\n            throw new SaOAuth2Exception(\"登录失败\").setCode(SaOAuth2ErrorCode.CODE_30161);\n        }\n\n        // 3、构建 ra 对象\n        RequestAuthModel ra = new RequestAuthModel();\n        ra.clientId = clientId;\n        ra.loginId = loginId;\n        ra.scopes = scopes;\n\n        // 4、生成 Access-Token\n        AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = GrantType.password);\n        return at;\n    }\n\n    /**\n     * 根据 username、password 进行登录，如果登录失败请直接抛出异常或返回 loginId = null\n     * @param username /\n     * @param password /\n     */\n    public PasswordAuthResult loginByUsernamePassword(String username, String password) {\n        System.err.println(\"警告信息：当前 password 认证模式，使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle)，仅供开发测试\");\n        System.err.println(\"正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法\");\n        SaOAuth2Strategy.instance.doLoginHandle.apply(username, password);\n        Object loginId = StpUtil.getLoginIdDefaultNull();\n        return new PasswordAuthResult(loginId);\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/RefreshTokenGrantTypeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.granttype.handler;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.GrantType;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.RefreshTokenModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2ClientModelException;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2RefreshTokenException;\n\nimport java.util.List;\n\n/**\n * refresh_token grant_type 处理器\n *\n * @author click33\n * @since 1.39.0\n */\npublic class RefreshTokenGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {\n\n    @Override\n    public String getHandlerGrantType() {\n        return GrantType.refresh_token;\n    }\n\n    @Override\n    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {\n        // 获取参数\n        String refreshToken = req.getParamNotNull(SaOAuth2Consts.Param.refresh_token);\n\n        // 校验：Refresh-Token 是否存在\n        RefreshTokenModel rt = SaOAuth2Manager.getDao().getRefreshToken(refreshToken);\n        SaOAuth2RefreshTokenException.throwBy(rt == null, \"无效refresh_token: \" + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111);\n\n        // 校验：Refresh-Token 代表的 ClientId 与提供的 ClientId 是否一致\n        SaOAuth2ClientModelException.throwBy( ! rt.clientId.equals(clientId), \"无效client_id: \" + clientId, clientId, SaOAuth2ErrorCode.CODE_30122);\n\n        // 获取新 Access-Token\n        AccessTokenModel accessTokenModel = SaOAuth2Manager.getDataGenerate().refreshAccessToken(refreshToken);\n        return accessTokenModel;\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/SaOAuth2GrantTypeHandlerInterface.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.granttype.handler;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\n\nimport java.util.List;\n\n/**\n * 所有 OAuth2 GrantType 处理器的父接口，如果要自定义 GrantType 处理器，必须实现此接口\n *\n * @author click33\n * @since 1.39.0\n */\npublic interface SaOAuth2GrantTypeHandlerInterface {\n\n    /**\n     * 获取所要处理的 GrantType\n     *\n     * @return /\n     */\n    String getHandlerGrantType();\n\n    /**\n     * 获取 AccessTokenModel 对象\n     *\n     * @param req /\n     * @return /\n     */\n    AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes);\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/model/PasswordAuthResult.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.granttype.handler.model;\n\nimport java.io.Serializable;\n\n/**\n * Model: Password Grant_Type 认证结果\n *\n * @author click33\n * @since 1.43.0\n */\npublic class PasswordAuthResult implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * 对应账号id\n\t */\n\tpublic Object loginId;\n\n\t/**\n\t * 构建一个\n\t */\n\tpublic PasswordAuthResult() {\n\n\t}\n\t/**\n\t * 构建一个\n\t * @param loginId 对应的账号id\n\t */\n\tpublic PasswordAuthResult(Object loginId) {\n\t\tthis();\n\t\tthis.loginId = loginId;\n\t}\n\n\t/**\n\t * 获取 对应账号id\n\t * @return /\n\t */\n\tpublic Object getLoginId() {\n\t\treturn loginId;\n\t}\n\n\t/**\n\t * 设置 对应账号id\n\t * @param loginId 对应账号id\n\t * @return 对象自身\n\t */\n\tpublic PasswordAuthResult setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"PasswordAuthResult{\" +\n\t\t\t\t\", loginId=\" + loginId +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.processor;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.consts.GrantType;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Api;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.Param;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts.ResponseType;\nimport cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel;\nimport cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Template;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.List;\n\n/**\n * Sa-Token OAuth2 请求处理器\n *\n * @author click33\n * @since 1.23.0\n */\npublic class SaOAuth2ServerProcessor {\n\n\t/**\n\t * 全局默认实例\n\t */\n\tpublic static SaOAuth2ServerProcessor instance = new SaOAuth2ServerProcessor();\n\n\t/**\n\t * 处理 Server 端请求， 路由分发\n\t * @return 处理结果\n\t */\n\tpublic Object dister() {\n\n\t\t// 获取变量\n\t\tSaRequest req = SaHolder.getRequest();\n\n\t\t// ------------------ 路由分发 ------------------\n\n\t\t// 模式一：Code授权码 || 模式二：隐藏式\n\t\tif(req.isPath(Api.authorize)) {\n\t\t\treturn authorize();\n\t\t}\n\n\t\t// Code 换 Access-Token || 模式三：密码式\n\t\tif(req.isPath(Api.token)) {\n\t\t\treturn token();\n\t\t}\n\n\t\t// Refresh-Token 刷新 Access-Token\n\t\tif(req.isPath(Api.refresh)) {\n\t\t\treturn refresh();\n\t\t}\n\n\t\t// 回收 Access-Token\n\t\tif(req.isPath(Api.revoke)) {\n\t\t\treturn revoke();\n\t\t}\n\n\t\t// doLogin 登录接口\n\t\tif(req.isPath(Api.doLogin)) {\n\t\t\treturn doLogin();\n\t\t}\n\n\t\t// doConfirm 确认授权接口\n\t\tif(req.isPath(Api.doConfirm)) {\n\t\t\treturn doConfirm();\n\t\t}\n\n\t\t// 模式四：凭证式\n\t\tif(req.isPath(Api.client_token)) {\n\t\t\treturn clientToken();\n\t\t}\n\n\t\t// 默认返回\n\t\treturn SaOAuth2Consts.NOT_HANDLE;\n\t}\n\n\t/**\n\t * 模式一：Code授权码 / 模式二：隐藏式\n\t * @return 处理结果\n\t */\n\tpublic Object authorize() {\n\n\t\t// 获取变量\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaResponse res = SaHolder.getResponse();\n\t\tSaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();\n\t\tSaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();\n\t\tSaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n\t\tString responseType = req.getParamNotNull(Param.response_type);\n\n\t\t// 1、先判断是否开启了指定的授权模式\n\t\tcheckAuthorizeResponseType(responseType, req, cfg);\n\n\t\t// 2、如果尚未登录, 则先去登录\n\t\tObject loginId = SaOAuth2Manager.getStpLogic().getLoginIdDefaultNull();\n\t\tif( loginId == null) {\n\t\t\treturn SaOAuth2Strategy.instance.notLoginView.get();\n\t\t}\n\n\t\t// 3、构建请求 Model\n\t\tRequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);\n\n\t\t// 4、开发者自定义的授权前置检查\n\t\tSaOAuth2Strategy.instance.userAuthorizeClientCheck.run(ra.loginId, ra.clientId);\n\n\t\t// 5、校验：重定向域名是否合法\n\t\toauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri);\n\n\t\t// 6、校验：此次申请的Scope，该Client是否已经签约\n\t\toauth2Template.checkContractScope(ra.clientId, ra.scopes);\n\n\t\t// 7、判断：如果此次申请的Scope，该用户尚未授权，则转到授权页面\n\t\tboolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);\n\t\tif(isNeedCarefulConfirm) {\n\t\t\tSaClientModel cm = oauth2Template.checkClientModel(ra.clientId);\n\t\t\tif( ! cm.getIsAutoConfirm()) {\n\t\t\t\treturn SaOAuth2Strategy.instance.confirmView.apply(ra.clientId, ra.scopes);\n\t\t\t}\n\t\t}\n\n\t\t// 8、判断授权类型，重定向到不同地址\n\t\t// \t\t如果是 授权码式，则：开始重定向授权，下放code\n\t\tif(ResponseType.code.equals(ra.responseType)) {\n\t\t\tCodeModel codeModel = dataGenerate.generateCode(ra);\n\t\t\tString redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);\n\t\t\treturn res.redirect(redirectUri);\n\t\t}\n\t\t\n\t\t// \t\t如果是 隐藏式，则：开始重定向授权，下放 token\n\t\tif(ResponseType.token.equals(ra.responseType)) {\n\t\t\tAccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null);\n\t\t\tString redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);\n\t\t\treturn res.redirect(redirectUri);\n\t\t}\n\n\t\t// 默认返回\n\t\tthrow new SaOAuth2Exception(\"无效 response_type: \" + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);\n\t}\n\n\t/**\n\t * Code 换 Access-Token / 模式三：密码式 / 自定义 grant_type\n\t * @return 处理结果\n\t */\n\tpublic Object token() {\n\t\tAccessTokenModel accessTokenModel = SaOAuth2Strategy.instance.grantTypeAuth.apply(SaHolder.getRequest());\n\t\treturn SaOAuth2Manager.getDataResolver().buildAccessTokenReturnValue(accessTokenModel);\n\t}\n\n\t/**\n\t * Refresh-Token 刷新 Access-Token\n\t * @return 处理结果\n\t */\n\tpublic Object refresh() {\n\t\tSaRequest req = SaHolder.getRequest();\n\n\t\t// 校验 grant_type 必须为 refresh_token\n\t\tString grantType = req.getParamNotNull(Param.grant_type);\n\t\tSaOAuth2Exception.throwBy(!grantType.equals(GrantType.refresh_token), \"无效 grant_type：\" + grantType, SaOAuth2ErrorCode.CODE_30126);\n\n\t\t// 刷新 Access-Token\n\t\tAccessTokenModel accessTokenModel = SaOAuth2Strategy.instance.grantTypeAuth.apply(req);\n\t\treturn SaOAuth2Manager.getDataResolver().buildRefreshTokenReturnValue(accessTokenModel);\n\t}\n\n\t/**\n\t * 回收 Access-Token\n\t * @return 处理结果\n\t */\n\tpublic Object revoke() {\n\t\t// 获取变量\n\t\tSaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n\t\tSaRequest req = SaHolder.getRequest();\n\n\t\t// 获取参数\n\t\tClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req);\n\t\tString clientId = clientIdAndSecret.clientId;\n\t\tString clientSecret = clientIdAndSecret.clientSecret;\n\t\tString accessToken = req.getParamNotNull(Param.access_token);\n\n\t\t// 如果 Access-Token 不存在，直接返回\n\t\tif(oauth2Template.getAccessToken(accessToken) == null) {\n\t\t\treturn SaResult.ok(\"access_token 不存在：\" + accessToken);\n\t\t}\n\n\t\t// 校验参数\n\t\toauth2Template.checkAccessTokenParam(clientId, clientSecret, accessToken);\n\n\t\t// 回收 Access-Token\n\t\toauth2Template.revokeAccessToken(accessToken);\n\n\t\t// 返回\n\t\treturn SaOAuth2Manager.getDataResolver().buildRevokeTokenReturnValue();\n\t}\n\n\t/**\n\t * doLogin 登录接口\n\t * @return 处理结果\n\t */\n\tpublic Object doLogin() {\n\t\tSaRequest req = SaHolder.getRequest();\n\t\treturn SaOAuth2Strategy.instance.doLoginHandle.apply(req.getParam(Param.name), req.getParam(Param.pwd));\n\t}\n\n\t/**\n\t * doConfirm 确认授权接口\n\t * @return 处理结果\n\t */\n\tpublic Object doConfirm() {\n\t\t// 获取变量\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tString clientId = req.getParamNotNull(Param.client_id);\n\t\tObject loginId = SaOAuth2Manager.getStpLogic().getLoginId();\n\t\tString scope = req.getParamNotNull(Param.scope);\n\t\tList<String> scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(scope);\n\t\tSaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();\n\t\tSaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n\n\t\t// 此请求只允许 POST 方式\n\t\tif(!req.isMethod(SaHttpMethod.POST)) {\n\t\t\tthrow new SaOAuth2Exception(\"无效请求方式：\" + req.getMethod()).setCode(SaOAuth2ErrorCode.CODE_30151);\n\t\t}\n\n\t\t// 确认授权\n\t\toauth2Template.saveGrantScope(clientId, loginId, scopes);\n\n\t\t// 判断所需的返回结果模式\n\t\tboolean buildRedirectUri = req.isParam(Param.build_redirect_uri, \"true\");\n\n\t\t// -------- 情况1：只返回确认结果即可\n\t\tif( ! buildRedirectUri ) {\n\t\t\toauth2Template.saveGrantScope(clientId, loginId, scopes);\n\t\t\treturn SaResult.ok();\n\t\t}\n\n\t\t// -------- 情况2：需要返回最终的 redirect_uri 地址\n\n\t\t// 构建请求 Model\n\t\tRequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);\n\n\t\t// 判断授权类型，构建不同的重定向地址\n\t\t// \t\t如果是 授权码式，则：开始重定向授权，下放code\n\t\tif(ResponseType.code.equals(ra.responseType)) {\n\t\t\tCodeModel codeModel = dataGenerate.generateCode(ra);\n\t\t\tString redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);\n\t\t\treturn SaResult.ok().set(Param.redirect_uri, redirectUri);\n\t\t}\n\n\t\t// \t\t如果是 隐藏式，则：开始重定向授权，下放 token\n\t\tif(ResponseType.token.equals(ra.responseType)) {\n\t\t\tAccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null);\n\t\t\tString redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);\n\t\t\treturn SaResult.ok().set(Param.redirect_uri, redirectUri);\n\t\t}\n\n\t\t// 默认返回\n\t\tthrow new SaOAuth2Exception(\"无效response_type: \" + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);\n\t}\n\n\t/**\n\t * 模式四：凭证式\n\t * @return 处理结果\n\t */\n\tpublic Object clientToken() {\n\t\t// 获取变量\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();\n\t\tSaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n\n\t\tString grantType = req.getParamNotNull(Param.grant_type);\n\t\tif(!grantType.equals(GrantType.client_credentials)) {\n\t\t\tthrow new SaOAuth2Exception(\"无效 grant_type：\" + grantType).setCode(SaOAuth2ErrorCode.CODE_30126);\n\t\t}\n\t\tif(!cfg.enableClientCredentials) {\n\t\t\tthrowErrorSystemNotEnableModel();\n\t\t}\n\t\tif(!currClientModel().getAllowGrantTypes().contains(GrantType.client_credentials)) {\n\t\t\tthrowErrorClientNotEnableModel();\n\t\t}\n\n\t\t// 获取参数\n\t\tClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req);\n\t\tString clientId = clientIdAndSecret.clientId;\n\t\tString clientSecret = clientIdAndSecret.clientSecret;\n\t\tList<String> scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(Param.scope));\n\n\t\t// 校验 ClientScope\n\t\toauth2Template.checkContractScope(clientId, scopes);\n\n\t\t// 校验 ClientSecret\n\t\toauth2Template.checkClientSecret(clientId, clientSecret);\n\n\t\t// 生成\n\t\tClientTokenModel ct = SaOAuth2Manager.getDataGenerate().generateClientToken(clientId, scopes);\n\n\t\t// 返回\n\t\treturn SaOAuth2Manager.getDataResolver().buildClientTokenReturnValue(ct);\n\t}\n\n\n\t// ----------- 代码块封装 --------------\n\n\t/**\n\t * 根据当前请求提交的 client_id 参数获取 SaClientModel 对象 \n\t * @return / \n\t */\n\tpublic SaClientModel currClientModel() {\n\t\tSaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n\t\tClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(SaHolder.getRequest());\n\t\treturn oauth2Template.checkClientModel(clientIdAndSecret.clientId);\n\t}\n\n\t/**\n\t * 校验当前请求中提交的 clientId 和 clientSecret 是否正确，如果正确则返回 SaClientModel 对象\n\t *\n\t * @return /\n\t */\n\tpublic SaClientModel checkCurrClientSecret() {\n\t\tSaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();\n\t\tClientIdAndSecretModel clientIdAndSecret = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(SaHolder.getRequest());\n\t\treturn oauth2Template.checkClientSecret(clientIdAndSecret.clientId, clientIdAndSecret.clientSecret);\n\t}\n\n\t/**\n\t * 校验 authorize 路由的 ResponseType 参数\n\t */\n\tpublic void checkAuthorizeResponseType(String responseType, SaRequest req, SaOAuth2ServerConfig cfg) {\n\t\t// 模式一：Code授权码\n\t\tif(responseType.equals(ResponseType.code)) {\n\t\t\tif(!cfg.enableAuthorizationCode) {\n\t\t\t\tthrowErrorSystemNotEnableModel();\n\t\t\t}\n\t\t\tif(!currClientModel().getAllowGrantTypes().contains(GrantType.authorization_code)) {\n\t\t\t\tthrowErrorClientNotEnableModel();\n\t\t\t}\n\t\t}\n\t\t// 模式二：隐藏式\n\t\telse if(responseType.equals(ResponseType.token)) {\n\t\t\tif(!cfg.enableImplicit) {\n\t\t\t\tthrowErrorSystemNotEnableModel();\n\t\t\t}\n\t\t\tif(!currClientModel().getAllowGrantTypes().contains(GrantType.implicit)) {\n\t\t\t\tthrowErrorClientNotEnableModel();\n\t\t\t}\n\t\t}\n\t\t// 其它\n\t\telse {\n\t\t\tthrow new SaOAuth2Exception(\"无效 response_type: \" + responseType).setCode(SaOAuth2ErrorCode.CODE_30125);\n\t\t}\n\t}\n\n\t/**\n\t * 系统未开放此授权模式时抛出异常\n\t */\n\tpublic void throwErrorSystemNotEnableModel() {\n\t\tthrow new SaOAuth2Exception(\"系统暂未开放此授权模式\").setCode(SaOAuth2ErrorCode.CODE_30141);\n\t}\n\n\t/**\n\t * 应用未开放此授权模式时抛出异常\n\t */\n\tpublic void throwErrorClientNotEnableModel() {\n\t\tthrow new SaOAuth2Exception(\"应用暂未开放此授权模式\").setCode(SaOAuth2ErrorCode.CODE_30142);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/CommonScope.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.scope;\n\n/**\n * OAuth2 常见 Scope 定义\n *\n * @author click33\n * @since 1.39.0\n */\npublic final class CommonScope {\n\n    private CommonScope() {\n    }\n\n    /**\n     * 获取 openid\n     */\n    public static final String OPENID = \"openid\";\n\n    /**\n     * 获取 unionid\n     */\n    public static final String UNIONID = \"unionid\";\n\n    /**\n     * 获取 userid\n     */\n    public static final String USERID = \"userid\";\n\n    /**\n     * 获取 id_token\n     */\n    public static final String OIDC = \"oidc\";\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.scope.handler;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.jwt.SaJwtUtil;\nimport cn.dev33.satoken.jwt.error.SaJwtErrorCode;\nimport cn.dev33.satoken.jwt.exception.SaJwtException;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.oidc.IdTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.oauth2.scope.CommonScope;\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * id_token 权限处理器：在 AccessToken 扩展参数中追加 id_token 字段\n *\n * @author click33\n * @since 1.39.0\n */\npublic class OidcScopeHandler implements SaOAuth2ScopeHandlerInterface {\n\n    public String getHandlerScope() {\n        return CommonScope.OIDC;\n    }\n\n    @Override\n    public void workAccessToken(AccessTokenModel at) {\n        SaRequest req = SaHolder.getRequest();\n        ClientIdAndSecretModel client = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req);\n\n        // 基础参数\n        IdTokenModel idToken = new IdTokenModel();\n        idToken.iss = getIss();\n        idToken.sub = at.loginId;\n        idToken.aud = client.clientId;\n        idToken.iat = System.currentTimeMillis() / 1000;\n        idToken.exp = idToken.iat + SaOAuth2Manager.getServerConfig().getOidc().getIdTokenTimeout();\n        idToken.authTime = SaOAuth2Manager.getStpLogic().getSessionByLoginId(at.loginId).getCreateTime() / 1000;\n        idToken.nonce = getNonce();\n        idToken.acr = null;\n        idToken.amr = null;\n        idToken.azp = client.clientId;\n\n        // 额外参数\n        idToken.extraData = new LinkedHashMap<>();\n        idToken = workExtraData(idToken);\n\n        // 构建 jwtIdToken\n        String jwtIdToken = generateJwtIdToken(idToken);\n\n        // 放入 AccessTokenModel\n        at.extraData.put(\"id_token\", jwtIdToken);\n    }\n\n    @Override\n    public void workClientToken(ClientTokenModel ct) {\n\n    }\n\n    @Override\n    public boolean refreshAccessTokenIsWork() {\n        return true;\n    }\n\n    /**\n     * 获取 iss\n     * @return /\n     */\n    public String getIss() {\n        // 如果开发者配置了 iss，则使用开发者配置的 iss\n        String cfgIss = SaOAuth2Manager.getServerConfig().getOidc().getIss();\n        if(SaFoxUtil.isNotEmpty(cfgIss)) {\n            return cfgIss;\n        }\n        // 否则根据请求的 url 计算 iss\n        //      例如请求 url 为： http://localhost:8081/abc/xyz?name=张三\n        //      则计算的 iss 为： http://localhost:8081\n        String urlString = SaHolder.getRequest().getUrl();\n        try {\n            URL url = new URL(urlString);\n            String iss = url.getProtocol() + \"://\" + url.getHost();\n            if(url.getPort() != -1) {\n                iss += \":\" + url.getPort();\n            }\n            return iss;\n        } catch (MalformedURLException e) {\n            throw new SaOAuth2Exception(e);\n        }\n    }\n\n    /**\n     * 获取 nonce\n     * @return /\n     */\n    public String getNonce() {\n        String nonce = SaHolder.getRequest().getParam(SaOAuth2Consts.Param.nonce);\n        if(SaFoxUtil.isEmpty(nonce)) {\n            // 通过 code 查找nonce\n            // 为了避免其它 handler 可能会用到 nonce, 任由其自然过期，只取用不删除\n            nonce = SaOAuth2Manager.getDao().getNonce(SaHolder.getRequest().getParam(SaOAuth2Consts.Param.code));\n        }\n        if(SaFoxUtil.isEmpty(nonce)) {\n            nonce = SaFoxUtil.getRandomString(32);\n        }\n        SaSignManager.getSaSignTemplate().checkNonce(nonce);\n        return nonce;\n    }\n\n    /**\n     * 加工 IdTokenModel\n     * @return /\n     */\n    public IdTokenModel workExtraData(IdTokenModel idToken) {\n        // 留给开发者扩展\n        return idToken;\n    }\n\n    /**\n     * 将 IdTokenModel 转化为 Map 数据\n     * @return /\n     */\n    public Map<String, Object> convertIdTokenToMap(IdTokenModel idToken) {\n        // 基础参数\n        Map<String, Object> map = new LinkedHashMap<>();\n        map.put(\"iss\", idToken.iss);\n        map.put(\"sub\", idToken.sub);\n        map.put(\"aud\", idToken.aud);\n        map.put(\"exp\", idToken.exp);\n        map.put(\"iat\", idToken.iat);\n        map.put(\"auth_time\", idToken.authTime);\n        map.put(\"nonce\", idToken.nonce);\n        map.put(\"acr\", idToken.acr);\n        map.put(\"amr\", idToken.amr);\n        map.put(\"azp\", idToken.azp);\n\n        // 移除 null 值\n        idToken.extraData.entrySet().removeIf(entry -> entry.getValue() == null);\n\n        // 扩展参数\n        map.putAll(idToken.extraData);\n\n        // 返回\n        return map;\n    }\n\n    /**\n     * 生成 jwt 格式的 id_token\n     * @param idToken /\n     * @return /\n     */\n    public String generateJwtIdToken(IdTokenModel idToken) {\n        Map<String, Object> dataMap = convertIdTokenToMap(idToken);\n        String keyt = SaOAuth2Manager.getStpLogic().getConfigOrGlobal().getJwtSecretKey();\n        SaJwtException.throwByNull(keyt, \"请配置jwt秘钥\", SaJwtErrorCode.CODE_30205);\n        return SaJwtUtil.createToken(dataMap, keyt);\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OpenIdScopeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.scope.handler;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.scope.CommonScope;\n\n/**\n * OpenId 权限处理器：在 AccessToken 扩展参数中追加 openid 字段\n *\n * @author click33\n * @since 1.39.0\n */\npublic class OpenIdScopeHandler implements SaOAuth2ScopeHandlerInterface {\n\n    public String getHandlerScope() {\n        return CommonScope.OPENID;\n    }\n\n    @Override\n    public void workAccessToken(AccessTokenModel at) {\n        at.extraData.put(SaOAuth2Consts.ExtraField.openid, SaOAuth2Manager.getDataLoader().getOpenid(at.clientId, at.loginId));\n    }\n\n    @Override\n    public void workClientToken(ClientTokenModel ct) {\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/SaOAuth2ScopeHandlerInterface.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.scope.handler;\n\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\n\n/**\n * 所有 OAuth2 权限处理器的父接口，如果要自定义 Scope 处理器，必须实现此接口\n *\n * @author click33\n * @since 1.39.0\n */\npublic interface SaOAuth2ScopeHandlerInterface {\n\n    /**\n     * 获取所要处理的权限\n     *\n     * @return /\n     */\n    String getHandlerScope();\n\n    /**\n     * 当构建的 AccessToken 具有此权限时，所需要执行的方法\n     *\n     * @param at /\n     */\n    void workAccessToken(AccessTokenModel at);\n\n    /**\n     * 当构建的 ClientToken 具有此权限时，所需要执行的方法\n     *\n     * @param ct /\n     */\n    void workClientToken(ClientTokenModel ct);\n\n    /**\n     * 当使用 RefreshToken 刷新 AccessToken 时，是否重新执行 workAccessToken 构建方法\n     *\n     * @return /\n     */\n    default boolean refreshAccessTokenIsWork() {\n        return false;\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UnionIdScopeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.scope.handler;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.scope.CommonScope;\n\n/**\n * UnionId Scope 处理器，在返回的 AccessToken 中增加 unionid 字段\n *\n * @author click33\n * @since 1.40.0\n */\npublic class UnionIdScopeHandler implements SaOAuth2ScopeHandlerInterface {\n\n    @Override\n    public String getHandlerScope() {\n        return CommonScope.UNIONID;\n    }\n\n    @Override\n    public void workAccessToken(AccessTokenModel at) {\n        SaOAuth2DataLoader dataLoader = SaOAuth2Manager.getDataLoader();\n        SaClientModel cm = dataLoader.getClientModelNotNull(at.clientId);\n        at.extraData.put(SaOAuth2Consts.ExtraField.unionid, dataLoader.getUnionid(cm.subjectId, at.loginId));\n    }\n\n    @Override\n    public void workClientToken(ClientTokenModel ct) {\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/UserIdScopeHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.scope.handler;\n\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.scope.CommonScope;\n\n/**\n * UserId 权限处理器：在 AccessToken 扩展参数中追加 userid 字段\n *\n * @author click33\n * @since 1.39.0\n */\npublic class UserIdScopeHandler implements SaOAuth2ScopeHandlerInterface {\n\n    public String getHandlerScope() {\n        return CommonScope.USERID;\n    }\n\n    @Override\n    public void workAccessToken(AccessTokenModel at) {\n        at.extraData.put(SaOAuth2Consts.ExtraField.userid, at.loginId);\n    }\n\n    @Override\n    public void workClientToken(ClientTokenModel ct) {\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.strategy;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.fun.SaTwoParamFunction;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.consts.GrantType;\nimport cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;\nimport cn.dev33.satoken.oauth2.function.SaOAuth2ConfirmViewFunction;\nimport cn.dev33.satoken.oauth2.function.SaOAuth2DoLoginHandleFunction;\nimport cn.dev33.satoken.oauth2.function.SaOAuth2NotLoginViewFunction;\nimport cn.dev33.satoken.oauth2.function.strategy.*;\nimport cn.dev33.satoken.oauth2.granttype.handler.AuthorizationCodeGrantTypeHandler;\nimport cn.dev33.satoken.oauth2.granttype.handler.PasswordGrantTypeHandler;\nimport cn.dev33.satoken.oauth2.granttype.handler.RefreshTokenGrantTypeHandler;\nimport cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface;\nimport cn.dev33.satoken.oauth2.scope.CommonScope;\nimport cn.dev33.satoken.oauth2.scope.handler.*;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Sa-Token OAuth2 相关策略\n *\n * @author click33\n * @since 1.39.0\n */\npublic final class SaOAuth2Strategy {\n\n\tprivate SaOAuth2Strategy() {\n\t\tregisterDefaultScopeHandler();\n\t\tregisterDefaultGrantTypeHandler();\n\t}\n\n\t/**\n\t * 全局单例引用\n\t */\n\tpublic static final SaOAuth2Strategy instance = new SaOAuth2Strategy();\n\n\n\t// ------------------ 权限处理器 ------------------\n\n\t/**\n\t * Scope 权限处理器集合\n\t */\n\tpublic Map<String, SaOAuth2ScopeHandlerInterface> scopeHandlerMap = new LinkedHashMap<>();\n\n\t/**\n\t * 注册所有默认的 Scope 权限处理器\n\t */\n\tpublic void registerDefaultScopeHandler() {\n\t\tscopeHandlerMap.put(CommonScope.OPENID, new OpenIdScopeHandler());\n\t\tscopeHandlerMap.put(CommonScope.UNIONID, new UnionIdScopeHandler());\n\t\tscopeHandlerMap.put(CommonScope.USERID, new UserIdScopeHandler());\n\t\tscopeHandlerMap.put(CommonScope.OIDC, new OidcScopeHandler());\n\t}\n\n\t/**\n\t * 注册一个权限处理器\n\t */\n\tpublic void registerScopeHandler(SaOAuth2ScopeHandlerInterface handler) {\n\t\tscopeHandlerMap.put(handler.getHandlerScope(), handler);\n\t\tSaManager.getLog().info(\"自定义 SCOPE [{}] (处理器: {})\", handler.getHandlerScope(), handler.getClass().getCanonicalName());\n\t}\n\n\t/**\n\t * 移除一个权限处理器\n\t */\n\tpublic void removeScopeHandler(String scope) {\n\t\tscopeHandlerMap.remove(scope);\n\t}\n\n\t/**\n\t * 根据 scope 信息对一个 AccessTokenModel 进行加工处理\n\t */\n\tpublic SaOAuth2ScopeWorkAccessTokenFunction workAccessTokenByScope = (at) -> {\n\t\tif(at.scopes != null && !at.scopes.isEmpty()) {\n\t\t\tfor (String scope : at.scopes) {\n\t\t\t\tSaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope);\n\t\t\t\tif(handler != null) {\n\t\t\t\t\thandler.workAccessToken(at);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tSaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE);\n\t\tif(finallyWorkScopeHandler != null) {\n\t\t\tfinallyWorkScopeHandler.workAccessToken(at);\n\t\t}\n\t};\n\n\t/**\n\t * 当使用 RefreshToken 刷新 AccessToken 时，根据 scope 信息对一个 AccessTokenModel 进行加工处理\n\t */\n\tpublic SaOAuth2ScopeWorkAccessTokenFunction refreshAccessTokenWorkByScope = (at) -> {\n\t\tif(at.scopes != null && !at.scopes.isEmpty()) {\n\t\t\tfor (String scope : at.scopes) {\n\t\t\t\tSaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope);\n\t\t\t\tif(handler != null && handler.refreshAccessTokenIsWork()) {\n\t\t\t\t\thandler.workAccessToken(at);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tSaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE);\n\t\tif(finallyWorkScopeHandler != null && finallyWorkScopeHandler.refreshAccessTokenIsWork()) {\n\t\t\tfinallyWorkScopeHandler.workAccessToken(at);\n\t\t}\n\t};\n\n\t/**\n\t * 根据 scope 信息对一个 ClientTokenModel 进行加工处理\n\t */\n\tpublic SaOAuth2ScopeWorkClientTokenFunction workClientTokenByScope = (ct) -> {\n\t\tif(ct.scopes != null && !ct.scopes.isEmpty()) {\n\t\t\tfor (String scope : ct.scopes) {\n\t\t\t\tSaOAuth2ScopeHandlerInterface handler = scopeHandlerMap.get(scope);\n\t\t\t\tif(handler != null) {\n\t\t\t\t\thandler.workClientToken(ct);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tSaOAuth2ScopeHandlerInterface finallyWorkScopeHandler = scopeHandlerMap.get(SaOAuth2Consts._FINALLY_WORK_SCOPE);\n\t\tif(finallyWorkScopeHandler != null) {\n\t\t\tfinallyWorkScopeHandler.workClientToken(ct);\n\t\t}\n\t};\n\n\n\t// ------------------ grant_type 处理器 ------------------\n\n\t/**\n\t * grant_type 处理器集合\n\t */\n\tpublic Map<String, SaOAuth2GrantTypeHandlerInterface> grantTypeHandlerMap = new LinkedHashMap<>();\n\n\t/**\n\t * 注册所有默认的 grant_type 处理器\n\t */\n\tpublic void registerDefaultGrantTypeHandler() {\n\t\tgrantTypeHandlerMap.put(GrantType.authorization_code, new AuthorizationCodeGrantTypeHandler());\n\t\tgrantTypeHandlerMap.put(GrantType.password, new PasswordGrantTypeHandler());\n\t\tgrantTypeHandlerMap.put(GrantType.refresh_token, new RefreshTokenGrantTypeHandler());\n\t}\n\n\t/**\n\t * 注册一个 grant_type 处理器\n\t */\n\tpublic void registerGrantTypeHandler(SaOAuth2GrantTypeHandlerInterface handler) {\n\t\tgrantTypeHandlerMap.put(handler.getHandlerGrantType(), handler);\n\t\tSaManager.getLog().info(\"自定义 GRANT_TYPE [{}] (处理器: {})\", handler.getHandlerGrantType(), handler.getClass().getCanonicalName());\n\t}\n\n\t/**\n\t * 移除一个 grant_type 处理器\n\t */\n\tpublic void removeGrantTypeHandler(String scope) {\n\t\tgrantTypeHandlerMap.remove(scope);\n\t}\n\n\t/**\n\t * 根据 grantType 构造一个 AccessTokenModel\n\t */\n\tpublic SaOAuth2GrantTypeAuthFunction grantTypeAuth = (req) -> {\n\t\t// 先校验提供的 grant_type 是否有效\n\t\tString grantType = req.getParamNotNull(SaOAuth2Consts.Param.grant_type);\n\t\tSaOAuth2GrantTypeHandlerInterface grantTypeHandler = grantTypeHandlerMap.get(grantType);\n\t\tif(grantTypeHandler == null) {\n\t\t\tthrow new SaOAuth2Exception(\"无效 grant_type: \" + grantType).setCode(SaOAuth2ErrorCode.CODE_30126);\n\t\t}\n\n\t\t// 针对 authorization_code 与 password 两种特殊 grant_type，需要判断全局是否开启\n\t\tSaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig();\n\t\tif(grantType.equals(GrantType.authorization_code) && !config.getEnableAuthorizationCode() ) {\n\t\t\tthrow new SaOAuth2Exception(\"系统未开放的 grant_type: \" + grantType).setCode(SaOAuth2ErrorCode.CODE_30126);\n\t\t}\n\t\tif(grantType.equals(GrantType.password) && !config.getEnablePassword() ) {\n\t\t\tthrow new SaOAuth2Exception(\"系统未开放的 grant_type: \" + grantType).setCode(SaOAuth2ErrorCode.CODE_30126);\n\t\t}\n\n\t\t// 校验 clientSecret 和 scope\n\t\tClientIdAndSecretModel clientIdAndSecretModel = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req);\n\t\tList<String> scopes = SaOAuth2Manager.getDataConverter().convertScopeStringToList(req.getParam(SaOAuth2Consts.Param.scope));\n\t\tSaClientModel clientModel = SaOAuth2Manager.getTemplate().checkClientSecretAndScope(clientIdAndSecretModel.getClientId(), clientIdAndSecretModel.getClientSecret(), scopes);\n\n\t\t// 检测应用是否开启此 grantType\n\t\tif(!clientModel.getAllowGrantTypes().contains(grantType)) {\n\t\t\tthrow new SaOAuth2Exception(\"应用未开放的 grant_type: \" + grantType).setCode(SaOAuth2ErrorCode.CODE_30141);\n\t\t}\n\n\t\t// 调用 处理器构建 Access-Token\n\t\treturn grantTypeHandler.getAccessToken(req, clientIdAndSecretModel.getClientId(), scopes);\n\t};\n\n\n\t// ------------------ 凭证创建 ------------------\n\n\t/**\n\t * 创建一个 code value\n\t */\n\tpublic SaOAuth2CreateCodeValueFunction createCodeValue = (clientId, loginId, scopes) -> {\n\t\treturn SaFoxUtil.getRandomString(60);\n\t};\n\n\t/**\n\t * 创建一个 AccessToken value\n\t */\n\tpublic SaOAuth2CreateAccessTokenValueFunction createAccessToken = (clientId, loginId, scopes) -> {\n\t\treturn SaFoxUtil.getRandomString(60);\n\t};\n\n\t/**\n\t * 创建一个 RefreshToken value\n\t */\n\tpublic SaOAuth2CreateRefreshTokenValueFunction createRefreshToken = (clientId, loginId, scopes) -> {\n\t\treturn SaFoxUtil.getRandomString(60);\n\t};\n\n\t/**\n\t * 创建一个 ClientToken value\n\t */\n\tpublic SaOAuth2CreateClientTokenValueFunction createClientToken = (clientId, scopes) -> {\n\t\treturn SaFoxUtil.getRandomString(60);\n\t};\n\n\n\t// ------------------ 认证流程回调 ------------------\n\n\t/**\n\t * OAuth-Server端：未登录时返回的View\n\t */\n\tpublic SaOAuth2NotLoginViewFunction notLoginView = () -> \"当前会话在 OAuth-Server 认证中心尚未登录\";\n\n\t/**\n\t * OAuth-Server端：确认授权时返回的View\n\t */\n\tpublic SaOAuth2ConfirmViewFunction confirmView = (clientId, scopes) -> \"本次操作需要用户授权\";\n\n\t/**\n\t * OAuth-Server端：登录函数\n\t */\n\tpublic SaOAuth2DoLoginHandleFunction doLoginHandle = (name, pwd) -> SaResult.error();\n\n\t/**\n\t * OAuth-Server端：用户在授权指定 client 前的检查，如果检查不通过，请直接抛出异常\n\t */\n\tpublic SaTwoParamFunction<Object, String> userAuthorizeClientCheck = (loginId, clientId) -> {\n\n\t};\n\n\n\t// ------------------ 其它 ------------------\n\n\t/**\n\t * 在创建 SaClientModel 时，设置其默认字段\n\t */\n\tpublic SaParamFunction<SaClientModel> setSaClientModelDefaultFields = (clientModel) -> {\n\t\tSaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig();\n\t\tclientModel.accessTokenTimeout = config.getAccessTokenTimeout();\n\t\tclientModel.refreshTokenTimeout = config.getRefreshTokenTimeout();\n\t\tclientModel.clientTokenTimeout = config.getClientTokenTimeout();\n\t\tclientModel.maxAccessTokenCount = config.getMaxAccessTokenCount();\n\t\tclientModel.maxRefreshTokenCount = config.getMaxRefreshTokenCount();\n\t\tclientModel.maxClientTokenCount = config.getMaxClientTokenCount();\n\t\tclientModel.isNewRefresh = config.getIsNewRefresh();\n\t};\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.template;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.dao.SaOAuth2Dao;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.RefreshTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\nimport cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;\nimport cn.dev33.satoken.oauth2.exception.*;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.List;\n\n/**\n * Sa-Token-OAuth2 模块 代码实现\n *\n * @author click33\n * @since 1.23.0\n */\npublic class SaOAuth2Template {\n\n\t// ----------------- SaClientModel 相关 -----------------\n\n\t/**\n\t * 获取 ClientModel，根据 clientId\n\t *\n\t * @param clientId /\n\t * @return /\n\t */\n\tpublic SaClientModel getClientModel(String clientId) {\n\t\treturn SaOAuth2Manager.getDataLoader().getClientModel(clientId);\n\t}\n\n\t/**\n\t * 校验 clientId 信息并返回 ClientModel，如果找不到对应 Client 信息则抛出异常\n\t * @param clientId /\n\t * @return /\n\t */\n\tpublic SaClientModel checkClientModel(String clientId) {\n\t\tSaClientModel clientModel = getClientModel(clientId);\n\t\tif(clientModel == null) {\n\t\t\tthrow new SaOAuth2ClientModelException(\"无效 client_id: \" + clientId)\n\t\t\t\t\t.setClientId(clientId)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30105);\n\t\t}\n\t\treturn clientModel;\n\t}\n\n\t/**\n\t * 校验：clientId 与 clientSecret 是否正确，正确返回 SaClientModel，不正确抛出异常\n\t * @param clientId 应用id\n\t * @param clientSecret 秘钥\n\t * @return SaClientModel对象\n\t */\n\tpublic SaClientModel checkClientSecret(String clientId, String clientSecret) {\n\t\tSaClientModel cm = checkClientModel(clientId);\n\t\tif(cm.clientSecret == null || ! cm.clientSecret.equals(clientSecret)) {\n\t\t\tthrow new SaOAuth2ClientModelException(\"无效 client_secret: \" + clientSecret)\n\t\t\t\t\t.setClientId(clientId)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30115);\n\t\t}\n\t\treturn cm;\n\t}\n\n\t/**\n\t * 校验：clientId 与 clientSecret 是否正确，并且是否签约了指定 scopes\n\t * @param clientId 应用id\n\t * @param clientSecret 秘钥\n\t * @param scopes 权限\n\t * @return SaClientModel对象\n\t */\n\tpublic SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List<String> scopes) {\n\t\tSaClientModel cm = checkClientSecret(clientId, clientSecret);\n\t\tcheckContractScope(cm, scopes);\n\t\treturn cm;\n\t}\n\n\t/**\n\t * 判断：该 Client 是否签约了指定的 Scope，返回 true 或 false\n\t * @param clientId 应用id\n\t * @param scopes 权限\n\t * @return /\n\t */\n\tpublic boolean isContractScope(String clientId, List<String> scopes) {\n\t\ttry {\n\t\t\tcheckContractScope(clientId, scopes);\n\t\t\treturn true;\n\t\t} catch (SaOAuth2ClientModelException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常\n\t * @param clientId 应用id\n\t * @param scopes 权限列表\n\t * @return /\n\t */\n\tpublic SaClientModel checkContractScope(String clientId, List<String> scopes) {\n\t\treturn checkContractScope(checkClientModel(clientId), scopes);\n\t}\n\n\t/**\n\t * 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常\n\t * @param cm 应用\n\t * @param scopes 权限列表\n\t * @return /\n\t */\n\tpublic SaClientModel checkContractScope(SaClientModel cm, List<String> scopes) {\n\t\tif(SaFoxUtil.isEmptyList(scopes)) {\n\t\t\treturn cm;\n\t\t}\n\t\tfor (String scope : scopes) {\n\t\t\tif(! cm.contractScopes.contains(scope)) {\n\t\t\t\tthrow new SaOAuth2ClientModelScopeException(\"该 client 暂未签约 scope: \" + scope)\n\t\t\t\t\t\t.setClientId(cm.clientId)\n\t\t\t\t\t\t.setScope(scope)\n\t\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30112);\n\t\t\t}\n\t\t}\n\t\treturn cm;\n\t}\n\n\t// --------- redirect_uri 相关\n\n\t/**\n\t * 校验：该 Client 使用指定 url 作为回调地址，是否合法\n\t * @param clientId 应用id\n\t * @param url 指定url\n\t */\n\tpublic void checkRedirectUri(String clientId, String url) {\n\t\t// 1、是否是一个有效的url\n\t\tif( ! SaFoxUtil.isUrl(url)) {\n\t\t\tthrow new SaOAuth2ClientModelException(\"无效 redirect_url：\" + url)\n\t\t\t\t\t.setClientId(clientId)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30113);\n\t\t}\n\n\t\t// 2、截取掉?后面的部分\n\t\tint qIndex = url.indexOf(\"?\");\n\t\tif(qIndex != -1) {\n\t\t\turl = url.substring(0, qIndex);\n\t\t}\n\n\t\t// 3、不允许出现@字符\n\t\tif(url.contains(\"@\")) {\n\t\t\t//  为什么不允许出现 @ 字符呢，因为这有可能导致 redirect_url 参数绕过 AllowUrl 列表的校验\n\t\t\t//\n\t\t\t//  举个例子 SaClientModel 配置：\n\t\t\t//       allow-url=http://sa-oauth-client.com*\n\t\t\t//\n\t\t\t//  开发者原意是为了允许 sa-oauth-client.com 下的所有地址都可以下放 code\n\t\t\t//\n\t\t\t//  但是如果攻击者精心构建一个url：\n\t\t\t// \t     http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com@sa-token.cc\n\t\t\t//\n\t\t\t//  那么这个url就会绕过 allow-url 的校验，code 被下发到了第三方服务器地址：\n\t\t\t//       http://sa-token.cc/?code=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul\n\t\t\t//\n\t\t\t//  造成了 code 参数劫持\n\t\t\t//  所以此处需要禁止在 url 中出现 @ 字符\n\t\t\t//\n\t\t\t//  这么一刀切的做法，可能会导致一些特殊的正常url也无法通过校验，例如：\n\t\t\t//       http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com/@getInfo\n\t\t\t//\n\t\t\t//  但是为了安全起见，这么做还是有必要的\n\t\t\tthrow new SaOAuth2ClientModelException(\"无效 redirect_url（不允许出现@字符）：\" + url)\n\t\t\t\t\t.setClientId(clientId)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30113);\n\t\t}\n\n\t\t// 4、是否在[允许地址列表]之中\n\t\tSaClientModel clientModel = checkClientModel(clientId);\n\t\tcheckRedirectUriListNormal(clientModel.allowRedirectUris);\n\t\tif( ! SaStrategy.instance.hasElement.apply(clientModel.allowRedirectUris, url)) {\n\t\t\tthrow new SaOAuth2ClientModelException(\"非法 redirect_url: \" + url)\n\t\t\t\t\t.setClientId(clientId)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30114);\n\t\t}\n\t}\n\n\t/**\n\t * 校验配置的 allowRedirectUris 是否合规，如果不合规则抛出异常\n\t * @param redirectUriList 待校验的 allow-url 地址列表\n\t */\n\tpublic void checkRedirectUriListNormal(List<String> redirectUriList){\n\t\tcheckRedirectUriListNormalStaticMethod(redirectUriList);\n\t}\n\n\t/**\n\t * 校验配置的 allowRedirectUris 是否合规，如果不合规则抛出异常，静态方法内部实现\n\t * @param redirectUriList 待校验的 allow-url 地址列表\n\t */\n\tpublic static void checkRedirectUriListNormalStaticMethod(List<String> redirectUriList){\n\t\tfor (String url : redirectUriList) {\n\t\t\tint index = url.indexOf(\"*\");\n\t\t\t// 如果配置了 * 字符，则必须出现在最后一位，否则属于无效配置项\n\t\t\tif(index != -1 && index != url.length() - 1) {\n\t\t\t\t//  为什么不允许 * 字符出现在中间位置呢，因为这有可能导致 redirect 参数绕过 allow-url 列表的校验\n\t\t\t\t//\n\t\t\t\t//  举个例子 SaClientModel 配置：\n\t\t\t\t//      allow-url=http://*.sa-oauth-client.com/\n\t\t\t\t//\n\t\t\t\t//  开发者原意是为了允许 sa-oauth-client.com 下的所有子域名都可以下放 ticket\n\t\t\t\t//      例如：http://shop.sa-oauth-client.com/\n\t\t\t\t//\n\t\t\t\t//  但是如果攻击者精心构建一个url：\n\t\t\t\t//       http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-token.cc/a.sa-oauth-client.com/\n\t\t\t\t//\n\t\t\t\t//  那么这个 url 就会绕过 allow-url 的校验，ticket 被下发到了第三方服务器地址：\n\t\t\t\t//       http://sa-token.cc/a.sa-oauth-client.com/?code=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx\n\t\t\t\t//\n\t\t\t\t//  造成了 ticket 参数劫持\n\t\t\t\t//  所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符（出现在末尾是没有问题的）\n\t\t\t\t//\n\t\t\t\t//  这么一刀切的做法，可能会导致正常场景下的子域名url也无法通过校验，例如：\n\t\t\t\t//       http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://shop.sa-oauth2-client.com/\n\t\t\t\t//\n\t\t\t\t//  但是为了安全起见，这么做还是有必要的\n\t\t\t\tthrow new SaOAuth2Exception(\"无效的 allow-url 配置（*通配符只允许出现在最后一位）：\" + url)\n\t\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30114);\n\t\t\t}\n\t\t}\n\t}\n\n\t// --------- 授权相关\n\n\t/**\n\t * 判断：指定 loginId 是否对一个 Client 授权给了指定 Scope\n\t * @param loginId 账号id\n\t * @param clientId 应用id\n\t * @param scopes 权限\n\t * @return 是否已经授权\n\t */\n\tpublic boolean isGrantScope(Object loginId, String clientId, List<String> scopes) {\n\t\tList<String> grantScopeList = SaOAuth2Manager.getDao().getGrantScope(clientId, loginId);\n\t\treturn SaFoxUtil.list1ContainList2AllElement(grantScopeList, scopes);\n\t}\n\n\t/**\n\t * 判断：指定 loginId 在指定 Client 请求指定 Scope 时，是否需要手动确认授权\n\t * @param loginId 账号id\n\t * @param clientId 应用id\n\t * @param scopes 权限\n\t * @return 是否已经授权\n\t */\n\tpublic boolean isNeedCarefulConfirm(Object loginId, String clientId, List<String> scopes) {\n\t\t// 如果请求的权限为空，则不需要确认\n\t\tif(scopes == null || scopes.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 如果包含高级权限，则必须手动确认授权\n\t\tList<String> higherScopeList = getHigherScopeList();\n\t\tif(SaFoxUtil.list1ContainList2AnyElement(scopes, higherScopeList)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// 如果包含低级权限，则先将低级权限剔除掉\n\t\tList<String> lowerScopeList = getLowerScopeList();\n\t\tscopes = SaFoxUtil.list1RemoveByList2(scopes, lowerScopeList);\n\n\t\t// 如果剔除后的权限为空，则不需要确认\n\t\tif(scopes.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 根据近期授权记录，判断是否需要确认\n\t\treturn !isGrantScope(loginId, clientId, scopes);\n\t}\n\n\t/**\n\t * 删除：指定 loginId 针对指定 Client 的授权信息\n\t * @param loginId 账号id\n\t * @param clientId 应用id\n\t */\n\tpublic void deleteGrantScope(Object loginId, String clientId) {\n\t\tSaOAuth2Manager.getDao().deleteGrantScope(clientId, loginId);\n\t}\n\n\n\t// --------- 请求数据校验相关\n\n\t/**\n\t * 校验：使用 code 获取 token 时提供的参数校验\n\t * @param code 授权码\n\t * @param clientId 应用id\n\t * @param clientSecret 秘钥\n\t * @param redirectUri 重定向地址\n\t * @return CodeModel对象\n\t */\n\tpublic CodeModel checkGainTokenParam(String code, String clientId, String clientSecret, String redirectUri) {\n\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n\t\t// 校验：Code是否存在\n\t\tCodeModel cm = dao.getCode(code);\n\t\tSaOAuth2AuthorizationCodeException.throwBy(cm == null, \"无效 code: \" + code, code, SaOAuth2ErrorCode.CODE_30110);\n\n\t\t// 校验：ClientId是否一致\n\t\tSaOAuth2ClientModelException.throwBy( ! cm.clientId.equals(clientId), \"无效 client_id: \" + clientId, clientId, SaOAuth2ErrorCode.CODE_30105);\n\n\t\t// 校验：Secret是否正确\n\t\tString dbSecret = checkClientModel(clientId).clientSecret;\n\t\tSaOAuth2ClientModelException.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), \"无效 client_secret: \" + clientSecret, clientId, SaOAuth2ErrorCode.CODE_30115);\n\n\t\t// 如果提供了redirectUri，则校验其是否与请求Code时提供的一致\n\t\tif( ! SaFoxUtil.isEmpty(redirectUri)) {\n\t\t\tSaOAuth2ClientModelException.throwBy( ! redirectUri.equals(cm.redirectUri), \"无效 redirect_uri: \" + redirectUri, clientId, SaOAuth2ErrorCode.CODE_30120);\n\t\t}\n\n\t\t// 返回CodeModel\n\t\treturn cm;\n\t}\n\n\t/**\n\t * 校验：使用 Refresh-Token 刷新 Access-Token 时提供的参数校验\n\t * @param clientId 应用id\n\t * @param clientSecret 秘钥\n\t * @param refreshToken Refresh-Token\n\t * @return CodeModel对象\n\t */\n\tpublic RefreshTokenModel checkRefreshTokenParam(String clientId, String clientSecret, String refreshToken) {\n\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n\t\t// 校验：Refresh-Token是否存在\n\t\tRefreshTokenModel rt = dao.getRefreshToken(refreshToken);\n\t\tSaOAuth2RefreshTokenException.throwBy(rt == null, \"无效 refresh_token: \" + refreshToken, refreshToken, SaOAuth2ErrorCode.CODE_30111);\n\n\t\t// 校验：ClientId 是否一致\n\t\tSaOAuth2ClientModelException.throwBy( ! rt.clientId.equals(clientId), \"无效 client_id: \" + clientId, clientId, SaOAuth2ErrorCode.CODE_30122);\n\n\t\t// 校验：Secret 是否正确\n\t\tString dbSecret = checkClientModel(clientId).clientSecret;\n\t\tSaOAuth2ClientModelException.throwBy(dbSecret == null || ! dbSecret.equals(clientSecret), \"无效 client_secret: \" + clientSecret,\n\t\t\t\tclientId, SaOAuth2ErrorCode.CODE_30115);\n\n\t\t// 返回 Refresh-Token\n\t\treturn rt;\n\t}\n\n\t/**\n\t * 校验：Access-Token、clientId、clientSecret 三者是否匹配成功\n\t * @param clientId 应用id\n\t * @param clientSecret 秘钥\n\t * @param accessToken Access-Token\n\t * @return SaClientModel对象\n\t */\n\tpublic AccessTokenModel checkAccessTokenParam(String clientId, String clientSecret, String accessToken) {\n\t\tAccessTokenModel at = checkAccessToken(accessToken);\n\t\tSaOAuth2ClientModelException.throwBy( ! at.clientId.equals(clientId), \"无效 client_id：\" + clientId, clientId, SaOAuth2ErrorCode.CODE_30122);\n\t\tcheckClientSecret(clientId, clientSecret);\n\t\treturn at;\n\t}\n\n\n\t// ----------------- Code 相关 -----------------\n\n\t/**\n\t * 获取 CodeModel，无效的 code 会返回 null\n\t * @param code /\n\t * @return /\n\t */\n\tpublic CodeModel getCode(String code) {\n\t\treturn SaOAuth2Manager.getDao().getCode(code);\n\t}\n\n\t/**\n\t * 校验 Code，成功返回 CodeModel，失败则抛出异常\n\t * @param code /\n\t * @return /\n\t */\n\tpublic CodeModel checkCode(String code) {\n\t\tCodeModel cm = SaOAuth2Manager.getDao().getCode(code);\n\t\tif(cm == null) {\n\t\t\tthrow new SaOAuth2AuthorizationCodeException(\"无效 code: \" + code)\n\t\t\t\t\t.setAuthorizationCode(code)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30110);\n\t\t}\n\t\treturn cm;\n\t}\n\n\t/**\n\t * 获取 Code，根据索引： clientId、loginId\n\t * @param clientId /\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic String getCodeValue(String clientId, Object loginId) {\n\t\treturn SaOAuth2Manager.getDao().getCodeValue(clientId, loginId);\n\t}\n\n\n\t// ----------------- Access-Token 相关 -----------------\n\n\t/**\n\t * 获取 AccessTokenModel，无效的 AccessToken 会返回 null\n\t * @param accessToken /\n\t * @return /\n\t */\n\tpublic AccessTokenModel getAccessToken(String accessToken) {\n\t\treturn SaOAuth2Manager.getDao().getAccessToken(accessToken);\n\t}\n\n\t/**\n\t * 校验 Access-Token，成功返回 AccessTokenModel，失败则抛出异常\n\t * @param accessToken /\n\t * @return /\n\t */\n\tpublic AccessTokenModel checkAccessToken(String accessToken) {\n\t\tAccessTokenModel at = SaOAuth2Manager.getDao().getAccessToken(accessToken);\n\t\tif(at == null) {\n\t\t\tthrow new SaOAuth2AccessTokenException(\"无效 access_token: \" + accessToken)\n\t\t\t\t\t.setAccessToken(accessToken)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30106);\n\t\t}\n\t\treturn at;\n\t}\n\n\t/**\n\t * 获取 Access-Token 列表：此应用下 对 某个用户 签发的所有 Access-token\n\t *\n\t * @param clientId /\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic List<String> getAccessTokenValueList(String clientId, Object loginId) {\n\t\treturn SaOAuth2Manager.getDao().getAccessTokenValueList_FromAdjustAfter(clientId, loginId);\n\t}\n\n\t/**\n\t * 判断：指定 Access-Token 是否具有指定 Scope 列表，返回 true 或 false\n\t * @param accessToken Access-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic boolean hasAccessTokenScope(String accessToken, String... scopes) {\n\t\ttry {\n\t\t\tcheckAccessTokenScope(accessToken, scopes);\n\t\t\treturn true;\n\t\t} catch (SaOAuth2AccessTokenException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：指定 Access-Token 是否具有指定 Scope 列表，如果不具备则抛出异常\n\t * @param accessToken Access-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic void checkAccessTokenScope(String accessToken, String... scopes) {\n\t\tAccessTokenModel at = checkAccessToken(accessToken);\n\t\tif(SaFoxUtil.isEmptyArray(scopes)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (String scope : scopes) {\n\t\t\tif(! at.scopes.contains(scope)) {\n\t\t\t\tthrow new SaOAuth2AccessTokenScopeException(\"该 access_token 不具备 scope：\" + scope)\n\t\t\t\t\t\t.setAccessToken(accessToken)\n\t\t\t\t\t\t.setScope(scope)\n\t\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30108);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 获取 Access-Token 所代表的LoginId\n\t * @param accessToken Access-Token\n\t * @return LoginId\n\t */\n\tpublic Object getLoginIdByAccessToken(String accessToken) {\n\t\treturn checkAccessToken(accessToken).loginId;\n\t}\n\n\t/**\n\t * 获取 Access-Token 所代表的 clientId\n\t * @param accessToken Access-Token\n\t * @return LoginId\n\t */\n\tpublic Object getClientIdByAccessToken(String accessToken) {\n\t\treturn checkAccessToken(accessToken).clientId;\n\t}\n\n\t/**\n\t * 回收一个 Access-Token\n\t * @param accessToken Access-Token值\n\t */\n\tpublic void revokeAccessToken(String accessToken) {\n\t\tAccessTokenModel at = getAccessToken(accessToken);\n\t\tif(at == null) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 删 at、索引\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\t\tdao.deleteAccessToken(accessToken);\n\t\tdao.deleteAccessTokenIndex_BySingleData(at.clientId, at.loginId, accessToken);\n\t}\n\n\t/**\n\t * 回收全部 Access-Token：指定应用下 指定用户 的全部 Access-Token\n\t * @param clientId /\n\t * @param loginId /\n\t */\n\tpublic void revokeAccessTokenByIndex(String clientId, Object loginId) {\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n\t\tList<String> accessTokenList = getAccessTokenValueList(clientId, loginId);\n\t\tif( ! accessTokenList.isEmpty()) {\n\t\t\t// 删 AT\n\t\t\tfor (String accessToken : accessTokenList) {\n\t\t\t\tdao.deleteAccessToken(accessToken);\n\t\t\t}\n\t\t\t// 删索引\n\t\t\tdao.deleteAccessTokenIndex(clientId, loginId);\n\t\t}\n\t}\n\n\n\t// ----------------- Refresh-Token 相关 -----------------\n\n\t/**\n\t * 获取 RefreshTokenModel，无效的 RefreshToken 会返回 null\n\t * @param refreshToken /\n\t * @return /\n\t */\n\tpublic RefreshTokenModel getRefreshToken(String refreshToken) {\n\t\treturn SaOAuth2Manager.getDao().getRefreshToken(refreshToken);\n\t}\n\n\t/**\n\t * 校验 Refresh-Token，成功返回 RefreshTokenModel，失败则抛出异常\n\t * @param refreshToken /\n\t * @return /\n\t */\n\tpublic RefreshTokenModel checkRefreshToken(String refreshToken) {\n\t\tRefreshTokenModel rt = SaOAuth2Manager.getDao().getRefreshToken(refreshToken);\n\t\tif(rt == null) {\n\t\t\tthrow new SaOAuth2RefreshTokenException(\"无效 refresh_token: \" + refreshToken)\n\t\t\t\t\t.setRefreshToken(refreshToken)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30111);\n\t\t}\n\t\treturn rt;\n\t}\n\n\t/**\n\t * 获取 Refresh-Token 列表：此应用下 对 某个用户 签发的所有 Refresh-Token\n\t *\n\t * @param clientId /\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic List<String> getRefreshTokenValueList(String clientId, Object loginId) {\n\t\treturn SaOAuth2Manager.getDao().getRefreshTokenValueList_FromAdjustAfter(clientId, loginId);\n\t}\n\n\t/**\n\t * 回收一个 Refresh-Token\n\t *\n\t * @param refreshToken Refresh-Token 值\n\t */\n\tpublic void revokeRefreshToken(String refreshToken) {\n\t\tRefreshTokenModel rt = getRefreshToken(refreshToken);\n\t\tif(rt == null) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 删 rt、索引\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\t\tdao.deleteRefreshToken(refreshToken);\n\t\tdao.deleteRefreshTokenIndex_BySingleData(rt.clientId, rt.loginId, refreshToken);\n\t}\n\n\t/**\n\t * 回收全部 Refresh-Token：指定应用下 指定用户 的全部 Refresh-Token\n\t *\n\t * @param clientId /\n\t * @param loginId /\n\t */\n\tpublic void revokeRefreshTokenByIndex(String clientId, Object loginId) {\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n\t\tList<String> refreshTokenList = getRefreshTokenValueList(clientId, loginId);\n\t\tif( ! refreshTokenList.isEmpty()) {\n\t\t\t// 删 RT\n\t\t\tfor (String refreshToken : refreshTokenList) {\n\t\t\t\tdao.deleteRefreshToken(refreshToken);\n\t\t\t}\n\t\t\t// 删索引\n\t\t\tdao.deleteRefreshTokenIndex(clientId, loginId);\n\t\t}\n\t}\n\n\t/**\n\t * 根据 RefreshToken 刷新出一个 AccessToken\n\t * @param refreshToken /\n\t * @return /\n\t */\n\tpublic AccessTokenModel refreshAccessToken(String refreshToken) {\n\t\treturn SaOAuth2Manager.getDataGenerate().refreshAccessToken(refreshToken);\n\t}\n\n\n\t// ----------------- Client-Token 相关 -----------------\n\n\t/**\n\t * 获取 ClientTokenModel，无效的 ClientToken 会返回 null\n\t * @param clientToken /\n\t * @return /\n\t */\n\tpublic ClientTokenModel getClientToken(String clientToken) {\n\t\treturn SaOAuth2Manager.getDao().getClientToken(clientToken);\n\t}\n\n\t/**\n\t * 校验 Client-Token，成功返回 ClientTokenModel，失败则抛出异常\n\t * @param clientToken /\n\t * @return /\n\t */\n\tpublic ClientTokenModel checkClientToken(String clientToken) {\n\t\tClientTokenModel ct = getClientToken(clientToken);\n\t\tif(ct == null) {\n\t\t\tthrow new SaOAuth2ClientTokenException(\"无效 client_token: \" + clientToken)\n\t\t\t\t\t.setClientToken(clientToken)\n\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30107);\n\t\t}\n\t\treturn ct;\n\t}\n\n\t/**\n\t * 获取 Client-Token 列表：此应用下 对 某个用户 签发的所有 Client-token\n\t *\n\t * @param clientId /\n\t * @return /\n\t */\n\tpublic List<String> getClientTokenValueList(String clientId) {\n\t\treturn SaOAuth2Manager.getDao().getClientTokenValueList_FromAdjustAfter(clientId);\n\t}\n\n\t/**\n\t * 判断：指定 Client-Token 是否具有指定 Scope 列表，返回 true 或 false\n\t * @param clientToken Client-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic boolean hasClientTokenScope(String clientToken, String... scopes) {\n\t\ttry {\n\t\t\tcheckClientTokenScope(clientToken, scopes);\n\t\t\treturn true;\n\t\t} catch (SaOAuth2ClientTokenException e) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * 校验：指定 Client-Token 是否具有指定 Scope 列表，如果不具备则抛出异常\n\t * @param clientToken Client-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic void checkClientTokenScope(String clientToken, String... scopes) {\n\t\tClientTokenModel ct = checkClientToken(clientToken);\n\t\tif(SaFoxUtil.isEmptyArray(scopes)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (String scope : scopes) {\n\t\t\tif(! ct.scopes.contains(scope)) {\n\t\t\t\tthrow new SaOAuth2ClientTokenScopeException(\"该 client_token 不具备 scope：\" + scope)\n\t\t\t\t\t\t.setClientToken(clientToken)\n\t\t\t\t\t\t.setScope(scope)\n\t\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30109);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * 回收一个 ClientToken\n\t *\n\t * @param clientToken /\n\t */\n\tpublic void revokeClientToken(String clientToken) {\n\t\tClientTokenModel ct = getClientToken(clientToken);\n\t\tif(ct == null) {\n\t\t\treturn;\n\t\t}\n\t\t// 删 ct、删索引\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\t\tdao.deleteClientToken(clientToken);\n\t\tdao.deleteClientTokenIndex_BySingleData(ct.clientId, clientToken);\n\t}\n\n\t/**\n\t * 回收全部 Client-Token：指定应用下的全部 Client-Token\n\t * 回收 ClientToken，根据索引： clientId\n\t *\n\t * @param clientId /\n\t */\n\tpublic void revokeClientTokenByIndex(String clientId) {\n\t\tSaOAuth2Dao dao = SaOAuth2Manager.getDao();\n\n\t\tList<String> clientTokenList = getClientTokenValueList(clientId);\n\t\tif( ! clientTokenList.isEmpty()) {\n\t\t\t// 删 AT\n\t\t\tfor (String clientToken : clientTokenList) {\n\t\t\t\tdao.deleteClientToken(clientToken);\n\t\t\t}\n\t\t\t// 删索引\n\t\t\tdao.deleteClientTokenIndex(clientId);\n\t\t}\n\t}\n\n\n\t// ------------------- 请求查询\n\n\t/**\n\t * 数据读取：从当前请求对象中读取 access_token，并查询到 AccessTokenModel 信息，无效 access_token 抛出异常\n\t * <br /> 1、请求参数 access_token，2、请求头 Authorization Bearer access_token\n\t */\n\tpublic AccessTokenModel currentAccessToken() {\n\t\tString accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());\n\t\treturn checkAccessToken(accessToken);\n\t}\n\n\t/**\n\t * 数据读取：从当前请求对象中读取 client_token，并查询到 ClientTokenModel 信息，无效 client_token 抛出异常\n\t * <br /> 1、请求参数 client_token，2、请求头 Authorization Bearer client_token\n\t */\n\tpublic ClientTokenModel currentClientToken() {\n\t\tString clientToken = SaOAuth2Manager.getDataResolver().readClientToken(SaHolder.getRequest());\n\t\treturn checkClientToken(clientToken);\n\t}\n\n\n\t// ----------------- 包装其它 bean 的方法 -----------------\n\n\t/**\n\t * 持久化：用户授权记录\n\t * @param clientId 应用id\n\t * @param loginId 账号id\n\t * @param scopes 权限列表\n\t */\n\tpublic void saveGrantScope(String clientId, Object loginId, List<String> scopes) {\n\t\tSaOAuth2Manager.getDao().saveGrantScope(clientId, loginId, scopes);\n\t}\n\n\t/**\n\t * 获取高级权限列表\n\t * @return /\n\t */\n\tpublic List<String> getHigherScopeList() {\n\t\treturn SaOAuth2Manager.getDataLoader().getHigherScopeList();\n\t}\n\n\t/**\n\t * 获取低级权限列表\n\t * @return /\n\t */\n\tpublic List<String> getLowerScopeList() {\n\t\treturn SaOAuth2Manager.getDataLoader().getLowerScopeList();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.oauth2.template;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.data.model.AccessTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.ClientTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.CodeModel;\nimport cn.dev33.satoken.oauth2.data.model.RefreshTokenModel;\nimport cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;\n\nimport java.util.List;\n\n/**\n * Sa-Token OAuth2 模块 工具类\n *\n * @author click33\n * @since 1.23.0\n */\npublic class SaOAuth2Util {\n\n\t// ----------------- ClientModel 相关 -----------------\n\n\t/**\n\t * 获取 ClientModel，根据 clientId\n\t *\n\t * @param clientId /\n\t * @return /\n\t */\n\tpublic static SaClientModel getClientModel(String clientId) {\n\t\treturn SaOAuth2Manager.getTemplate().getClientModel(clientId);\n\t}\n\n\t/**\n\t * 校验 clientId 信息并返回 ClientModel，如果找不到对应 Client 信息则抛出异常\n\t * @param clientId /\n\t * @return /\n\t */\n\tpublic static SaClientModel checkClientModel(String clientId) {\n\t\treturn SaOAuth2Manager.getTemplate().checkClientModel(clientId);\n\t}\n\n\t/**\n\t * 校验：clientId 与 clientSecret 是否正确\n\t * @param clientId 应用id\n\t * @param clientSecret 秘钥\n\t * @return SaClientModel对象\n\t */\n\tpublic static SaClientModel checkClientSecret(String clientId, String clientSecret) {\n\t\treturn SaOAuth2Manager.getTemplate().checkClientSecret(clientId, clientSecret);\n\t}\n\n\t/**\n\t * 校验：clientId 与 clientSecret 是否正确，并且是否签约了指定 scopes\n\t * @param clientId 应用id\n\t * @param clientSecret 秘钥\n\t * @param scopes 权限\n\t * @return SaClientModel对象\n\t */\n\tpublic static SaClientModel checkClientSecretAndScope(String clientId, String clientSecret, List<String> scopes) {\n\t\treturn SaOAuth2Manager.getTemplate().checkClientSecretAndScope(clientId, clientSecret, scopes);\n\t}\n\n\t/**\n\t * 判断：该 Client 是否签约了指定的 Scope，返回 true 或 false\n\t * @param clientId 应用id\n\t * @param scopes 权限\n\t * @return /\n\t */\n\tpublic static boolean isContractScope(String clientId, List<String> scopes) {\n\t\treturn SaOAuth2Manager.getTemplate().isContractScope(clientId, scopes);\n\t}\n\n\t/**\n\t * 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常\n\t * @param clientId 应用id\n\t * @param scopes 权限列表\n\t * @return /\n\t */\n\tpublic static SaClientModel checkContractScope(String clientId, List<String> scopes) {\n\t\treturn SaOAuth2Manager.getTemplate().checkContractScope(clientId, scopes);\n\t}\n\n\t/**\n\t * 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常\n\t * @param cm 应用\n\t * @param scopes 权限列表\n\t * @return /\n\t */\n\tpublic static SaClientModel checkContractScope(SaClientModel cm, List<String> scopes) {\n\t\treturn SaOAuth2Manager.getTemplate().checkContractScope(cm, scopes);\n\t}\n\n\t// --------- redirect_uri 相关\n\n\t/**\n\t * 校验：该 Client 使用指定 url 作为回调地址，是否合法\n\t * @param clientId 应用id\n\t * @param url 指定url\n\t */\n\tpublic static void checkRedirectUri(String clientId, String url) {\n\t\tSaOAuth2Manager.getTemplate().checkRedirectUri(clientId, url);\n\t}\n\n\t// --------- 授权相关\n\n\t/**\n\t * 判断：指定 loginId 是否对一个 Client 授权给了指定 Scope\n\t * @param loginId 账号id\n\t * @param clientId 应用id\n\t * @param scopes 权限\n\t * @return 是否已经授权\n\t */\n\tpublic static boolean isGrantScope(Object loginId, String clientId, List<String> scopes) {\n\t\treturn SaOAuth2Manager.getTemplate().isGrantScope(loginId, clientId, scopes);\n\t}\n\n\t/**\n\t * 删除：指定 loginId 针对指定 Client 的授权信息\n\t * @param loginId 账号id\n\t * @param clientId 应用id\n\t */\n\tpublic static void deleteGrantScope(Object loginId, String clientId) {\n\t\tSaOAuth2Manager.getTemplate().deleteGrantScope(loginId, clientId);\n\t}\n\n\n\t// ----------------- Code 相关 -----------------\n\n\t/**\n\t * 获取 CodeModel，无效的 code 会返回 null\n\t * @param code /\n\t * @return /\n\t */\n\tpublic static CodeModel getCode(String code) {\n\t\treturn SaOAuth2Manager.getTemplate().getCode(code);\n\t}\n\n\t/**\n\t * 校验 Code，成功返回 CodeModel，失败则抛出异常\n\t * @param code /\n\t * @return /\n\t */\n\tpublic static CodeModel checkCode(String code) {\n\t\treturn SaOAuth2Manager.getTemplate().checkCode(code);\n\t}\n\n\t/**\n\t * 获取 Code，根据索引： clientId、loginId\n\t * @param clientId /\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic static String getCodeValue(String clientId, Object loginId) {\n\t\treturn SaOAuth2Manager.getTemplate().getCodeValue(clientId, loginId);\n\t}\n\n\n\t// ----------------- Access-Token 相关 -----------------\n\n\t/**\n\t * 获取 AccessTokenModel，无效的 AccessToken 会返回 null\n\t * @param accessToken /\n\t * @return /\n\t */\n\tpublic static AccessTokenModel getAccessToken(String accessToken) {\n\t\treturn SaOAuth2Manager.getTemplate().getAccessToken(accessToken);\n\t}\n\n\t/**\n\t * 校验 Access-Token，成功返回 AccessTokenModel，失败则抛出异常\n\t * @param accessToken /\n\t * @return /\n\t */\n\tpublic static AccessTokenModel checkAccessToken(String accessToken) {\n\t\treturn SaOAuth2Manager.getTemplate().checkAccessToken(accessToken);\n\t}\n\n\t/**\n\t * 获取 Access-Token 列表：此应用下 对 某个用户 签发的所有 Access-token\n\t * @param clientId /\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic static List<String> getAccessTokenValueList(String clientId, Object loginId) {\n\t\treturn SaOAuth2Manager.getTemplate().getAccessTokenValueList(clientId, loginId);\n\t}\n\n\t/**\n\t * 判断：指定 Access-Token 是否具有指定 Scope 列表，返回 true 或 false\n\t * @param accessToken Access-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static boolean hasAccessTokenScope(String accessToken, String... scopes) {\n\t\treturn SaOAuth2Manager.getTemplate().hasAccessTokenScope(accessToken, scopes);\n\t}\n\n\t/**\n\t * 校验：指定 Access-Token 是否具有指定 Scope 列表，如果不具备则抛出异常\n\t * @param accessToken Access-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static void checkAccessTokenScope(String accessToken, String... scopes) {\n\t\tSaOAuth2Manager.getTemplate().checkAccessTokenScope(accessToken, scopes);\n\t}\n\n\t/**\n\t * 获取 Access-Token 所代表的LoginId\n\t * @param accessToken Access-Token\n\t * @return LoginId\n\t */\n\tpublic static Object getLoginIdByAccessToken(String accessToken) {\n\t\treturn SaOAuth2Manager.getTemplate().getLoginIdByAccessToken(accessToken);\n\t}\n\n\t/**\n\t * 获取 Access-Token 所代表的 clientId\n\t * @param accessToken Access-Token\n\t * @return LoginId\n\t */\n\tpublic static Object getClientIdByAccessToken(String accessToken) {\n\t\treturn SaOAuth2Manager.getTemplate().getClientIdByAccessToken(accessToken);\n\t}\n\n\t/**\n\t * 回收一个 Access-Token\n\t * @param accessToken Access-Token值\n\t */\n\tpublic static void revokeAccessToken(String accessToken) {\n\t\tSaOAuth2Manager.getTemplate().revokeAccessToken(accessToken);\n\t}\n\n\t/**\n\t * 回收全部 Access-Token：指定应用下 指定用户 的全部 Access-Token\n\t * @param clientId /\n\t * @param loginId /\n\t */\n\tpublic static void revokeAccessTokenByIndex(String clientId, Object loginId) {\n\t\tSaOAuth2Manager.getTemplate().revokeAccessTokenByIndex(clientId, loginId);\n\t}\n\n\n\t// ----------------- Refresh-Token 相关 -----------------\n\n\t/**\n\t * 获取 RefreshTokenModel，无效的 RefreshToken 会返回 null\n\t * @param refreshToken /\n\t * @return /\n\t */\n\tpublic static RefreshTokenModel getRefreshToken(String refreshToken) {\n\t\treturn SaOAuth2Manager.getTemplate().getRefreshToken(refreshToken);\n\t}\n\n\t/**\n\t * 校验 Refresh-Token，成功返回 RefreshTokenModel，失败则抛出异常\n\t * @param refreshToken /\n\t * @return /\n\t */\n\tpublic static RefreshTokenModel checkRefreshToken(String refreshToken) {\n\t\treturn SaOAuth2Manager.getTemplate().checkRefreshToken(refreshToken);\n\t}\n\n\t/**\n\t * 获取 Refresh-Token 列表：此应用下 对 某个用户 签发的所有 Refresh-Token\n\t *\n\t * @param clientId /\n\t * @param loginId /\n\t * @return /\n\t */\n\tpublic static List<String> getRefreshTokenValueList(String clientId, Object loginId) {\n\t\treturn SaOAuth2Manager.getTemplate().getRefreshTokenValueList(clientId, loginId);\n\t}\n\n\t/**\n\t * 回收一个 Refresh-Token\n\t *\n\t * @param refreshToken Refresh-Token 值\n\t */\n\tpublic static void revokeRefreshToken(String refreshToken) {\n\t\tSaOAuth2Manager.getTemplate().revokeRefreshToken(refreshToken);\n\t}\n\n\t/**\n\t * 回收全部 Refresh-Token：指定应用下 指定用户 的全部 Refresh-Token\n\t * @param clientId /\n\t * @param loginId /\n\t */\n\tpublic static void revokeRefreshTokenByIndex(String clientId, Object loginId) {\n\t\tSaOAuth2Manager.getTemplate().revokeRefreshTokenByIndex(clientId, loginId);\n\t}\n\n\t/**\n\t * 根据 RefreshToken 刷新出一个 AccessToken\n\t * @param refreshToken /\n\t * @return /\n\t */\n\tpublic static AccessTokenModel refreshAccessToken(String refreshToken) {\n\t\treturn SaOAuth2Manager.getTemplate().refreshAccessToken(refreshToken);\n\t}\n\n\n\t// ----------------- Client-Token 相关 -----------------\n\n\t/**\n\t * 获取 ClientTokenModel，无效的 ClientToken 会返回 null\n\t * @param clientToken /\n\t * @return /\n\t */\n\tpublic static ClientTokenModel getClientToken(String clientToken) {\n\t\treturn SaOAuth2Manager.getTemplate().getClientToken(clientToken);\n\t}\n\n\t/**\n\t * 校验 Client-Token，成功返回 ClientTokenModel，失败则抛出异常\n\t * @param clientToken /\n\t * @return /\n\t */\n\tpublic static ClientTokenModel checkClientToken(String clientToken) {\n\t\treturn SaOAuth2Manager.getTemplate().checkClientToken(clientToken);\n\t}\n\n\t/**\n\t * 获取 Client-Token 列表：此应用下 对 某个用户 签发的所有 Client-token\n\t *\n\t * @param clientId /\n\t * @return /\n\t */\n\tpublic static List<String> getClientTokenValueList(String clientId) {\n\t\treturn SaOAuth2Manager.getTemplate().getClientTokenValueList(clientId);\n\t}\n\n\t/**\n\t * 判断：指定 Client-Token 是否具有指定 Scope 列表，返回 true 或 false\n\t * @param clientToken Client-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static boolean hasClientTokenScope(String clientToken, String... scopes) {\n\t\treturn SaOAuth2Manager.getTemplate().hasClientTokenScope(clientToken, scopes);\n\t}\n\n\t/**\n\t * 校验：指定 Client-Token 是否具有指定 Scope 列表，如果不具备则抛出异常\n\t * @param clientToken Client-Token\n\t * @param scopes 需要校验的权限列表\n\t */\n\tpublic static void checkClientTokenScope(String clientToken, String... scopes) {\n\t\tSaOAuth2Manager.getTemplate().checkClientTokenScope(clientToken, scopes);\n\t}\n\n\t/**\n\t * 回收一个 ClientToken\n\t *\n\t * @param clientToken /\n\t */\n\tpublic static void revokeClientToken(String clientToken) {\n\t\tSaOAuth2Manager.getTemplate().revokeClientToken(clientToken);\n\t}\n\n\t/**\n\t * 回收全部 Client-Token：指定应用下的全部 Client-Token\n\t *\n\t * @param clientId /\n\t */\n\tpublic static void revokeClientTokenByIndex(String clientId) {\n\t\tSaOAuth2Manager.getTemplate().revokeClientTokenByIndex(clientId);\n\t}\n\n\n\t// ------------------- 请求查询\n\n\t/**\n\t * 数据读取：从当前请求对象中读取 access_token，并查询到 AccessTokenModel 信息，无效 access_token 抛出异常\n\t * <br /> 1、请求参数 access_token，2、请求头 Authorization Bearer access_token\n\t */\n\tpublic static AccessTokenModel currentAccessToken() {\n\t\treturn SaOAuth2Manager.getTemplate().currentAccessToken();\n\t}\n\n\t/**\n\t * 数据读取：从当前请求对象中读取 client_token，并查询到 ClientTokenModel 信息，无效 client_token 抛出异常\n\t * <br /> 1、请求参数 client_token，2、请求头 Authorization Bearer client_token\n\t */\n\tpublic static ClientTokenModel currentClientToken() {\n\t\treturn SaOAuth2Manager.getTemplate().currentClientToken();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOAuth2.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.oauth2.annotation.handler.SaCheckAccessTokenHandler;\nimport cn.dev33.satoken.oauth2.annotation.handler.SaCheckClientIdSecretHandler;\nimport cn.dev33.satoken.oauth2.annotation.handler.SaCheckClientTokenHandler;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\n\n/**\n * SaToken 插件安装：OAuth2 相关功能\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForOAuth2 implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        // 安装 OAuth2 鉴权注解\n        SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckAccessTokenHandler());\n        SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckClientTokenHandler());\n        SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckClientIdSecretHandler());\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-oauth2/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForOAuth2"
  },
  {
    "path": "sa-token-plugin/sa-token-okhttps/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>sa-token-okhttps</name>\n    <artifactId>sa-token-okhttps</artifactId>\n    <description>sa-token integrate OkHttps</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>cn.zhxu</groupId>\n            <artifactId>okhttps</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/http/SaHttpTemplateForOkHttps.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.http;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.zhxu.okhttps.OkHttps;\n\nimport java.util.Map;\n\n/**\n * Http 请求处理器， OkHttps 版实现\n * \n * @author click33\n * @since 1.43.0\n */\npublic class SaHttpTemplateForOkHttps implements SaHttpTemplate {\n\n\t@Override\n\tpublic String get(String url) {\n\t\tSaManager.log.debug(\"发起请求，GET：{}\", url);\n\t\tString res = OkHttps.sync(url).get().getBody().toString();\n\t\tSaManager.log.debug(\"返回结果：{}\", res);\n\t\treturn res;\n\t}\n\n\t@Override\n\tpublic String postByFormData(String url, Map<String, Object> params) {\n\t\tSaManager.log.debug(\"发起请求，POST：{}\\t参数：{}\", url, params);\n\t\tString res = OkHttps.sync(url).addBodyPara(params).post().getBody().toString();\n\t\tSaManager.log.debug(\"返回结果：{}\", res);\n\t\treturn res;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-okhttps/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForOkHttps.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.http.SaHttpTemplateForOkHttps;\n\n/**\n * SaToken 插件安装：Http 请求处理器 - OkHttps 版\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaTokenPluginForOkHttps implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        // 设置 OkHttps 作为 Http 请求处理器\n        SaManager.setSaHttpTemplate(new SaHttpTemplateForOkHttps());\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-okhttps/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForOkHttps"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-quick-login</name>\n    <artifactId>sa-token-quick-login</artifactId>\n\t<description>sa-token-quick-login</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot3-starter</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n        \n\t\t<!-- 视图引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-thymeleaf</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.quick;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Import;\n\nimport cn.dev33.satoken.quick.config.SaQuickConfig;\nimport cn.dev33.satoken.quick.web.SaQuickController;\n\n/**\n * Quick-Bean 注入\n * \n * @author click33\n * @since 1.30.0\n */\n@Configuration\n@Import({ SaQuickController.class, SaQuickRegister.class})\npublic class SaQuickInject {\n\n\t/**\n\t * 注入 quick-login 配置\n\t * \n\t * @param saQuickConfig 配置对象\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaQuickConfig(SaQuickConfig saQuickConfig) {\n\t\tSaQuickManager.setConfig(saQuickConfig);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickManager.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.quick;\n\nimport cn.dev33.satoken.quick.config.SaQuickConfig;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * SaQuickManager，持有 SaQuickConfig 配置对象全局引用\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaQuickManager {\n\n\t/**\n\t * 配置文件 Bean \n\t */\n\tprivate static volatile SaQuickConfig config;\n\tpublic static void setConfig(SaQuickConfig config) {\n\t\tSaQuickManager.config = config;\n\t\t// 如果配置了 auto=true，则随机生成账号名密码\n\t\tif(config.getAuto()) {\n\t\t\tconfig.setName(SaFoxUtil.getRandomString(8));\n\t\t\tconfig.setPwd(SaFoxUtil.getRandomString(8));\n\t\t}\n\t}\n\tpublic static SaQuickConfig getConfig() {\n\t\tif (config == null) {\n\t\t\tsynchronized (SaQuickManager.class) {\n\t\t\t\tif (config == null) {\n\t\t\t\t\tsetConfig(new SaQuickConfig());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn config;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/SaQuickRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.quick;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicAccount;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.quick.config.SaQuickConfig;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.core.annotation.Order;\n\n/**\n * Quick Login 相关 Bean 注册\n * \n * @author click33\n * @since 1.30.0\n */\n@Configuration\npublic class SaQuickRegister {\n\n\t/**\n\t * 使用一个比较短的前缀，尽量提高 cmd 命令台启动时指定参数的便利性\n\t */\n\tpublic static final String CONFIG_VERSION = \"sa\";\n\n\t/**\n\t * 注册 Quick-Login 配置\n\t * \n\t * @return see note\n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = CONFIG_VERSION)\n\tpublic SaQuickConfig getSaQuickConfig() {\n\t\treturn new SaQuickConfig();\n\t}\n\n\n\t/**\n\t * 注册 Sa-Token 全局过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\t@Order(SaTokenConsts.ASSEMBLY_ORDER - 1)\n\tSaServletFilter getSaServletFilterForQuickLogin() {\n\t\treturn new SaServletFilter()\n\n\t\t\t\t// 拦截路由\n\t\t\t\t.addInclude(\"/**\")\n\n\t\t\t\t// 排除掉登录相关接口，不需要鉴权的\n\t\t\t\t.addExclude(\"/favicon.ico\", \"/saLogin\", \"/doLogin\", \"/sa-res/**\")\n\n\t\t\t\t// 认证函数: 每次请求执行\n\t\t\t\t.setAuth(obj -> {\n\t\t\t\t\tSaRouter\n\t\t\t\t\t\t\t.match(SaFoxUtil.convertStringToList(SaQuickManager.getConfig().getInclude()))\n\t\t\t\t\t\t\t.notMatch(SaFoxUtil.convertStringToList(SaQuickManager.getConfig().getExclude()))\n\t\t\t\t\t\t\t.check(r -> {\n\n\t\t\t\t\t\t\t\t// 如果已关闭认证要求，则直接通过\n\t\t\t\t\t\t\t\tif (!SaQuickManager.getConfig().getAuth()) {\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// 如果请求端提供了 Http Basic 认证信息，那么直接使用此认证信息进行登录判断\n\t\t\t\t\t\t\t\tSaHttpBasicAccount hba = SaHttpBasicUtil.getHttpBasicAccount();\n\t\t\t\t\t\t\t\tif(hba != null) {\n\t\t\t\t\t\t\t\t\tSaResult res = SaQuickManager.getConfig().doLoginHandle.apply(hba.getUsername(), hba.getPassword());\n\t\t\t\t\t\t\t\t\tif(res.getCode() != SaResult.CODE_SUCCESS) {\n\t\t\t\t\t\t\t\t\t\tSaRouter.back(res);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// 未登录时直接转发到 login.html 页面\n\t\t\t\t\t\t\t\t\tif (! StpUtil.isLogin()) {\n\t\t\t\t\t\t\t\t\t\tSaHolder.getRequest().forward(\"/saLogin\");\n\t\t\t\t\t\t\t\t\t\tSaRouter.back();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t});\n\t\t\t\t}).\n\n\t\t\t\t// 异常处理函数：每次认证函数发生异常时执行此函数\n\t\t\t\tsetError(e -> {\n\t\t\t\t\treturn e.getMessage();\n\t\t\t\t});\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/config/SaQuickConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.quick.config;\n\nimport cn.dev33.satoken.quick.function.DoLoginHandleFunction;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * sa-quick 配置类 Model\n * \n * @author click33\n * @since 1.19.0\n */\npublic class SaQuickConfig {\n\n\t/** 是否开启全局登录校验，如果为 false，则不再拦截请求出现登录页 */\n\tprivate Boolean auth = true;\n\n\t/** 用户名 */\n\tprivate String name = \"sa\";\n\n\t/** 密码 */\n\tprivate String pwd = \"123456\";\n\n\t/** 是否自动生成一个账号和密码，此配置项为 true 后，name、pwd 字段将失效 */\n\tprivate Boolean auto = false; \n\t\n\t/** 登录页面的标题 */\n\tprivate String title = \"Sa-Token 登录\";\n\n\t/** 是否显示底部版权信息 */\n\tprivate Boolean copr = true;\n\n\t/** 配置拦截的路径，逗号分隔 */\n\tprivate String include = \"/**\";\n\n\t/** 配置拦截的路径，逗号分隔 */\n\tprivate String exclude = \"\";\n\n\tpublic Boolean getAuth() {\n\t\treturn auth;\n\t}\n\n\tpublic void setAuth(Boolean auth) {\n\t\tthis.auth = auth;\n\t}\n\t\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\tpublic String getPwd() {\n\t\treturn pwd;\n\t}\n\n\tpublic void setPwd(String pwd) {\n\t\tthis.pwd = pwd;\n\t}\n\n\tpublic Boolean getAuto() {\n\t\treturn auto;\n\t}\n\n\tpublic void setAuto(Boolean auto) {\n\t\tthis.auto = auto;\n\t}\n\n\tpublic String getTitle() {\n\t\treturn title;\n\t}\n\n\tpublic void setTitle(String title) {\n\t\tthis.title = title;\n\t}\n\n\tpublic Boolean getCopr() {\n\t\treturn copr;\n\t}\n\n\tpublic void setCopr(Boolean copr) {\n\t\tthis.copr = copr;\n\t}\n\n\tpublic String getInclude() {\n\t\treturn include;\n\t}\n\n\tpublic void setInclude(String include) {\n\t\tthis.include = include;\n\t}\n\n\tpublic String getExclude() {\n\t\treturn exclude;\n\t}\n\n\tpublic void setExclude(String exclude) {\n\t\tthis.exclude = exclude;\n\t}\n\n\t/**\n\t * 登录处理函数\n\t */\n\tpublic DoLoginHandleFunction doLoginHandle = (name, pwd) -> {\n\n\t\t// 参数完整性校验\n\t\tif(SaFoxUtil.isEmpty(name) || SaFoxUtil.isEmpty(pwd)) {\n\t\t\treturn SaResult.get(500, \"请输入账号和密码\", null);\n\t\t}\n\n\t\t// 密码校验：将前端提交的 name、pwd 与配置文件中的配置项进行比对\n\t\tif(name.equals(this.getName()) && pwd.equals(this.getPwd())) {\n\t\t\tStpUtil.login(this.getName());\n\t\t\treturn SaResult.data(StpUtil.getTokenInfo());\n\t\t} else {\n\t\t\treturn SaResult.error(\"账号或密码输入错误\");\n\t\t}\n\t};\n\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SaQuickConfig{\" +\n\t\t\t\t\"auth=\" + auth +\n\t\t\t\t\", name='\" + name + '\\'' +\n\t\t\t\t\", pwd='\" + pwd + '\\'' +\n\t\t\t\t\", auto=\" + auto +\n\t\t\t\t\", title='\" + title + '\\'' +\n\t\t\t\t\", copr=\" + copr +\n\t\t\t\t\", include='\" + include + '\\'' +\n\t\t\t\t\", exclude='\" + exclude + '\\'' +\n\t\t\t\t'}';\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/function/DoLoginHandleFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.quick.function;\n\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：登录处理函数\n *\n * <p>  参数：账号、密码  </p>\n * <p>  返回：登录结果  </p>\n *\n * @author click33\n * @since 1.41.0\n */\n@FunctionalInterface\npublic interface DoLoginHandleFunction extends BiFunction<String, String, SaResult> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/java/cn/dev33/satoken/quick/web/SaQuickController.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.quick.web;\n\nimport cn.dev33.satoken.quick.SaQuickManager;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\n/**\n * 登录Controller，处理登录相关请求\n *\n * @author click33\n * @since 1.19.0\n */\n@Controller\npublic class SaQuickController {\n\n\t/**\n\t * 进入登录页面\n\t * @param model /\n\t * @return /\n\t */\n\t@GetMapping(\"/saLogin\")\n\tpublic String saLogin(Model model) {\n\t\tmodel.addAttribute(\"cfg\", SaQuickManager.getConfig());\n\t\treturn \"sa-login.html\";\n\t}\n\n\t/**\n\t * 登录接口\n\t * @param name 账号\n\t * @param pwd 密码\n\t * @return 是否登录成功 \n\t */\n\t@PostMapping(\"/doLogin\")\n\t@ResponseBody\n\tpublic SaResult doLogin(@RequestParam(\"name\") String name, @RequestParam(\"pwd\") String pwd) {\n\t\treturn SaQuickManager.getConfig().doLoginHandle.apply(name, pwd);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.quick.SaQuickInject"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncn.dev33.satoken.quick.SaQuickInject"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/layer/layer.js",
    "content": "/*! layer-v3.1.1 Web弹层组件 MIT License  http://layer.layui.com/  By 贤心 */\n ;!function(e,t){\"use strict\";var i,n,a=e.layui&&layui.define,o={getPath:function(){var e=document.currentScript?document.currentScript.src:function(){for(var e,t=document.scripts,i=t.length-1,n=i;n>0;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:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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:\"&#x4FE1;&#x606F;\",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?'<div class=\"layui-layer-title\" style=\"'+(f?r.title[1]:\"\")+'\">'+(f?r.title[0]:r.title)+\"</div>\":\"\";return r.zIndex=s,t([r.shade?'<div class=\"layui-layer-shade\" id=\"layui-layer-shade'+a+'\" times=\"'+a+'\" style=\"'+(\"z-index:\"+(s-1)+\"; \")+'\"></div>':\"\",'<div class=\"'+l[0]+(\" layui-layer-\"+o.type[r.type])+(0!=r.type&&2!=r.type||r.shade?\"\":\" layui-layer-border\")+\" \"+(r.skin||\"\")+'\" id=\"'+l[0]+a+'\" type=\"'+o.type[r.type]+'\" times=\"'+a+'\" showtime=\"'+r.time+'\" conType=\"'+(e?\"object\":\"string\")+'\" style=\"z-index: '+s+\"; width:\"+r.area[0]+\";height:\"+r.area[1]+(r.fixed?\"\":\";position:absolute;\")+'\">'+(e&&2!=r.type?\"\":u)+'<div id=\"'+(r.id||\"\")+'\" class=\"layui-layer-content'+(0==r.type&&r.icon!==-1?\" layui-layer-padding\":\"\")+(3==r.type?\" layui-layer-loading\"+r.icon:\"\")+'\">'+(0==r.type&&r.icon!==-1?'<i class=\"layui-layer-ico layui-layer-ico'+r.icon+'\"></i>':\"\")+(1==r.type&&e?\"\":r.content||\"\")+'</div><span class=\"layui-layer-setwin\">'+function(){var e=c?'<a class=\"layui-layer-min\" href=\"javascript:;\"><cite></cite></a><a class=\"layui-layer-ico layui-layer-max\" href=\"javascript:;\"></a>':\"\";return r.closeBtn&&(e+='<a class=\"layui-layer-ico '+l[7]+\" \"+l[7]+(r.title?r.closeBtn:4==r.type?\"1\":\"2\")+'\" href=\"javascript:;\"></a>'),e}()+\"</span>\"+(r.btn?function(){var e=\"\";\"string\"==typeof r.btn&&(r.btn=[r.btn]);for(var t=0,i=r.btn.length;t<i;t++)e+='<a class=\"'+l[6]+t+'\">'+r.btn[t]+\"</a>\";return'<div class=\"'+l[6]+\" layui-layer-btn-\"+(r.btnAlign||\"\")+'\">'+e+\"</div>\"}():\"\")+(r.resize?'<span class=\"layui-layer-resize\"></span>':\"\")+\"</div>\"],u,i('<div class=\"layui-layer-move\"></div>')),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='<iframe scrolling=\"'+(t.content[1]||\"auto\")+'\" allowtransparency=\"true\" id=\"'+l[4]+a+'\" name=\"'+l[4]+a+'\" onload=\"this.className=\\'\\';\" class=\"layui-layer-load\" frameborder=\"0\" src=\"'+t.content[0]+'\"></iframe>';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]+'<i class=\"layui-layer-TipsG\"></i>',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;a<c.stX&&(a=c.stX),a>f&&(a=f),o<c.stY&&(o=c.stY),o>u&&(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?'<textarea class=\"layui-layer-input\"'+a+\">\"+(e.value||\"\")+\"</textarea>\":function(){return'<input type=\"'+(1==e.formType?\"password\":\"text\")+'\" class=\"layui-layer-input\" value=\"'+(e.value||\"\")+'\">'}(),f=e.success;return delete e.success,r.open(i.extend({type:1,btn:[\"&#x786E;&#x5B9A;\",\"&#x53D6;&#x6D88;\"],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(\"&#x6700;&#x591A;&#x8F93;&#x5165;\"+(e.maxlength||500)+\"&#x4E2A;&#x5B57;&#x6570;\",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='<span class=\"'+n+'\">'+t[0].title+\"</span>\";i<e;i++)a+=\"<span>\"+t[i].title+\"</span>\";return a}(),content:'<ul class=\"layui-layer-tabmain\">'+function(){var e=t.length,i=1,a=\"\";if(e>0)for(a='<li class=\"layui-layer-tabli '+n+'\">'+(t[0].content||\"no content\")+\"</li>\";i<e;i++)a+='<li class=\"layui-layer-tabli\">'+(t[i].content||\"no  content\")+\"</li>\";return a}()+\"</ul>\",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(\"&#x6CA1;&#x6709;&#x56FE;&#x7247;\")}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]<r[1]&&(a[0]=a[0]/r[1],a[1]=a[1]/r[1])}return[a[0]+\"px\",a[1]+\"px\"]}(),title:!1,shade:.9,shadeClose:!0,closeBtn:!1,move:\".layui-layer-phimg img\",moveType:1,scrollbar:!1,moveOut:!0,isOutAnim:!1,skin:\"layui-layer-photos\"+c(\"photos\"),content:'<div class=\"layui-layer-phimg\"><img src=\"'+u[d].src+'\" alt=\"'+(u[d].alt||\"\")+'\" layer-pid=\"'+u[d].pid+'\"><div class=\"layui-layer-imgsee\">'+(u.length>1?'<span class=\"layui-layer-imguide\"><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgprev\"></a><a href=\"javascript:;\" class=\"layui-layer-iconext layui-layer-imgnext\"></a></span>':\"\")+'<div class=\"layui-layer-imgbar\" style=\"display:'+(a?\"block\":\"\")+'\"><span class=\"layui-layer-imgtit\"><a href=\"javascript:;\">'+(u[d].alt||\"\")+\"</a><em>\"+s.imgIndex+\"/\"+u.length+\"</em></span></div></div></div>\",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(\"&#x5F53;&#x524D;&#x56FE;&#x7247;&#x5730;&#x5740;&#x5F02;&#x5E38;<br>&#x662F;&#x5426;&#x7EE7;&#x7EED;&#x67E5;&#x770B;&#x4E0B;&#x4E00;&#x5F20;&#xFF1F;\",{time:3e4,btn:[\"&#x4E0B;&#x4E00;&#x5F20;\",\"&#x4E0D;&#x770B;&#x4E86;\"],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);"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/layer/mobile/layer.js",
    "content": "/*! layer mobile-v2.0.0 Web弹层组件 MIT License  http://layer.layui.com/mobile  By 贤心 */\n ;!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?'<h3 style=\"'+(e?n.title[1]:\"\")+'\">'+(e?n.title[0]:n.title)+\"</h3>\":\"\"}(),c=function(){\"string\"==typeof n.btn&&(n.btn=[n.btn]);var e,t=(n.btn||[]).length;return 0!==t&&n.btn?(e='<span yes type=\"1\">'+n.btn[0]+\"</span>\",2===t&&(e='<span no type=\"0\">'+n.btn[1]+\"</span>\"+e),'<div class=\"layui-m-layerbtn\">'+e+\"</div>\"):\"\"}();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='<i></i><i class=\"layui-m-layerload\"></i><i></i><p>'+(n.content||\"\")+\"</p>\"),n.skin&&(n.anim=\"up\"),\"msg\"===n.skin&&(n.shade=!1),s.innerHTML=(n.shade?\"<div \"+(\"string\"==typeof n.shade?'style=\"'+n.shade+'\"':\"\")+' class=\"layui-m-layershade\"></div>':\"\")+'<div class=\"layui-m-layermain\" '+(n.fixed?\"\":'style=\"position:static;\"')+'><div class=\"layui-m-layersection\"><div class=\"layui-m-layerchild '+(n.skin?\"layui-m-layer-\"+n.skin+\" \":\"\")+(n.className?n.className:\"\")+\" \"+(n.anim?\"layui-m-anim-\"+n.anim:\"\")+'\" '+(n.style?'style=\"'+n.style+'\"':\"\")+\">\"+l+'<div class=\"layui-m-layercont\">'+n.content+\"</div>\"+c+\"</div></div></div>\",!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;o<r;o++)l.touch(s[o],a);if(e.shade&&e.shadeClose){var c=t[i](\"layui-m-layershade\")[0];l.touch(c,function(){layer.close(n.index,e.end)})}e.end&&(l.end[n.index]=e.end)},e.layer={v:\"2.0\",index:r,open:function(e){var t=new c(e||{});return t.index},close:function(e){var n=a(\"#\"+o[0]+e)[0];n&&(n.innerHTML=\"\",t.body.removeChild(n),clearTimeout(l.timer[e]),delete l.timer[e],\"function\"==typeof l.end[e]&&l.end[e](),delete l.end[e])},closeAll:function(){for(var e=t[i](o[0]),n=0,a=e.length;n<a;n++)layer.close(0|e[0].getAttribute(\"index\"))}},\"function\"==typeof define?define(function(){return layer}):function(){var e=document.scripts,n=e[e.length-1],i=n.src,a=i.substring(0,i.lastIndexOf(\"/\")+1);n.getAttribute(\"merge\")||document.head.appendChild(function(){var e=t.createElement(\"link\");return e.href=a+\"need/layer.css?2.0\",e.type=\"text/css\",e.rel=\"styleSheet\",e.id=\"layermcss\",e}())}()}(window);"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/layer/mobile/need/layer.css",
    "content": ".layui-m-layer{position:relative;z-index:19891014}.layui-m-layer *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.layui-m-layermain,.layui-m-layershade{position:fixed;left:0;top:0;width:100%;height:100%}.layui-m-layershade{background-color:rgba(0,0,0,.7);pointer-events:auto}.layui-m-layermain{display:table;font-family:Helvetica,arial,sans-serif;pointer-events:none}.layui-m-layermain .layui-m-layersection{display:table-cell;vertical-align:middle;text-align:center}.layui-m-layerchild{position:relative;display:inline-block;text-align:left;background-color:#fff;font-size:14px;border-radius:5px;box-shadow:0 0 8px rgba(0,0,0,.1);pointer-events:auto;-webkit-overflow-scrolling:touch;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;animation-duration:.2s}@-webkit-keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes layui-m-anim-scale{0%{opacity:0;-webkit-transform:scale(.5);transform:scale(.5)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.layui-m-anim-scale{animation-name:layui-m-anim-scale;-webkit-animation-name:layui-m-anim-scale}@-webkit-keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes layui-m-anim-up{0%{opacity:0;-webkit-transform:translateY(800px);transform:translateY(800px)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}.layui-m-anim-up{-webkit-animation-name:layui-m-anim-up;animation-name:layui-m-anim-up}.layui-m-layer0 .layui-m-layerchild{width:90%;max-width:640px}.layui-m-layer1 .layui-m-layerchild{border:none;border-radius:0}.layui-m-layer2 .layui-m-layerchild{width:auto;max-width:260px;min-width:40px;border:none;background:0 0;box-shadow:none;color:#fff}.layui-m-layerchild h3{padding:0 10px;height:60px;line-height:60px;font-size:16px;font-weight:400;border-radius:5px 5px 0 0;text-align:center}.layui-m-layerbtn span,.layui-m-layerchild h3{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.layui-m-layercont{padding:50px 30px;line-height:22px;text-align:center}.layui-m-layer1 .layui-m-layercont{padding:0;text-align:left}.layui-m-layer2 .layui-m-layercont{text-align:center;padding:0;line-height:0}.layui-m-layer2 .layui-m-layercont i{width:25px;height:25px;margin-left:8px;display:inline-block;background-color:#fff;border-radius:100%;-webkit-animation:layui-m-anim-loading 1.4s infinite ease-in-out;animation:layui-m-anim-loading 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.layui-m-layerbtn,.layui-m-layerbtn span{position:relative;text-align:center;border-radius:0 0 5px 5px}.layui-m-layer2 .layui-m-layercont p{margin-top:20px}@-webkit-keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}@keyframes layui-m-anim-loading{0%,100%,80%{transform:scale(0);-webkit-transform:scale(0)}40%{transform:scale(1);-webkit-transform:scale(1)}}.layui-m-layer2 .layui-m-layercont i:first-child{margin-left:0;-webkit-animation-delay:-.32s;animation-delay:-.32s}.layui-m-layer2 .layui-m-layercont i.layui-m-layerload{-webkit-animation-delay:-.16s;animation-delay:-.16s}.layui-m-layer2 .layui-m-layercont>div{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}"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/layer/theme/default/layer.css",
    "content": ".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}}"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.css",
    "content": "*{margin: 0; padding: 0;}\nbody{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}\n::-webkit-input-placeholder{color: #ccc;}\n\n/* 视图盒子 */\n.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}\n/* 背景 EAEFF3 */\n.bg-1{height: 50%; background: linear-gradient(to bottom right, #0466c5, #3496F5);}\n.bg-2{height: 50%; background-color: #EAEFF3;}\n\n/* 渐变背景 */\n.bg-1{\n    background-size: 500%;\n\tbackground-image: linear-gradient(125deg,#0466c5,#3496F5,#0466c5,#3496F5,#0466c5,#2496F5);\n\tanimation: bganimation 30s infinite;\n}\n@keyframes bganimation{\n    0%{background-position: 0% 50%;}\n    50%{background-position: 100% 50%;}\n    100%{background-position: 0% 50%;}\n}\n\n/* 内容盒子 */\n.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}\n\n/* 登录盒子 */\n/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */\n.login-box{width: 400px; margin: auto; max-width: 90%; height: 100%;}\n.login-box{display: flex; align-items: center; text-align: center;}\n\n/* 表单 */\n.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}\n.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}\n.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}\n\n/* 输入框 */\n.from-item{border: 0px #000 solid; margin-bottom: 15px;}\n.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;}\n.s-input{font-size: 12px;}\n.s-input:focus{border-color: #409eff}\n\n/* 登录按钮 */\n.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}\n.s-btn:hover{background-color: #50aEFF;}\n\n/* 重置按钮 */\n.reset-box{text-align: left; font-size: 12px;}\n.reset-box a{text-decoration: none;}\n.reset-box a:hover{text-decoration: underline;}\n\n/* loading框样式 */\n.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}\n.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/static/sa-res/login.js",
    "content": "// sa \nvar sa = {};\n\n// 打开loading\nsa.loading = function(msg) {\n\tlayer.closeAll();\t// 开始前先把所有弹窗关了\n\treturn layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });\n};\n\n// 隐藏loading\nsa.hideLoading = function() {\n\tlayer.closeAll();\n};\n\n\n// ----------------------------------- 登录事件 -----------------------------------\n\n$('.login-btn').click(function(){\n\tsa.loading(\"正在登录...\");\n\t// 开始登录\n\tsetTimeout(function() {\n\t\t$.ajax({\n\t\t\turl: \"doLogin\",\n\t\t\ttype: \"post\", \n\t\t\tdata: {\n\t\t\t\tname: $('[name=name]').val(),\n\t\t\t\tpwd: $('[name=pwd]').val()\n\t\t\t},\n\t\t\tdataType: 'json',\n\t\t\tsuccess: function(res){\n\t\t\t\tconsole.log('返回数据：', res);\n\t\t\t\tsa.hideLoading();\n\t\t\t\tif(res.code == 200) {\n\t\t\t\t\tlayer.msg('登录成功', {anim: 0, icon: 6 }); \n\t\t\t\t\tsetTimeout(function() {\n\t\t\t\t\t\tlocation.reload();\n\t\t\t\t\t}, 800)\n\t\t\t\t} else {\n\t\t\t\t\tlayer.msg(res.msg, {anim: 6, icon: 2 }); \n\t\t\t\t}\n\t\t\t},\n\t\t\terror: function(xhr, type, errorThrown){\n\t\t\t\tsa.hideLoading();\n\t\t\t\tif(xhr.status == 0){\n\t\t\t\t\treturn layer.alert('无法连接到服务器，请检查网络');\n\t\t\t\t}\n\t\t\t\treturn layer.alert(\"异常：\" + JSON.stringify(xhr));\n\t\t\t}\n\t\t});\n\t}, 400);\n});\n\n// 绑定回车事件\n$('[name=name],[name=pwd]').bind('keypress', function(event){\n\tif(event.keyCode == \"13\") {\n\t\t$('.login-btn').click();\n\t}\n});\n\n// 输入框获取焦点\n$(\"[name=name]\").focus();\n\n// 打印信息 \nvar str = \"This page is provided by Sa-Token, Please refer to: \" + \"https://sa-token.cc/\";\nconsole.log(str);\n"
  },
  {
    "path": "sa-token-plugin/sa-token-quick-login/src/main/resources/templates/sa-login.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh\">\n\t<head>\n\t\t<title>登录</title>\n\t\t<meta charset=\"utf-8\">\n\t\t<base th:href=\"@{/}\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n\t\t<link rel=\"stylesheet\" href=\"./sa-res/login.css\">\n\t</head>\n\t<body>\n\t\t<div class=\"view-box\">\n\t\t\t<div class=\"bg-1\"></div>\n\t\t\t<div class=\"bg-2\"></div>\n\t\t\t<div class=\"content-box\">\n\t\t\t\t<div class=\"login-box\">\n\t\t\t\t\t<div class=\"from-box\">\n\t\t\t\t\t\t<h2 class=\"from-title\" th:utext=\"${cfg.title}\"></h2>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"name\" placeholder=\"请输入账号\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<input class=\"s-input\" name=\"pwd\" type=\"password\" placeholder=\"请输入密码\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item\">\n\t\t\t\t\t\t\t<button class=\"s-input s-btn login-btn\">登录</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"from-item reset-box\">\n\t\t\t\t\t\t\t<a href=\"javascript: location.reload();\" >刷新</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- 底部 版权 -->\n\t\t\t<div style=\"position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;\" th:if=\"${cfg.copr}\">\n\t\t\t\tThis page is provided by Sa-Token\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- scripts -->\n\t\t<script src=\"./sa-res/jquery.min.js\"></script>\n\t\t<script src=\"./sa-res/layer/layer.js\"></script>\n\t\t<script src=\"./sa-res/login.js\"></script>\n\t\t\n\t</body>\n</html>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-jackson/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-redis-jackson</name>\n    <artifactId>sa-token-redis-jackson</artifactId>\n\t<description>sa-token integrate redis (to jackson)</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redis-template</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-redis-template</name>\n    <artifactId>sa-token-redis-template</artifactId>\n\t<description>sa-token integrate RedisTemplate</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- RedisTemplate 相关操作API -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\nimport cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.core.StringRedisTemplate;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Sa-Token 持久层实现 [ Redis 存储 ] (可用环境: SpringBoot2、SpringBoot3)\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenDaoForRedisTemplate implements SaTokenDaoByObjectFollowString, SaTokenDao {\n\n\tpublic StringRedisTemplate stringRedisTemplate;\n\n\t/**\n\t * 标记：当前 redis 连接信息是否已初始化成功\n\t */\n\tpublic boolean isInit;\n\t\n\t@Autowired\n\tpublic void init(RedisConnectionFactory connectionFactory) {\n\t\t// 如果已经初始化成功了，就立刻退出，不重复初始化\n\t\tif(this.isInit) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 构建StringRedisTemplate\n\t\tStringRedisTemplate stringTemplate = new StringRedisTemplate();\n\t\tstringTemplate.setConnectionFactory(connectionFactory);\n\t\tstringTemplate.afterPropertiesSet();\n\t\tthis.stringRedisTemplate = stringTemplate;\n\n\t\tinitMore(connectionFactory);\n\n\t\t// 打上标记，表示已经初始化成功，后续无需再重新初始化\n\t\tthis.isInit = true;\n\t}\n\n\tprotected void initMore(RedisConnectionFactory connectionFactory) {\n\n\t}\n\n\n\t/**\n\t * 获取Value，如无返空 \n\t */\n\t@Override\n\tpublic String get(String key) {\n\t\treturn stringRedisTemplate.opsForValue().get(key);\n\t}\n\n\t/**\n\t * 写入Value，并设定存活时间 (单位: 秒)\n\t */\n\t@Override\n\tpublic void set(String key, String value, long timeout) {\n\t\tif(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {\n\t\t\treturn;\n\t\t}\n\t\t// 判断是否为永不过期 \n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value);\n\t\t} else {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);\n\t\t}\n\t}\n\n\t/**\n\t * 修改指定key-value键值对 (过期时间不变) \n\t */\n\t@Override\n\tpublic void update(String key, String value) {\n\t\t@SuppressWarnings(\"all\")\n\t\tlong expireMs = stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS);\n\t\t// -2 = 无此键\n\t\tif (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn;\n\t\t}\n\t\t// -1 = 永不过期\n\t\tif(expireMs == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value);\n\t\t} else {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value, expireMs, TimeUnit.MILLISECONDS);\n\t\t}\n\t}\n\t\n\t/**\n\t * 删除Value \n\t */\n\t@Override\n\tpublic void delete(String key) {\n\t\tstringRedisTemplate.delete(key);\n\t}\n\n\t/**\n\t * 获取Value的剩余存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic long getTimeout(String key) {\n\t\treturn stringRedisTemplate.getExpire(key);\n\t}\n\n\t/**\n\t * 修改Value的剩余存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic void updateTimeout(String key, long timeout) {\n\t\t// 判断是否想要设置为永久\n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tlong expire = getTimeout(key);\n\t\t\tif(expire == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t\t// 如果其已经被设置为永久，则不作任何处理 \n\t\t\t} else {\n\t\t\t\t// 如果尚未被设置为永久，那么再次set一次\n\t\t\t\tthis.set(key, this.get(key), timeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tstringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);\n\t}\n\n\n\t\n\t/**\n\t * 搜索数据 \n\t */\n\t@Override\n\tpublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\t\tSet<String> keys = stringRedisTemplate.keys(prefix + \"*\" + keyword + \"*\");\n\t\tList<String> list = new ArrayList<>(keys);\n\t\treturn SaFoxUtil.searchList(list, start, size, sortType);\n\t}\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoForRedisTemplate"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template-jdk-serializer/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-redis-template-jdk-serializer</name>\n    <artifactId>sa-token-redis-template-jdk-serializer</artifactId>\n\t<description>sa-token integrate RedisTemplate （jdk-serializer）</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- RedisTemplate 相关操作API -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\nimport cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.core.StringRedisTemplate;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Sa-Token 持久层实现 [ RedisTemplate 存储 ] (可用环境: SpringBoot2、SpringBoot3)\n * <br> copy by: sa-token-redis-template 插件\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenDaoForRedisTemplate implements SaTokenDaoByObjectFollowString, SaTokenDao {\n\n\tpublic StringRedisTemplate stringRedisTemplate;\n\n\t/**\n\t * 标记：当前 redis 连接信息是否已初始化成功\n\t */\n\tpublic boolean isInit;\n\t\n\t@Autowired\n\tpublic void init(RedisConnectionFactory connectionFactory) {\n\t\t// 如果已经初始化成功了，就立刻退出，不重复初始化\n\t\tif(this.isInit) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 构建StringRedisTemplate\n\t\tStringRedisTemplate stringTemplate = new StringRedisTemplate();\n\t\tstringTemplate.setConnectionFactory(connectionFactory);\n\t\tstringTemplate.afterPropertiesSet();\n\t\tthis.stringRedisTemplate = stringTemplate;\n\n\t\tinitMore(connectionFactory);\n\n\t\t// 打上标记，表示已经初始化成功，后续无需再重新初始化\n\t\tthis.isInit = true;\n\t}\n\n\tprotected void initMore(RedisConnectionFactory connectionFactory) {\n\n\t}\n\n\n\t/**\n\t * 获取Value，如无返空 \n\t */\n\t@Override\n\tpublic String get(String key) {\n\t\treturn stringRedisTemplate.opsForValue().get(key);\n\t}\n\n\t/**\n\t * 写入Value，并设定存活时间 (单位: 秒)\n\t */\n\t@Override\n\tpublic void set(String key, String value, long timeout) {\n\t\tif(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {\n\t\t\treturn;\n\t\t}\n\t\t// 判断是否为永不过期 \n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value);\n\t\t} else {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);\n\t\t}\n\t}\n\n\t/**\n\t * 修改指定key-value键值对 (过期时间不变) \n\t */\n\t@Override\n\tpublic void update(String key, String value) {\n\t\t@SuppressWarnings(\"all\")\n\t\tlong expireMs = stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS);\n\t\t// -2 = 无此键\n\t\tif (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn;\n\t\t}\n\t\t// -1 = 永不过期\n\t\tif(expireMs == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value);\n\t\t} else {\n\t\t\tstringRedisTemplate.opsForValue().set(key, value, expireMs, TimeUnit.MILLISECONDS);\n\t\t}\n\t}\n\t\n\t/**\n\t * 删除Value \n\t */\n\t@Override\n\tpublic void delete(String key) {\n\t\tstringRedisTemplate.delete(key);\n\t}\n\n\t/**\n\t * 获取Value的剩余存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic long getTimeout(String key) {\n\t\treturn stringRedisTemplate.getExpire(key);\n\t}\n\n\t/**\n\t * 修改Value的剩余存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic void updateTimeout(String key, long timeout) {\n\t\t// 判断是否想要设置为永久\n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tlong expire = getTimeout(key);\n\t\t\tif(expire == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t\t// 如果其已经被设置为永久，则不作任何处理 \n\t\t\t} else {\n\t\t\t\t// 如果尚未被设置为永久，那么再次set一次\n\t\t\t\tthis.set(key, this.get(key), timeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tstringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);\n\t}\n\n\n\t\n\t/**\n\t * 搜索数据 \n\t */\n\t@Override\n\tpublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\t\tSet<String> keys = stringRedisTemplate.keys(prefix + \"*\" + keyword + \"*\");\n\t\tList<String> list = new ArrayList<>(keys);\n\t\treturn SaFoxUtil.searchList(list, start, size, sortType);\n\t}\n\t\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisTemplateUseJdkSerializer.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;\nimport org.springframework.data.redis.serializer.StringRedisSerializer;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Sa-Token 持久层实现 [ RedisTemplate 存储、JDK默认序列化 ] (可用环境: SpringBoot2、SpringBoot3)\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenDaoForRedisTemplateUseJdkSerializer extends SaTokenDaoForRedisTemplate implements SaTokenDao {\n\n\t/**\n\t * Object 读写专用\n\t */\n\tpublic RedisTemplate<String, Object> objectRedisTemplate;\n\n\t@Override\n\tprotected void initMore(RedisConnectionFactory connectionFactory) {\n\n\t\t// 指定相应的序列化方案\n\t\tStringRedisSerializer keySerializer = new StringRedisSerializer();\n\t\tJdkSerializationRedisSerializer valueSerializer = new JdkSerializationRedisSerializer();\n\n\t\t// 构建RedisTemplate\n\t\tRedisTemplate<String, Object> template = new RedisTemplate<>();\n\t\ttemplate.setConnectionFactory(connectionFactory);\n\t\ttemplate.setKeySerializer(keySerializer);\n\t\ttemplate.setHashKeySerializer(keySerializer);\n\t\ttemplate.setValueSerializer(valueSerializer);\n\t\ttemplate.setHashValueSerializer(valueSerializer);\n\t\ttemplate.afterPropertiesSet();\n\t\tthis.objectRedisTemplate = template;\n\t}\n\n\t\n\t/**\n\t * 获取Object，如无返空 \n\t */\n\t@Override\n\tpublic Object getObject(String key) {\n\t\treturn objectRedisTemplate.opsForValue().get(key);\n\t}\n\n\t/**\n\t * 获取 Object (指定反序列化类型)，如无返空\n\t *\n\t * @param key 键名称\n\t * @return object\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic <T> T getObject(String key, Class<T> classType) {\n\t\treturn (T) objectRedisTemplate.opsForValue().get(key);\n\t}\n\n\t/**\n\t * 写入Object，并设定存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic void setObject(String key, Object object, long timeout) {\n\t\tif(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {\n\t\t\treturn;\n\t\t}\n\t\t// 判断是否为永不过期 \n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tobjectRedisTemplate.opsForValue().set(key, object);\n\t\t} else {\n\t\t\tobjectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);\n\t\t}\n\t}\n\n\t/**\n\t * 更新Object (过期时间不变) \n\t */\n\t@Override\n\tpublic void updateObject(String key, Object object) {\n\t\t@SuppressWarnings(\"all\")\n\t\tlong expireMs = stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS);\n\t\t// -2 = 无此键\n\t\tif (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn;\n\t\t}\n\t\t// -1 = 永不过期\n\t\tif(expireMs == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tobjectRedisTemplate.opsForValue().set(key, object);\n\t\t} else {\n\t\t\tobjectRedisTemplate.opsForValue().set(key, object, expireMs, TimeUnit.MILLISECONDS);\n\t\t}\n\t}\n\n\t/**\n\t * 删除Object \n\t */\n\t@Override\n\tpublic void deleteObject(String key) {\n\t\tobjectRedisTemplate.delete(key);\n\t}\n\n\t/**\n\t * 获取Object的剩余存活时间 (单位: 秒)\n\t */\n\t@Override\n\tpublic long getObjectTimeout(String key) {\n\t\treturn objectRedisTemplate.getExpire(key);\n\t}\n\n\t/**\n\t * 修改Object的剩余存活时间 (单位: 秒)\n\t */\n\t@Override\n\tpublic void updateObjectTimeout(String key, long timeout) {\n\t\t// 判断是否想要设置为永久\n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tlong expire = getObjectTimeout(key);\n\t\t\tif(expire == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t\t// 如果其已经被设置为永久，则不作任何处理 \n\t\t\t} else {\n\t\t\t\t// 如果尚未被设置为永久，那么再次set一次\n\t\t\t\tthis.setObject(key, this.getObject(key), timeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tobjectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer"
  },
  {
    "path": "sa-token-plugin/sa-token-redis-template-jdk-serializer/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoForRedisTemplateUseJdkSerializer"
  },
  {
    "path": "sa-token-plugin/sa-token-redisson/README.md",
    "content": "## sa-token-redisson\n\n此扩展，不与生态绑定。可用于不同的生态（SpringBoot，Solon，JFinal等）。\n\n### 1、例 solon 集成\n\n添加关键依赖\n\n```xml\n<dependencies>\n    <dependency>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-redisson</artifactId>\n        <version>${sa-token.version}</version>\n    </dependency>\n    <dependency>\n        <groupId>org.noear</groupId>\n        <artifactId>redisson-solon-plugin</artifactId>\n        <version>${solon.version}</version>\n    </dependency>\n</dependencies>\n```\n\n添加 dao 配置\n\n```yaml\nsa-token-dao:\n    config: |\n        singleServerConfig:\n          password: \"123456\"\n          address: \"redis://localhost:6379\"\n          database: 0\n```\n\n开始组装\n\n```java\n@Configuration\npublic class SaTokenConfigure {\n\t/**\n\t * 构造 RedissonClient\n\t * */\n\t@Bean\n\tpublic RedissonClient saTokenDaoInit(@Inject(\"${sa-token-dao}\") RedissonSupplier supplier) {\n\t\treturn supplier.get();\n\t}\n\n\t/**\n\t * 构建  SaTokenDao\n\t * */\n\t@Bean\n\tpublic SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {\n\t\treturn new SaTokenDaoForRedisson(redissonClient);\n\t}\n}\n```\n\n\n### 2、例 springboot 集成\n\n\n添加关键依赖\n\n```xml\n<dependencies>\n    <dependency>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-redisson</artifactId>\n        <version>${sa-token.version}</version>\n    </dependency>\n    <dependency>\n        <groupId>org.redisson</groupId>\n        <artifactId>redisson-spring-boot-starter</artifactId>\n        <version>${redisson.version}</version>\n    </dependency>\n</dependencies>\n```\n\n添加 dao 配置\n\n```yaml\nspring.redis:\n  redisson:\n    file: classpath:redisson.yml\n```\n\n开始组装\n\n```java\n@Configuration\npublic class SaTokenConfigure {\n\t/**\n\t * 构建  SaTokenDao\n\t * */\n\t@Bean\n\tpublic SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {\n\t\treturn new SaTokenDaoForRedisson(redissonClient);\n\t}\n}\n```"
  },
  {
    "path": "sa-token-plugin/sa-token-redisson/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-redisson</name>\n    <artifactId>sa-token-redisson</artifactId>\n\t<description>sa-token integrate Redisson</description>\n\n\t<!-- parent 里的版本适合与 springboot2 兼容；其它更新的框架里版本容易冲突 -->\n\t<!--<properties>\n\t\t<redisson.version>3.27.2</redisson.version>\n\t</properties>-->\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\n\t\t<!-- Redisson 相关操作API -->\n\t\t<dependency>\n\t\t\t<groupId>org.redisson</groupId>\n\t\t\t<artifactId>redisson</artifactId>\n\t\t\t<!--<exclusions>\n\t\t\t\t<exclusion>\n\t\t\t\t\t<groupId>org.yaml</groupId>\n\t\t\t\t\t<artifactId>snakeyaml</artifactId>\n\t\t\t\t</exclusion>\n\t\t\t</exclusions>-->\n\t\t</dependency>\n\n\t</dependencies>\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redisson/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisson.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\nimport cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.redisson.api.RBatch;\nimport org.redisson.api.RBucket;\nimport org.redisson.api.RBucketAsync;\nimport org.redisson.api.RedissonClient;\n\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * Sa-Token 持久层实现  [ Redisson客户端、Redis存储 ]\n * \n * @author 疯狂的狮子Li\n * @author noear\n * @since 1.34.0\n */\npublic class SaTokenDaoForRedisson implements SaTokenDaoByObjectFollowString, SaTokenDao {\n\n\t/**\n\t * redisson 客户端\n\t */\n\tpublic final RedissonClient redissonClient;\n\n\tpublic SaTokenDaoForRedisson(RedissonClient redissonClient) {\n\t\tthis.redissonClient = redissonClient;\n\t}\n\t\n\t\n\t/**\n\t * 获取Value，如无返空 \n\t */\n\t@Override\n\tpublic String get(String key) {\n\t\tRBucket<String> rBucket = redissonClient.getBucket(key);\n\t\treturn rBucket.get();\n\t}\n\n\t/**\n\t * 写入Value，并设定存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic void set(String key, String value, long timeout) {\n\t\tif(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {\n\t\t\treturn;\n\t\t}\n\t\t// 判断是否为永不过期\n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tRBucket<String> bucket = redissonClient.getBucket(key);\n\t\t\tbucket.set(value);\n\t\t} else {\n\t\t\tRBatch batch = redissonClient.createBatch();\n\t\t\tRBucketAsync<String> bucket = batch.getBucket(key);\n\t\t\tbucket.setAsync(value);\n\t\t\tbucket.expireAsync(Duration.ofSeconds(timeout));\n\t\t\tbatch.execute();\n\t\t}\n\t}\n\n\t/**\n\t * 修修改指定key-value键值对 (过期时间不变) \n\t */\n\t@Override\n\tpublic void update(String key, String value) {\n\t\tlong expire = getTimeout(key);\n\t\t// -2 = 无此键 \n\t\tif(expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n\t\t\treturn;\n\t\t}\n\t\tthis.set(key, value, expire);\n\t}\n\t\n\t/**\n\t * 删除Value \n\t */\n\t@Override\n\tpublic void delete(String key) {\n\t\tredissonClient.getBucket(key).delete();\n\t}\n\n\t/**\n\t * 获取Value的剩余存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic long getTimeout(String key) {\n\t\tRBucket<String> rBucket = redissonClient.getBucket(key);\n\t\tlong timeout = rBucket.remainTimeToLive();\n\t\treturn timeout < 0 ? timeout : timeout / 1000;\n\t}\n\n\t/**\n\t * 修改Value的剩余存活时间 (单位: 秒) \n\t */\n\t@Override\n\tpublic void updateTimeout(String key, long timeout) {\n\t\t// 判断是否想要设置为永久\n\t\tif(timeout == SaTokenDao.NEVER_EXPIRE) {\n\t\t\tlong expire = getTimeout(key);\n\t\t\tif(expire == SaTokenDao.NEVER_EXPIRE) {\n\t\t\t\t// 如果其已经被设置为永久，则不作任何处理 \n\t\t\t} else {\n\t\t\t\t// 如果尚未被设置为永久，那么再次set一次\n\t\t\t\tthis.set(key, this.get(key), timeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tRBucket<String> rBucket = redissonClient.getBucket(key);\n\t\trBucket.expire(Duration.ofSeconds(timeout));\n\t}\n\n\t\n\t/**\n\t * 搜索数据 \n\t */\n\t@Override\n\tpublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n\t\tStream<String> stream = redissonClient.getKeys().getKeysStreamByPattern(prefix + \"*\" + keyword + \"*\");\n\t\tList<String> list = stream.collect(Collectors.toList());\n\t\treturn SaFoxUtil.searchList(list, start, size, sortType);\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redisson-spring-boot-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-redisson-spring-boot-starter</name>\n    <artifactId>sa-token-redisson-spring-boot-starter</artifactId>\n\t<description>sa-token integrate redisson (to jackson)</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\n\t\t<!-- sa-token-redisson -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-redisson</artifactId>\n\t\t</dependency>\n\n\t\t<!-- Redisson 相关操作API -->\n\t\t<dependency>\n\t\t\t<groupId>org.redisson</groupId>\n\t\t\t<artifactId>redisson-spring-boot-starter</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenDaoForRedissonBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisson;\nimport org.redisson.api.RedissonClient;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 SaTokenDaoForRedisson Bean\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenDaoForRedissonBeanRegister {\n\n\t@Bean\n\tpublic SaTokenDao getSaTokenDaoForRedisson(RedissonClient redissonClient) {\n\t\treturn new SaTokenDaoForRedisson(redissonClient);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.spring.SaTokenDaoForRedissonBeanRegister"
  },
  {
    "path": "sa-token-plugin/sa-token-redisson-spring-boot-starter/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.spring.SaTokenDaoForRedissonBeanRegister"
  },
  {
    "path": "sa-token-plugin/sa-token-redisx/README.md",
    "content": "\nsa-token-redisx 是中立的扩展。可任何应用开发框架下使用（springboot, solon, jfinal 等..）\n\n### 使用示例\n\n#### 1.配置\n\n```yaml\nsa-token: #名字可以随意取\n  redis:\n    server: \"localhost:6379\"\n    password: 123456\n    db: 1\n#   serializer: \"org.noear.redisx.utils.SerializerJson\" #指定自定义序列化实现（默认为 SerializerDefault）\n```\n\n#### 2.代码\n\n**注入风格**\n\n```java\n@Configuration\npublic class Config {\n    @Bean\n    public SaTokenDao saTokenDaoInit(@Inject(\"${sa-token.redis}\") SaTokenDaoOfRedis saTokenDao) {\n        return saTokenDao;\n    }\n}\n```\n\n**手动风格**\n\n```java\nSaTokenDaoOfRedis saTokenDao = new SaTokenDaoOfRedis(props);\nSaManager.setSaTokenDao(saTokenDao);\n```"
  },
  {
    "path": "sa-token-plugin/sa-token-redisx/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-redisx</name>\n    <artifactId>sa-token-redisx</artifactId>\n\t<description>sa-token integrate redis</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.noear</groupId>\n            <artifactId>redisx</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.noear</groupId>\n            <artifactId>solon-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redisx/src/main/java/cn/dev33/satoken/dao/SaTokenDaoForRedisx.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.dao;\n\nimport cn.dev33.satoken.dao.auto.SaTokenDaoByObjectFollowString;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.noear.redisx.RedisClient;\nimport org.noear.redisx.plus.RedisBucket;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Properties;\nimport java.util.Set;\n\n/**\n * SaTokenDao 的 redis 适配（基于json序列化，不能完全精准还原所有类型）\n *\n * @author noear\n * @since 1.34.0\n * @since 1.41.0\n */\npublic class SaTokenDaoForRedisx implements SaTokenDaoByObjectFollowString, SaTokenDao {\n    private final RedisBucket redisBucket;\n\n    public SaTokenDaoForRedisx(Properties props) {\n        this(new RedisClient(props));\n    }\n\n    public SaTokenDaoForRedisx(RedisClient redisClient) {\n        redisBucket = redisClient.getBucket();\n    }\n\n    /**\n     * 获取Value，如无返空\n     */\n    @Override\n    public String get(String key) {\n        return redisBucket.get(key);\n    }\n\n    /**\n     * 写入Value，并设定存活时间 (单位: 秒)\n     */\n    @Override\n    public void set(String key, String value, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n\n        // 判断是否为永不过期\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            redisBucket.store(key, value);\n        } else {\n            redisBucket.store(key, value, (int) timeout);\n        }\n    }\n\n    /**\n     * 修改指定key-value键值对 (过期时间不变)\n     */\n    @Override\n    public void update(String key, String value) {\n        long expire = getTimeout(key);\n        // -2 = 无此键\n        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n\n        this.set(key, value, expire);\n    }\n\n    /**\n     * 删除Value\n     */\n    @Override\n    public void delete(String key) {\n        redisBucket.remove(key);\n    }\n\n    /**\n     * 获取Value的剩余存活时间 (单位: 秒)\n     */\n    @Override\n    public long getTimeout(String key) {\n        return redisBucket.ttl(key);\n    }\n\n    /**\n     * 修改Value的剩余存活时间 (单位: 秒)\n     */\n    @Override\n    public void updateTimeout(String key, long timeout) {\n        // 判断是否想要设置为永久\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            long expire = getTimeout(key);\n            if (expire == SaTokenDao.NEVER_EXPIRE) {\n                // 如果其已经被设置为永久，则不作任何处理\n            } else {\n                // 如果尚未被设置为永久，那么再次set一次\n                this.set(key, this.get(key), timeout);\n            }\n            return;\n        }\n\n\n        redisBucket.delay(key, (int) timeout);\n    }\n\n    /**\n     * 搜索数据\n     */\n    @Override\n    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n        Set<String> keys = redisBucket.keys(prefix + \"*\" + keyword + \"*\");\n        List<String> list = new ArrayList<>(keys);\n        return SaFoxUtil.searchList(list, start, size, sortType);\n    }\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-redisx/src/test/java/demo/App.java",
    "content": "package demo;\n\nimport org.noear.solon.Solon;\n\n/**\n * @author noear 2022/3/30 created\n */\npublic class App {\n    public static void main(String[] args) {\n        Solon.start(App.class, args);\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redisx/src/test/java/demo/Config.java",
    "content": "package demo;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisx;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * @author noear 2022/3/30 created\n */\n@Configuration\npublic class Config {\n    @Bean\n    public void saTokenDaoInit(@Inject(\"${sa-token.redis}\") SaTokenDaoForRedisx saTokenDao) {\n        //手动操作，可适用于任何框架\n        SaManager.setSaTokenDao(saTokenDao);\n    }\n\n\n    @Bean\n    public SaTokenDao saTokenDaoInit2(@Inject(\"${sa-token.redis}\") SaTokenDaoForRedisx saTokenDao) {\n        //Solon 项目，可用此案\n        return saTokenDao;\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-redisx/src/test/resources/app.yml",
    "content": "\n\n\n\nsa-token: #名字可以随意取\n  redis:\n    server: \"localhost:6379\"\n    password: 123456\n    db: 1\n    maxTotal: 200\n"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-serializer-features</name>\n    <artifactId>sa-token-serializer-features</artifactId>\n\t<description>sa-token-serializer-features</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSerializerFeatures.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\n/**\n * SaToken 插件安装：自定义序列化器\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForSerializerFeatures implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        // 默认不注册，需要开发者手动注册去选择\n        // SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan());\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseCustomCharacters.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer;\n\nimport cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdk;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 序列化器，base64 算法，采用自定义字符集\n * \n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerForBase64UseCustomCharacters implements SaSerializerTemplateForJdk {\n\t\n\t// 自定义字符集，需确保包含64个中文字符\n\tpublic String CUSTOM_CHARS;\n\n\t// 填充符，确保不在字符集中\n\tpublic char PAD_CHAR;\n\n\tpublic SaSerializerForBase64UseCustomCharacters(String customChars, char padChar) {\n\t\tif (customChars.length() != 64) {\n\t\t\tthrow new IllegalArgumentException(\"自定义字符集长度必须为64\");\n\t\t}\n\t\tif (customChars.indexOf(padChar) != -1) {\n\t\t\tthrow new IllegalArgumentException(\"填充符不能在自定义字符集中\");\n\t\t}\n\t\tthis.CUSTOM_CHARS = customChars;\n\t\tthis.PAD_CHAR = padChar;\n\t}\n\n\t@Override\n\tpublic String bytesToString(byte[] data) {\n\n\t\tStringBuilder encoded = new StringBuilder();\n\t\tint length = data.length;\n\t\tint i = 0;\n\n\t\t// 处理完整的3字节组\n\t\twhile (i < length - 2) {\n\t\t\tint byte1 = data[i++] & 0xFF;\n\t\t\tint byte2 = data[i++] & 0xFF;\n\t\t\tint byte3 = data[i++] & 0xFF;\n\n\t\t\tint combined = (byte1 << 16) | (byte2 << 8) | byte3;\n\t\t\tencoded.append(CUSTOM_CHARS.charAt((combined >> 18) & 0x3F));\n\t\t\tencoded.append(CUSTOM_CHARS.charAt((combined >> 12) & 0x3F));\n\t\t\tencoded.append(CUSTOM_CHARS.charAt((combined >> 6) & 0x3F));\n\t\t\tencoded.append(CUSTOM_CHARS.charAt(combined & 0x3F));\n\t\t}\n\n\t\t// 处理剩余字节（0、1或2个）\n\t\tint remaining = length - i;\n\t\tif (remaining > 0) {\n\t\t\tint byte1 = data[i++] & 0xFF;\n\t\t\tint byte2 = remaining > 1 ? data[i++] & 0xFF : 0;\n\n\t\t\tint combined = (byte1 << 16) | (byte2 << 8);\n\t\t\tencoded.append(CUSTOM_CHARS.charAt((combined >> 18) & 0x3F));\n\t\t\tencoded.append(CUSTOM_CHARS.charAt((combined >> 12) & 0x3F));\n\n\t\t\tif (remaining == 1) {\n\t\t\t\tencoded.append(PAD_CHAR).append(PAD_CHAR);\n\t\t\t} else {\n\t\t\t\tencoded.append(CUSTOM_CHARS.charAt((combined >> 6) & 0x3F));\n\t\t\t\tencoded.append(PAD_CHAR);\n\t\t\t}\n\t\t}\n\n\t\treturn encoded.toString();\n\t}\n\n\t@Override\n\tpublic byte[] stringToBytes(String encodedStr) {\n\t\tif (CUSTOM_CHARS.length() != 64) {\n\t\t\tthrow new IllegalStateException(\"自定义字符集长度必须为64\");\n\t\t}\n\n\t\tMap<Character, Integer> charMap = new HashMap<>();\n\t\tfor (int i = 0; i < CUSTOM_CHARS.length(); i++) {\n\t\t\tcharMap.put(CUSTOM_CHARS.charAt(i), i);\n\t\t}\n\n\t\tint length = encodedStr.length();\n\t\tif (length % 4 != 0) {\n\t\t\tthrow new IllegalArgumentException(\"编码字符串长度无效\");\n\t\t}\n\n\t\t// 计算填充符数量\n\t\tint paddingCount = 0;\n\t\tfor (int i = length - 1; i >= 0 && encodedStr.charAt(i) == PAD_CHAR; i--) {\n\t\t\tpaddingCount++;\n\t\t}\n\n\t\tint numGroups = length / 4;\n\t\tbyte[] decoded = new byte[numGroups * 3 - paddingCount];\n\t\tint decodedIndex = 0;\n\n\t\tfor (int group = 0; group < numGroups; group++) {\n\t\t\tint[] indices = new int[4];\n\t\t\tfor (int j = 0; j < 4; j++) {\n\t\t\t\tchar c = encodedStr.charAt(group * 4 + j);\n\t\t\t\tif (c == PAD_CHAR) {\n\t\t\t\t\tindices[j] = 0; // 填充符处理为0，后续根据paddingCount调整\n\t\t\t\t} else {\n\t\t\t\t\tInteger index = charMap.get(c);\n\t\t\t\t\tif (index == null) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"无效字符: \" + c);\n\t\t\t\t\t}\n\t\t\t\t\tindices[j] = index;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tint combined = (indices[0] << 18) | (indices[1] << 12) | (indices[2] << 6) | indices[3];\n\t\t\tfor (int k = 0; k < 3; k++) {\n\t\t\t\tif (decodedIndex < decoded.length) {\n\t\t\t\t\tdecoded[decodedIndex++] = (byte) ((combined >> (16 - 8 * k)) & 0xFF);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn decoded;\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseEmoji.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer;\n\nimport cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdk;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 序列化器，base64 算法，采用 64 个 Emoji 小黄脸作为元字符集，无填充字符\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerForBase64UseEmoji implements SaSerializerTemplateForJdk {\n\n\tprivate final List<String> EMOJI_TABLE = new ArrayList<>();  // 编码表\n\tprivate final Map<String, Integer> EMOJI_MAP = new HashMap<>();  // 解码表\n\n\tpublic SaSerializerForBase64UseEmoji() {\n\t\t// 初始化编码表（64个Emoji，U+1F600 到 U+1F63F）\n\t\tfor (int i = 0; i < 64; i++) {\n\t\t\tint codePoint = 0x1F600 + i;\n\t\t\tString emoji = new String(Character.toChars(codePoint));\n\t\t\tEMOJI_TABLE.add(emoji);\n\t\t\tEMOJI_MAP.put(emoji, i);\n\t\t}\n\t}\n\n\t@Override\n\tpublic String bytesToString(byte[] data) {\n\t\tStringBuilder binaryStr = new StringBuilder();\n\t\tfor (byte b : data) {\n\t\t\tbinaryStr.append(String.format(\"%8s\", Integer.toBinaryString(b & 0xFF))\n\t\t\t\t\t.replace(' ', '0'));\n\t\t}\n\n\t\t// 补零到6的倍数\n\t\tint bitLength = binaryStr.length();\n\t\tint paddingBits = (6 - (bitLength % 6)) % 6;\n\t\tfor (int i = 0; i < paddingBits; i++) {\n\t\t\tbinaryStr.append('0');\n\t\t}\n\n\t\t// 转换为索引\n\t\tList<Integer> indices = new ArrayList<>();\n\t\tfor (int i = 0; i < binaryStr.length(); i += 6) {\n\t\t\tString chunk = binaryStr.substring(i, Math.min(i + 6, binaryStr.length()));\n\t\t\tindices.add(Integer.parseInt(chunk, 2));\n\t\t}\n\n\t\t// 拼接Emoji\n\t\tStringBuilder result = new StringBuilder();\n\t\tfor (int index : indices) {\n\t\t\tresult.append(EMOJI_TABLE.get(index));\n\t\t}\n\t\treturn result.toString();\n\t}\n\n\t@Override\n\tpublic byte[] stringToBytes(String encoded) {\n\t\tList<Integer> indices = new ArrayList<>();\n\n\t\t// 提取索引（每个Emoji占2个char）\n\t\tfor (int i = 0; i < encoded.length(); ) {\n\t\t\tif (i + 1 >= encoded.length()) break;\n\t\t\tString emoji = encoded.substring(i, i + 2);\n\t\t\ti += 2;\n\n\t\t\tInteger index = EMOJI_MAP.get(emoji);\n\t\t\tif (index == null) {\n\t\t\t\tthrow new IllegalArgumentException(\"非法Emoji: \" + emoji);\n\t\t\t}\n\t\t\tindices.add(index);\n\t\t}\n\n\t\t// 转换为二进制字符串\n\t\tStringBuilder binaryStr = new StringBuilder();\n\t\tfor (int index : indices) {\n\t\t\tbinaryStr.append(String.format(\"%6s\", Integer.toBinaryString(index))\n\t\t\t\t\t.replace(' ', '0'));\n\t\t}\n\n\t\t// 转换为字节数组（自动处理末尾补零）\n\t\tList<Byte> bytes = new ArrayList<>();\n\t\tfor (int i = 0; i < binaryStr.length(); i += 8) {\n\t\t\tint endIndex = Math.min(i + 8, binaryStr.length());\n\t\t\tString byteStr = binaryStr.substring(i, endIndex);\n\t\t\tif (byteStr.length() < 8) break; // 忽略末尾不足8位的部分\n\t\t\tbytes.add((byte) Integer.parseInt(byteStr, 2));\n\t\t}\n\n\t\tbyte[] result = new byte[bytes.size()];\n\t\tfor (int i = 0; i < bytes.size(); i++) {\n\t\t\tresult[i] = bytes.get(i);\n\t\t}\n\t\treturn result;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UsePeriodicTable.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer;\n\n/**\n * 序列化器，base64 算法，采用 元素周期表 前六十四位作为元字符集\n * \n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerForBase64UsePeriodicTable extends SaSerializerForBase64UseCustomCharacters {\n\n\tpublic SaSerializerForBase64UsePeriodicTable() {\n\t\tsuper(\n\t\t\t\t// 自定义字符集，需确保包含64个不重复的字符\n\t\t\t\t\"氢氦锂铍硼碳氮氧\" +\n\t\t\t\t\"氟氖钠镁铝硅磷硫\" +\n\t\t\t\t\"氯氩钾钙钪钛钒铬\" +\n\t\t\t\t\"锰铁钴镍铜锌镓锗\" +\n\t\t\t\t\"砷硒溴氪铷锶钇锆\" +\n\t\t\t\t\"铌钼锝钌铑钯银镉\" +\n\t\t\t\t\"铟锡锑碲碘氙铯钡\" +\n\t\t\t\t\"镧铈镨钕钷钐铕钆\"\n\t\t\t\t,\n\t\t\t\t// 填充符，确保不在字符集中\n\t\t\t\t'鿫'\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseSpecialSymbols.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer;\n\n/**\n * 序列化器，base64 算法，采用64个特殊符号作为元字符集\n * \n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerForBase64UseSpecialSymbols extends SaSerializerForBase64UseCustomCharacters {\n\n\tpublic SaSerializerForBase64UseSpecialSymbols() {\n\t\tsuper(\n\t\t\t\t// 自定义字符集，需确保包含64个不重复的字符\n\t\t\t\t\"▲▼●◆■★▶◀\" +\n\t\t\t\t\"♠♥♦♣▁▂▃▄\" +\n\t\t\t\t\"▅▆▇█▏▎▍▌\" +\n\t\t\t\t\"▋▊▉▬〓◤◥◣\" +\n\t\t\t\t\"◢♩♪♫♬§〼↖\" +\n\t\t\t\t\"↑↗←→↙↓↘☴\" +\n\t\t\t\t\"☲☷☳☱☶☵☰◐\" +\n\t\t\t\t\"◑☀☼▪•‥…∷\"\n\t\t\t\t,\n\t\t\t\t// 填充符，确保不在字符集中\n\t\t\t\t'※'\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/src/main/java/cn/dev33/satoken/serializer/SaSerializerForBase64UseTianGan.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.serializer;\n\n/**\n * 序列化器，base64 算法，采用 十大天干、十二地支 等64个中文字符作为元字符集\n * \n * @author click33\n * @since 1.41.0\n */\npublic class SaSerializerForBase64UseTianGan extends SaSerializerForBase64UseCustomCharacters {\n\n\tpublic SaSerializerForBase64UseTianGan() {\n\t\tsuper(\n\t\t\t\t// 自定义字符集，需确保包含64个不重复的字符\n\t\t\t\t\"甲乙丙丁戊己庚辛\" +\n\t\t\t\t\"壬癸子丑寅卯辰巳\" +\n\t\t\t\t\"午未申酉戌亥乾坤\" +\n\t\t\t\t\"震巽坎离艮兑金木\" +\n\t\t\t\t\"水火土天地日月山\" +\n\t\t\t\t\"石田风雷电霜雾露\" +\n\t\t\t\t\"东南西北中信谷岚\" +\n\t\t\t\t\"宇宙羽泰铭安鹤纤\"\n\t\t\t\t,\n\t\t\t\t// 填充符，确保不在字符集中\n\t\t\t\t'口'\n\t\t);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-serializer-features/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForSerializerFeatures"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>sa-token-sign</name>\n    <artifactId>sa-token-sign</artifactId>\n    <description>sa-token Sign</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSign.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.sign.annotation.handle.SaCheckSignHandler;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\n\n/**\n * SaToken 插件安装：API 参数签名 组件\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaTokenPluginForSign implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        // 安装 API 参数签名 鉴权注解\n        SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckSignHandler());\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/SaSignManager.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.template.SaSignTemplate;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 管理 Sa-Token API 参数签名 所有全局组件\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSignManager {\n\n\t/**\n\t * API 参数签名 配置 Bean\n\t */\n\tprivate static volatile SaSignConfig config;\n\tpublic static SaSignConfig getConfig() {\n\t\tif (config == null) {\n\t\t\t// 初始化默认值\n\t\t\tsynchronized (SaSignManager.class) {\n\t\t\t\tif (config == null) {\n\t\t\t\t\tsetConfig(new SaSignConfig());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn config;\n\t}\n\tpublic static void setConfig(SaSignConfig config) {\n\t\tSaSignManager.config = config;\n\t}\n\n\t/**\n\t * API 签名配置 多实例 配置 Bean\n\t */\n\tprivate static volatile Map<String, SaSignConfig> signMany;\n\tpublic static Map<String, SaSignConfig> getSignMany() {\n\t\tif (signMany == null) {\n\t\t\t// 初始化默认值\n\t\t\tsynchronized (SaSignManager.class) {\n\t\t\t\tif (signMany == null) {\n\t\t\t\t\tsetSignMany(new LinkedHashMap<>());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn signMany;\n\t}\n\tpublic static void setSignMany(Map<String, SaSignConfig> signMany) {\n\t\tSaSignManager.signMany = signMany;\n\t}\n\n\t/**\n\t * API 参数签名\n\t */\n\tprivate volatile static SaSignTemplate saSignTemplate;\n\tpublic static void setSaSignTemplate(SaSignTemplate saSignTemplate) {\n\t\tSaSignManager.saSignTemplate = saSignTemplate;\n\t\tSaTokenEventCenter.doRegisterComponent(\"SaSignTemplate\", saSignTemplate);\n\t}\n\tpublic static SaSignTemplate getSaSignTemplate() {\n\t\tif (saSignTemplate == null) {\n\t\t\tsynchronized (SaManager.class) {\n\t\t\t\tif (saSignTemplate == null) {\n\t\t\t\t\tSaSignManager.saSignTemplate = new SaSignTemplate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn saSignTemplate;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/SaCheckSign.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * API 参数签名校验：必须具有正确的参数签名才可以通过校验\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.41.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD,ElementType.TYPE})\npublic @interface SaCheckSign {\n\n\t/**\n\t * 多实例下的 appid 值，用于区分不同的实例，如不填写则代表使用全局默认实例 <br/>\n\t * 允许以 #{} 的形式指定为请求参数，如：#{appid}\n\t *\n\t * @return /\n\t */\n\tString appid() default \"\";\n\n\t/**\n\t * 指定参与签名的参数有哪些，如果不填写则默认为全部参数\n\t *\n\t * @return /\n\t */\n\tString [] verifyParams() default {};\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/annotation/handle/SaCheckSignHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.annotation.handle;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.sign.annotation.SaCheckSign;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.sign.template.SaSignMany;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * 注解 SaCheckSign 的处理器\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaCheckSignHandler implements SaAnnotationHandlerInterface<SaCheckSign> {\n\n    @Override\n    public Class<SaCheckSign> getHandlerAnnotationClass() {\n        return SaCheckSign.class;\n    }\n\n    @Override\n    public void checkMethod(SaCheckSign at, AnnotatedElement element) {\n        _checkMethod(at.appid(), at.verifyParams());\n    }\n\n    public static void _checkMethod(String appid, String[] verifyParams) {\n        SaRequest req = SaHolder.getRequest();\n        // 如果 appid 为 #{} 格式，则从请求参数中获取\n        if(appid.startsWith(\"#{\") && appid.endsWith(\"}\")) {\n            String reqParamName = appid.substring(2, appid.length() - 1);\n            appid = req.getParam(reqParamName);\n        }\n        SaSignMany.getSignTemplate(appid).checkRequest(req, verifyParams);\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.config;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.fun.SaParamRetFunction;\nimport cn.dev33.satoken.secure.SaSecureUtil;\n\n/**\n * Sa-Token API 接口签名/验签 相关配置类\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaSignConfig {\n\n    /**\n     * API 调用签名秘钥\n     */\n    private String secretKey;\n\n    /**\n     * 接口调用时的时间戳允许的差距（单位：ms），-1 代表不校验差距，默认15分钟\n     *\n     * <p> 比如此处你配置了60秒，当一个请求从 client 发起后，如果 server 端60秒内没有处理，60秒后再想处理就无法校验通过了。</p>\n     * <p> timestamp + nonce 有效防止重放攻击。 </p>\n     */\n    private long timestampDisparity = 1000  * 60 * 15;\n\n    /**\n     * 对 fullStr 的摘要算法\n     */\n    private String digestAlgo = \"md5\";\n\n    public SaSignConfig() {\n    }\n\n    /**\n     * 构造函数\n     * @param secretKey 秘钥\n     */\n    public SaSignConfig(String secretKey) {\n        this.secretKey = secretKey;\n    }\n\n\n    // -------------- 扩展方法\n\n    /**\n     * 计算保存 nonce 时应该使用的 ttl，单位：秒\n     * @return /\n     */\n    public long getSaveNonceExpire() {\n        // 如果 timestampDisparity >= 0，则 nonceTtl 的值等于 timestampDisparity 的值，单位转秒\n        if(timestampDisparity >= 0) {\n            return timestampDisparity / 1000;\n        }\n        // 否则，nonceTtl 的值为 24 小时\n        else {\n            return 60 * 60 * 24;\n        }\n    }\n\n    /**\n     * 复制对象\n     * @return /\n     */\n    public SaSignConfig copy()  {\n        SaSignConfig obj = new SaSignConfig();\n        obj.secretKey = this.secretKey;\n        obj.timestampDisparity = this.timestampDisparity;\n        obj.digestAlgo = this.digestAlgo;\n        obj.digestMethod = this.digestMethod;\n        return obj;\n    }\n\n\n    // -------------- 策略函数\n\n    /**\n     * 对 fullStr 的摘要算法函数\n     */\n    public SaParamRetFunction<String, String> digestMethod = (fullStr) -> {\n        // md5\n        if(digestAlgo.equalsIgnoreCase(\"md5\")) {\n            return SaSecureUtil.md5(fullStr);\n        }\n        // sha1\n        if(digestAlgo.equalsIgnoreCase(\"sha1\")) {\n            return SaSecureUtil.sha1(fullStr);\n        }\n        // sha256\n        if(digestAlgo.equalsIgnoreCase(\"sha256\")) {\n            return SaSecureUtil.sha256(fullStr);\n        }\n        // sha384\n        if(digestAlgo.equalsIgnoreCase(\"sha384\")) {\n            return SaSecureUtil.sha384(fullStr);\n        }\n        // sha512\n        if(digestAlgo.equalsIgnoreCase(\"sha512\")) {\n            return SaSecureUtil.sha512(fullStr);\n        }\n        // 未知\n        throw new SaTokenException(\"不支持的摘要算法：\" + digestAlgo + \"，你可以自定义摘要算法函数实现\");\n    };\n\n    /**\n     * 设置: 对 fullStr 的摘要算法函数\n     *\n     * @param digestMethod /\n     * @return 对象自身\n     */\n    public SaSignConfig setDigestMethod(SaParamRetFunction<String, String> digestMethod) {\n        this.digestMethod = digestMethod;\n        return this;\n    }\n\n\n\n    // -------------- get/set\n\n    /**\n     * 获取 API 调用签名秘钥\n     *\n     * @return /\n     */\n    public String getSecretKey() {\n        return this.secretKey;\n    }\n\n    /**\n     * 设置 API 调用签名秘钥\n     *\n     * @param secretKey /\n     * @return 对象自身\n     */\n    public SaSignConfig setSecretKey(String secretKey) {\n        this.secretKey = secretKey;\n        return this;\n    }\n\n    /**\n     * 获取 接口调用时的时间戳允许的差距（单位：ms），-1 代表不校验差距，默认15分钟\n     *\n     * <p> 比如此处你配置了60秒，当一个请求从 client 发起后，如果 server 端60秒内没有处理，60秒后再想处理就无法校验通过了。</p>\n     * <p> timestamp + nonce 有效防止重放攻击。 </p>\n     *\n     * @return /\n     */\n    public long getTimestampDisparity() {\n        return this.timestampDisparity;\n    }\n\n    /**\n     * 设置 接口调用时的时间戳允许的差距（单位：ms），-1 代表不校验差距，默认15分钟\n     *\n     * <p> 比如此处你配置了60秒，当一个请求从 client 发起后，如果 server 端60秒内没有处理，60秒后再想处理就无法校验通过了。</p>\n     * <p> timestamp + nonce 有效防止重放攻击。 </p>\n     *\n     * @param timestampDisparity /\n     * @return 对象自身\n     */\n    public SaSignConfig setTimestampDisparity(long timestampDisparity) {\n        this.timestampDisparity = timestampDisparity;\n        return this;\n    }\n\n    /**\n     * 获取 对 fullStr 的摘要算法\n     *\n     * @return digestAlgo 对 fullStr 的摘要算法\n     */\n    public String getDigestAlgo() {\n        return this.digestAlgo;\n    }\n\n    /**\n     * 设置 对 fullStr 的摘要算法\n     * @param digestAlgo /\n     * @return /\n     */\n    public SaSignConfig setDigestAlgo(String digestAlgo) {\n        this.digestAlgo = digestAlgo;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"SaSignConfig [\"\n                + \"secretKey=\" + secretKey\n                + \", timestampDisparity=\" + timestampDisparity\n                + \"]\";\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/config/SaSignManyConfigWrapper.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.config;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * SaSignManyConfig 配置包装类，以更方便框架完成属性注入操作\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSignManyConfigWrapper {\n\n    public Map<String, SaSignConfig> signMany = new LinkedHashMap<>();\n\n    /**\n     * 获取\n     *\n     * @return signMany\n     */\n    public Map<String, SaSignConfig> getSignMany() {\n        return this.signMany;\n    }\n\n    /**\n     * 设置\n     *\n     * @param signMany\n     */\n    public void setSignMany(Map<String, SaSignConfig> signMany) {\n        this.signMany = signMany;\n    }\n\n    @Override\n    public String toString() {\n        return \"SaSignManyConfigWrapper{\" +\n                \"signMany=\" + signMany +\n                '}';\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/error/SaSignErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.error;\n\n/**\n * 定义 sa-token-sign 模块所有异常细分状态码\n * \n * @author click33\n * @since 1.43.0\n */\npublic interface SaSignErrorCode {\n\n\t/** 参与参数签名的秘钥不可为空 */\n\tint CODE_12201 = 12201;\n\n\t/** 给定的签名无效 */\n\tint CODE_12202 = 12202;\n\n\t/** timestamp 超出允许的范围 */\n\tint CODE_12203 = 12203;\n\n\t/** 未找到对应 appid 的 SaSignConfig */\n\tint CODE_12211 = 12211;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/exception/SaSignException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.exception;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 一个异常：代表 API 参数签名校验失败\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaSignException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130144L;\n\n\t/**\n\t * 一个异常：代表 API 参数签名校验失败\n\t * @param message 异常描述\n\t */\n\tpublic SaSignException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 断言 flag 不为 true，否则抛出 message 异常\n\t * @param flag 表达式\n\t * @param message 异常信息\n\t */\n\tpublic static void notTrue(boolean flag, String message) {\n\t\t// notTrue\n\t\tif(flag) {\n\t\t\tthrow new SaSignException(message);\n\t\t}\n\t}\n\n\t/**\n\t * 断言 value 不为空，否则抛出 message 异常\n\t * @param value 值\n\t * @param message 异常信息\n\t */\n\tpublic static void notEmpty(Object value, String message) {\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\tthrow new SaSignException(message);\n\t\t}\n\t}\n\n\n\t// ------------------- 已过期 -------------------\n\n\t/**\n\t * 如果flag==true，则抛出message异常\n\t * <h2>已过期：请使用 notTrue 代替，用法不变</h2>\n\t *\n\t * @param flag 标记\n\t * @param message 异常信息\n\t */\n\t@Deprecated\n\tpublic static void throwBy(boolean flag, String message) {\n\t\tif(flag) {\n\t\t\tthrow new SaSignException(message);\n\t\t}\n\t}\n\n\t/**\n\t * 如果 value isEmpty，则抛出 message 异常\n\t * <h2>已过期：请使用 notEmpty 代替，用法不变</h2>\n\t *\n\t * @param value 值\n\t * @param message 异常信息\n\t */\n\t@Deprecated\n\tpublic static void throwByNull(Object value, String message) {\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\tthrow new SaSignException(message);\n\t\t}\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignMany.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.template;\n\nimport cn.dev33.satoken.fun.SaParamRetFunction;\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.error.SaSignErrorCode;\nimport cn.dev33.satoken.sign.exception.SaSignException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * API 参数签名算法 - 多实例总控类\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaSignMany {\n\n\t/**\n\t * 根据 appid 获取 SaSignConfig，允许自定义\n\t */\n\tpublic static SaParamRetFunction<String, SaSignConfig> findSaSignConfigMethod = (appid) -> {\n\t\treturn SaSignManager.getSignMany().get(appid);\n\t};\n\n\t/**\n\t * 获取 SaSignTemplate，根据 appid\n\t * @param appid /\n\t * @return /\n\t */\n\tpublic static SaSignTemplate getSignTemplate(String appid) {\n\n\t\t// appid 为空，返回全局默认 SaSignTemplate\n\t\tif(SaFoxUtil.isEmpty(appid)){\n\t\t\treturn SaSignManager.getSaSignTemplate();\n\t\t}\n\n\t\t// 获取 SaSignConfig\n\t\tSaSignConfig config = findSaSignConfigMethod.run(appid);\n\t\tif(config == null){\n\t\t\tthrow new SaSignException(\"未找到签名配置，appid=\" + appid).setCode(SaSignErrorCode.CODE_12211);\n\t\t}\n\n\t\t// 创建 SaSignTemplate 并返回\n\t\treturn new SaSignTemplate(config);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.template;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.sign.error.SaSignErrorCode;\nimport cn.dev33.satoken.sign.exception.SaSignException;\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.Map;\nimport java.util.TreeMap;\n\nimport static cn.dev33.satoken.SaManager.log;\n\n/**\n * API 参数签名算法，在跨系统接口调用时防参数篡改、防重放攻击。\n *\n * <p>\n *     以 SSO 数据拉取为例，流程大致如下：\n *     <br> 1. 以 md5( loginId={账号id}8nonce={随机字符串}8timestamp={13位时间戳}8key={secretkey秘钥} ) 生成签名 sign。\n *     <br> 2. 将 sign 作为参数，拼接到请求地址后面，如：http://xxx.com?loginId=100018nonce=xxx8timestamp=xxx8sign=xxx。\n *     <br> 3. 服务端接收到请求后，以同样的算法生成一次 sign 。\n *     <br> 4. 对比两次 sign 是否一致，一致则通过，否则拒绝 。\n * </p>\n *\n * @author click33\n * @since 1.30.0\n */\npublic class SaSignTemplate {\n\n\tpublic SaSignTemplate() {\n\t}\n\n\t/**\n\t * 构造函数\n\t * @param signConfig 签名参数配置对象\n\t */\n\tpublic SaSignTemplate(SaSignConfig signConfig) {\n\t\tthis.signConfig = signConfig;\n\t}\n\n\t// ----------- 签名配置\n\n\tSaSignConfig signConfig;\n\n\t/**\n\t * 获取：API 签名配置\n\t * @return /\n\t */\n\tpublic SaSignConfig getSignConfig() {\n\t\treturn signConfig;\n\t}\n\n\t/**\n\t * 获取：API 签名配置：\n\t * \t1. 如果用户自定义了 signConfig ，则使用用户自定义的。\n\t * \t2. 否则使用全局默认配置。\n\t *\n\t * @return /\n\t */\n\tpublic SaSignConfig getSignConfigOrGlobal() {\n\t\t// 如果用户自定义了 signConfig ，则使用用户自定义的\n\t\tif(signConfig != null) {\n\t\t\treturn signConfig;\n\t\t}\n\t\t// 否则使用全局默认配置\n\t\treturn SaSignManager.getConfig();\n\t}\n\n\t/**\n\t * 获取：API 签名配置的秘钥\n\t * @return /\n\t */\n\tpublic String getSecretKey() {\n\t\treturn getSignConfigOrGlobal().getSecretKey();\n\t}\n\n\t/**\n\t * 设置：API 签名配置\n\t * @param signConfig /\n\t */\n\tpublic SaSignTemplate setSignConfig(SaSignConfig signConfig) {\n\t\tthis.signConfig = signConfig;\n\t\treturn this;\n\t}\n\n\n\t// ----------- 自定义使用的参数名称 (不声明final，允许开发者自定义修改)\n\n\tpublic static String key = \"key\";\n\tpublic static String timestamp = \"timestamp\";\n\tpublic static String nonce = \"nonce\";\n\tpublic static String sign = \"sign\";\n\n\n\t// ----------- 拼接参数\n\n\t/**\n\t * 将所有参数连接成一个字符串(不排序)，形如：b=28a=18c=3\n\t * @param paramsMap 参数列表\n\t * @return 拼接出的参数字符串 \n\t */\n\tpublic String joinParams(Map<String, ?> paramsMap) {\n\t\t\n\t\t// 按照 k1=v1&k2=v2&k3=v3 排列 \n        StringBuilder sb = new StringBuilder();\n        for (String key : paramsMap.keySet()) {\n        \tObject value = paramsMap.get(key);\n        \tif( ! SaFoxUtil.isEmpty(value) ) {\n        \t\tsb.append(key).append(\"=\").append(value).append(\"&\");\n        \t}\n        }\n        \n        // 删除最后一位 & \n        if(sb.length() > 0) {\n        \tsb.deleteCharAt(sb.length() - 1);\n        }\n        \n        // .\n        return sb.toString();\n\t}\n\n\t/**\n\t * 将所有参数按照字典顺序连接成一个字符串，形如：a=18b=28c=3\n\t * @param paramsMap 参数列表\n\t * @return 拼接出的参数字符串 \n\t */\n\tpublic String joinParamsDictSort(Map<String, ?> paramsMap) {\n\t\t// 保证字段按照字典顺序排列 \n\t\tif( ! (paramsMap instanceof TreeMap) ) {\n\t\t\tparamsMap = new TreeMap<>(paramsMap);\n\t\t}\n\t\t\n\t\t// 拼接 \n        return joinParams(paramsMap);\n\t}\n\n\n\t// ----------- 创建签名\n\n\t/**\n\t * 创建签名：md5(paramsStr + keyStr)\n\t * @param paramsMap 参数列表\n\t * @return 签名 \n\t */\n\tpublic String createSign(Map<String, ?> paramsMap) {\n\t\tString secretKey = getSecretKey();\n\t\tSaSignException.notEmpty(secretKey, \"参与参数签名的秘钥不可为空\", SaSignErrorCode.CODE_12201);\n\n\t\t// 如果调用者不小心传入了 sign 参数，则此处需要将 sign 参数排除在外\n\t\tif(paramsMap.containsKey(sign)) {\n\t\t\t// 为了保证不影响原有的 paramsMap，此处需要再复制一份\n\t\t\tparamsMap = new TreeMap<>(paramsMap);\n\t\t\tparamsMap.remove(sign);\n\t\t}\n\n\t\t// 计算签名\n\t\tString paramsStr = joinParamsDictSort(paramsMap);\n\t\tString fullStr = paramsStr + \"&\" + key + \"=\" + secretKey;\n\t\tString signStr = digestFullStr(fullStr);\n\n\t\t// 输入日志，方便调试\n\t\tlog.debug(\"fullStr：{}\", fullStr);\n\t\tlog.debug(\"signStr：{}\", signStr);\n\n\t\t// 返回\n\t\treturn signStr;\n\t}\n\n\t/**\n\t * 使用摘要算法创建签名\n\t * @param fullStr 待摘要的字符串\n\t * @return 签名\n\t */\n\tpublic String digestFullStr(String fullStr) {\n\t\treturn getSignConfigOrGlobal().digestMethod.run(fullStr);\n\t}\n\n\t/**\n\t * 给 paramsMap 追加 timestamp、nonce、sign 三个参数 \n\t * @param paramsMap 参数列表\n\t * @return 加工后的参数列表 \n\t */\n\tpublic Map<String, Object> addSignParams(Map<String, Object> paramsMap) {\n\t\tparamsMap.put(timestamp, String.valueOf(System.currentTimeMillis()));\n\t\tparamsMap.put(nonce, SaFoxUtil.getRandomString(32));\n\t\tparamsMap.put(sign, createSign(paramsMap));\n\t\treturn paramsMap;\n\t}\n\n\t/**\n\t * 给 paramsMap 追加 timestamp、nonce、sign 三个参数，并转换为参数字符串，形如：\n\t * <code>data=xxx8nonce=xxx8timestamp=xxx8sign=xxx</code>\n\t * @param paramsMap 参数列表\n\t * @return 加工后的参数列表 转化为的参数字符串\n\t */\n\tpublic String addSignParamsAndJoin(Map<String, Object> paramsMap) {\n\t\t// 追加参数\n\t\tparamsMap = addSignParams(paramsMap);\n\n\t\t// 拼接参数\n\t\treturn joinParams(paramsMap);\n\t}\n\n\n\t// ----------- 校验签名\n\n\t/**\n\t * 判断：指定时间戳与当前时间戳的差距是否在允许的范围内\n\t * @param timestamp 待校验的时间戳\n\t * @return 是否在允许的范围内\n\t */\n\tpublic boolean isValidTimestamp(long timestamp) {\n\t\tlong allowDisparity = getSignConfigOrGlobal().getTimestampDisparity();\n\t\tlong disparity = Math.abs(System.currentTimeMillis() - timestamp);\n\t\treturn allowDisparity == -1 || disparity <= allowDisparity;\n\t}\n\n\t/**\n\t * 校验：指定时间戳与当前时间戳的差距是否在允许的范围内，如果超出则抛出异常\n\t * @param timestamp 待校验的时间戳\n\t */\n\tpublic void checkTimestamp(long timestamp) {\n\t\tif( ! isValidTimestamp(timestamp) ) {\n\t\t\tthrow new SaSignException(\"timestamp 超出允许的范围：\" + timestamp).setCode(SaSignErrorCode.CODE_12203);\n\t\t}\n\t}\n\n\t/**\n\t * 判断：随机字符串 nonce 是否有效。\n\t * \t\t注意：同一 nonce 可以被多次判断有效，不会被缓存\n\t * @param nonce 待判断的随机字符串\n\t * @return 是否有效\n\t */\n\tpublic boolean isValidNonce(String nonce) {\n\t\t// 为空代表无效\n\t\tif(SaFoxUtil.isEmpty(nonce)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 校验此 nonce 是否已被使用过\n\t\tString key = splicingNonceSaveKey(nonce);\n\t\treturn SaManager.getSaTokenDao().get(key) == null;\n\t}\n\n\t/**\n\t * 校验：随机字符串 nonce 是否有效，如果无效则抛出异常。\n\t * \t\t注意：同一 nonce 只可以被校验通过一次，校验后将保存在缓存中，再次校验将无法通过\n\t * @param nonce 待校验的随机字符串\n\t */\n\tpublic void checkNonce(String nonce) {\n\t\t// 为空代表无效\n\t\tif(SaFoxUtil.isEmpty(nonce)) {\n\t\t\tthrow new SaSignException(\"nonce 为空，无效\");\n\t\t}\n\n\t\t// 校验此 nonce 是否已被使用过\n\t\tString key = splicingNonceSaveKey(nonce);\n\t\tif(SaManager.getSaTokenDao().get(key) != null) {\n\t\t\tthrow new SaSignException(\"此 nonce 已被使用过，不可重复使用：\" + nonce);\n\t\t}\n\n\t\t// 校验通过后，将此 nonce 保存在缓存中，保证下次校验无法通过\n\t\tSaManager.getSaTokenDao().set(key, nonce, getSignConfigOrGlobal().getSaveNonceExpire() * 2 + 2);\n\t}\n\n\t/**\n\t * 判断：给定的参数 生成的签名是否为有效签名\n\t * @param paramsMap 参数列表\n\t * @param sign 待验证的签名\n\t * @return 签名是否有效\n\t */\n\tpublic boolean isValidSign(Map<String, ?> paramsMap, String sign) {\n\t\tString theSign = createSign(paramsMap);\n\t\treturn theSign.equals(sign);\n\t}\n\n\t/**\n\t * 校验：给定的参数 生成的签名是否为有效签名，如果签名无效则抛出异常\n\t * @param paramsMap 参数列表\n\t * @param sign 待验证的签名\n\t */\n\tpublic void checkSign(Map<String, ?> paramsMap, String sign) {\n\t\tif( ! isValidSign(paramsMap, sign) )  {\n\t\t\tthrow new SaSignException(\"无效签名：\" + sign).setCode(SaSignErrorCode.CODE_12202);\n\t\t}\n\t}\n\n\t/**\n\t * 判断：参数列表中的 nonce、timestamp、sign 是否均为合法的\n\t * @param paramMap 待校验的请求参数集合\n\t * @return 是否合法\n\t */\n\t@SuppressWarnings(\"all\")\n\tpublic boolean isValidParamMap(Map<String, String> paramMap) {\n\t\t// 获取必须的三个参数\n\t\tString timestampValue = paramMap.get(timestamp);\n\t\tString nonceValue = paramMap.get(nonce);\n\t\tString signValue = paramMap.get(sign);\n\n\t\t// 参数非空校验\n\t\t// 配置isCheckNonce=false时，可以不传 nonce\n\t\tif(SaFoxUtil.isEmpty(timestampValue) || SaFoxUtil.isEmpty(signValue)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// 三个值的校验必须全部通过\n\t\treturn isValidTimestamp(Long.parseLong(timestampValue))\n\t\t\t\t&& isValidNonce(nonceValue)\n\t\t\t\t&& isValidSign(paramMap, signValue);\n\t}\n\n\t/**\n\t * 校验：参数列表中的 nonce、timestamp、sign 是否均为合法的，如果不合法，则抛出对应的异常\n\t * @param paramMap 待校验的请求参数集合\n\t */\n\tpublic void checkParamMap(Map<String, String> paramMap) {\n\t\t// 获取必须的三个参数\n\t\tString timestampValue = paramMap.get(timestamp);\n\t\tString nonceValue = paramMap.get(nonce);\n\t\tString signValue = paramMap.get(sign);\n\n\t\t// 参数非空校验\n\t\tSaSignException.notEmpty(timestampValue, \"缺少 timestamp 字段\");\n\t\tSaSignException.notEmpty(nonceValue, \"缺少 nonce 字段\");\n\t\tSaSignException.notEmpty(signValue, \"缺少 sign 字段\");\n\n\t\t// 依次校验三个参数\n\t\tcheckTimestamp(Long.parseLong(timestampValue));\n\t\tcheckNonce(nonceValue);\n\t\tcheckSign(paramMap, signValue);\n\n\t\t// 通过 √\n\t}\n\n\n\t// ----------- Web 请求相关 封装\n\n\t/**\n\t * 判断：一个请求中的 nonce、timestamp、sign 是否均为合法的\n\t * @param request 待校验的请求对象\n\t * @param paramNames 指定参与签名的参数有哪些，如果不填写则默认为全部参数\n\t * @return 是否合法\n\t */\n\tpublic boolean isValidRequest(SaRequest request, String... paramNames) {\n\t\tif(paramNames.length == 0) {\n\t\t\treturn isValidParamMap(request.getParamMap());\n\t\t} else {\n\t\t\treturn isValidParamMap(takeRequestParam(request, paramNames));\n\t\t}\n\t}\n\n\t/**\n\t * 校验：一个请求的 nonce、timestamp、sign 是否均为合法的，如果不合法，则抛出对应的异常\n\t * @param request 待校验的请求对象\n\t * @param paramNames 指定参与签名的参数有哪些，如果不填写则默认为全部参数\n\t */\n\tpublic void checkRequest(SaRequest request, String... paramNames) {\n\t\tif (paramNames.length == 0) {\n\t\t\tcheckParamMap(request.getParamMap());\n\t\t} else {\n\t\t\tcheckParamMap(takeRequestParam(request, paramNames));\n\t\t}\n\t}\n\n\t/**\n\t * 从请求中提取指定的参数\n\t * @param request 请求对象\n\t * @param paramNames 指定的参数名称，不可为空，如果传入空数组则代表只拿 timestamp、nonce、sign 三个参数\n\t * @return 提取出的参数\n\t */\n\tprotected Map<String, String> takeRequestParam(SaRequest request, String [] paramNames) {\n\t\tMap<String, String> paramMap = new TreeMap<>();\n\n\t\t// 此三个参数是必须获取的\n\t\tparamMap.put(timestamp, request.getParam(timestamp));\n\t\tparamMap.put(nonce, request.getParam(nonce));\n\t\tparamMap.put(sign, request.getParam(sign));\n\n\t\t// 获取指定的参数\n\t\tfor (String paramName : paramNames) {\n\t\t\tparamMap.put(paramName, request.getParam(paramName));\n\t\t}\n\n\t\t// 返回\n\t\treturn paramMap;\n    }\n\n\t// ------------------- 返回相应key -------------------\n\n\t/**\n\t * 拼接key：存储 nonce 时使用的 key\n\t * @param nonce nonce 值\n\t * @return key\n\t */\n\tpublic String splicingNonceSaveKey(String nonce) {\n\t\treturn SaManager.getConfig().getTokenName() + \":sign:nonce:\" + nonce;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sign.template;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.sign.SaSignManager;\n\nimport java.util.Map;\n\n/**\n * API 参数签名算法 - 工具类\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaSignUtil {\n\n\t// ----------- 拼接参数\n\n\t/**\n\t * 将所有参数连接成一个字符串(不排序)，形如：b=28a=18c=3\n\t * @param paramsMap 参数列表\n\t * @return 拼接出的参数字符串 \n\t */\n\tpublic static String joinParams(Map<String, ?> paramsMap) {\n\t\treturn SaSignManager.getSaSignTemplate().joinParams(paramsMap);\n\t}\n\n\t/**\n\t * 将所有参数按照字典顺序连接成一个字符串，形如：a=18b=28c=3\n\t * @param paramsMap 参数列表\n\t * @return 拼接出的参数字符串 \n\t */\n\tpublic static String joinParamsDictSort(Map<String, ?> paramsMap) {\n\t\treturn SaSignManager.getSaSignTemplate().joinParamsDictSort(paramsMap);\n\t}\n\n\n\t// ----------- 创建签名\n\n\t/**\n\t * 创建签名：md5(paramsStr + keyStr)\n\t * @param paramsMap 参数列表\n\t * @return 签名 \n\t */\n\tpublic static String createSign(Map<String, ?> paramsMap) {\n\t\treturn SaSignManager.getSaSignTemplate().createSign(paramsMap);\n\t}\n\n\t/**\n\t * 给 paramsMap 追加 timestamp、nonce、sign 三个参数 \n\t * @param paramsMap 参数列表\n\t * @return 加工后的参数列表 \n\t */\n\tpublic static Map<String, Object> addSignParams(Map<String, Object> paramsMap) {\n\t\treturn SaSignManager.getSaSignTemplate().addSignParams(paramsMap);\n\t}\n\n\t/**\n\t * 给 paramsMap 追加 timestamp、nonce、sign 三个参数，并转换为参数字符串，形如：\n\t * <code>data=xxx8nonce=xxx8timestamp=xxx8sign=xxx</code>\n\t * @param paramsMap 参数列表\n\t * @return 加工后的参数列表 转化为的参数字符串\n\t */\n\tpublic static String addSignParamsAndJoin(Map<String, Object> paramsMap) {\n\t\treturn SaSignManager.getSaSignTemplate().addSignParamsAndJoin(paramsMap);\n\t}\n\n\n\t// ----------- 校验签名\n\n\t/**\n\t * 判断：指定时间戳与当前时间戳的差距是否在允许的范围内\n\t * @param timestamp 待校验的时间戳\n\t * @return 是否在允许的范围内\n\t */\n\tpublic static boolean isValidTimestamp(long timestamp) {\n\t\treturn SaSignManager.getSaSignTemplate().isValidTimestamp(timestamp);\n\t}\n\n\t/**\n\t * 校验：指定时间戳与当前时间戳的差距是否在允许的范围内，如果超出则抛出异常\n\t * @param timestamp 待校验的时间戳\n\t */\n\tpublic static void checkTimestamp(long timestamp) {\n\t\tSaSignManager.getSaSignTemplate().checkTimestamp(timestamp);\n\t}\n\n\t/**\n\t * 判断：随机字符串 nonce 是否有效。\n\t * \t\t注意：同一 nonce 可以被多次判断有效，不会被缓存\n\t * @param nonce 待判断的随机字符串\n\t * @return 是否有效\n\t */\n\tpublic static boolean isValidNonce(String nonce) {\n\t\treturn SaSignManager.getSaSignTemplate().isValidNonce(nonce);\n\t}\n\n\t/**\n\t * 校验：随机字符串 nonce 是否有效，如果无效则抛出异常。\n\t * \t\t注意：同一 nonce 只可以被校验通过一次，校验后将保存在缓存中，再次校验将无法通过\n\t * @param nonce 待校验的随机字符串\n\t */\n\tpublic static void checkNonce(String nonce) {\n\t\tSaSignManager.getSaSignTemplate().checkNonce(nonce);\n\t}\n\n\t/**\n\t * 判断：给定的参数 生成的签名是否为有效签名\n\t * @param paramsMap 参数列表\n\t * @param sign 待验证的签名\n\t * @return 签名是否有效\n\t */\n\tpublic static boolean isValidSign(Map<String, ?> paramsMap, String sign) {\n\t\treturn SaSignManager.getSaSignTemplate().isValidSign(paramsMap, sign);\n\t}\n\n\t/**\n\t * 校验：给定的参数 生成的签名是否为有效签名，如果签名无效则抛出异常\n\t * @param paramsMap 参数列表\n\t * @param sign 待验证的签名\n\t */\n\tpublic static void checkSign(Map<String, ?> paramsMap, String sign) {\n\t\tSaSignManager.getSaSignTemplate().checkSign(paramsMap, sign);\n\t}\n\n\t/**\n\t * 判断：参数列表中的 nonce、timestamp、sign 是否均为合法的\n\t * @param paramMap 待校验的请求参数集合\n\t * @return 是否合法\n\t */\n\tpublic static boolean isValidParamMap(Map<String, String> paramMap) {\n\t\treturn SaSignManager.getSaSignTemplate().isValidParamMap(paramMap);\n\t}\n\n\t/**\n\t * 校验：参数列表中的 nonce、timestamp、sign 是否均为合法的，如果不合法，则抛出对应的异常\n\t * @param paramMap 待校验的请求参数集合\n\t */\n\tpublic static void checkParamMap(Map<String, String> paramMap) {\n\t\tSaSignManager.getSaSignTemplate().checkParamMap(paramMap);\n\t}\n\n\n\t// ----------- Web 请求相关 封装\n\n\t/**\n\t * 判断：一个请求中的 nonce、timestamp、sign 是否均为合法的\n\t * @param request 待校验的请求对象\n\t * @param paramNames 指定参与签名的参数有哪些，如果不填写则默认为全部参数\n\t * @return 是否合法\n\t */\n\tpublic static boolean isValidRequest(SaRequest request, String... paramNames) {\n\t\treturn SaSignManager.getSaSignTemplate().isValidRequest(request, paramNames);\n\t}\n\n\t/**\n\t * 校验：一个请求的 nonce、timestamp、sign 是否均为合法的，如果不合法，则抛出对应的异常\n\t * @param request 待校验的请求对象\n\t * @param paramNames 指定参与签名的参数有哪些，如果不填写则默认为全部参数\n\t */\n\tpublic static void checkRequest(SaRequest request, String... paramNames) {\n\t\tSaSignManager.getSaSignTemplate().checkRequest(request, paramNames);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sign/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForSign"
  },
  {
    "path": "sa-token-plugin/sa-token-snack3/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>sa-token-snack3</name>\n    <artifactId>sa-token-snack3</artifactId>\n    <description>sa-token integrate Snack3</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.noear</groupId>\n            <artifactId>snack3</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack3.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.noear.snack.ONode;\nimport org.noear.snack.core.Feature;\n\n/**\n * JSON 转换器， Snack3 版实现\n *\n * @author click33\n * @author noear\n * @since 1.41.0\n */\npublic class SaJsonTemplateForSnack3 implements SaJsonTemplate {\n\n\t/**\n\t * 序列化：对象 -> json 字符串\n\t */\n\t@Override\n\tpublic String objectToJson(Object obj) {\n\t\tif (SaFoxUtil.isEmpty(obj)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ONode.loadObj(obj, Feature.WriteClassName, Feature.NotWriteRootClassName).toJson();\n\t}\n\n\t/**\n\t * 反序列化：json 字符串 → 对象\n\t */\n\t@Override\n\tpublic <T> T jsonToObject(String jsonStr, Class<T> type) {\n\t\tif (SaFoxUtil.isEmpty(jsonStr)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn ONode.deserialize(jsonStr, type);\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack3.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.json.SaJsonTemplateForSnack3;\nimport cn.dev33.satoken.session.SaSessionForSnack3Customized;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\n/**\n * SaToken 插件安装：JSON 转换器 - Snack3 版\n *\n * @author click33\n * @author noear\n * @since 1.41.0\n */\npublic class SaTokenPluginForSnack3 implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n\n        // 设置JSON转换器：Snack3 版\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack3());\n\n        // 重写 SaSession 生成策略\n        SaStrategy.instance.createSession = SaSessionForSnack3Customized::new;\n\n        // 指定 SaSession 类型\n        SaStrategy.instance.sessionClassType = SaSessionForSnack3Customized.class;\n\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-snack3/src/main/java/cn/dev33/satoken/session/SaSessionForSnack3Customized.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.noear.snack.ONode;\n\n/**\n * Fastjson 定制版 SaSession，重写类型转换API\n * \n * @author click33\n * @author noear\n * @since 1.34.0\n */\npublic class SaSessionForSnack3Customized extends SaSession {\n\n\tprivate static final long serialVersionUID = -7600983549653130681L;\n\n\t/**\n\t * 构建一个 SaSession 对象\n\t */\n\tpublic SaSessionForSnack3Customized() {\n\t\tsuper();\n\t}\n\n\t/**\n\t * 构建一个 SaSession 对象\n\t *\n\t * @param id Session 的 id\n\t */\n\tpublic SaSessionForSnack3Customized(String id) {\n\t\tsuper(id);\n\t}\n\n\t/**\n\t * 取值 (指定转换类型)\n\t *\n\t * @param <T> 泛型\n\t * @param key key\n\t * @param cs  指定转换类型\n\t * @return 值\n\t */\n\t@Override\n\tpublic <T> T getModel(String key, Class<T> cs) {\n\t\t// 如果是想取出为基础类型\n\t\tObject value = get(key);\n\t\tif (SaFoxUtil.isBasicType(cs)) {\n\t\t\treturn SaFoxUtil.getValueByType(value, cs);\n\t\t}\n\t\t// 为空提前返回\n\t\tif (valueIsNull(value)) {\n\t\t\treturn null;\n\t\t}\n\t\t// 如果是 JSONObject 类型直接转，否则先转为 String 再转\n\t\tif (value instanceof ONode) {\n\t\t\tONode jo = (ONode) value;\n\t\t\treturn jo.toObject(cs);\n\t\t} else if (value instanceof String) {\n\t\t\treturn ONode.deserialize((String) value, cs);\n\t\t} else {\n\t\t\t//有可能是 Map\n\t\t\treturn ONode.load(value).toObject(cs);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-snack3/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForSnack3"
  },
  {
    "path": "sa-token-plugin/sa-token-snack4/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-plugin</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <name>sa-token-snack4</name>\n    <artifactId>sa-token-snack4</artifactId>\n    <description>sa-token integrate Snack4</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.noear</groupId>\n            <artifactId>snack4</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/json/SaJsonTemplateForSnack4.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.json;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.noear.snack4.ONode;\nimport org.noear.snack4.Feature;\nimport org.noear.snack4.Options;\n\n/**\n * JSON 转换器， Snack3 版实现\n *\n * @author click33\n * @author noear\n * @since 1.41.0\n */\npublic class SaJsonTemplateForSnack4 implements SaJsonTemplate {\n    private final Options options = Options.of(Feature.Write_ClassName, Feature.Write_NotRootClassName, Feature.Read_AutoType);\n\n    /**\n     * 序列化：对象 -> json 字符串\n     */\n    @Override\n    public String objectToJson(Object obj) {\n        if (SaFoxUtil.isEmpty(obj)) {\n            return null;\n        }\n        return ONode.ofBean(obj, options).toJson();\n    }\n\n    /**\n     * 反序列化：json 字符串 → 对象\n     */\n    @Override\n    public <T> T jsonToObject(String jsonStr, Class<T> type) {\n        if (SaFoxUtil.isEmpty(jsonStr)) {\n            return null;\n        }\n        return ONode.deserialize(jsonStr, type, options);\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForSnack4.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.json.SaJsonTemplateForSnack4;\nimport cn.dev33.satoken.session.SaSessionForSnack4Customized;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\n/**\n * SaToken 插件安装：JSON 转换器 - Snack3 版\n *\n * @author click33\n * @author noear\n * @since 1.41.0\n */\npublic class SaTokenPluginForSnack4 implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n\n        // 设置JSON转换器：Snack3 版\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack4());\n\n        // 重写 SaSession 生成策略\n        SaStrategy.instance.createSession = SaSessionForSnack4Customized::new;\n\n        // 指定 SaSession 类型\n        SaStrategy.instance.sessionClassType = SaSessionForSnack4Customized.class;\n\n    }\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-snack4/src/main/java/cn/dev33/satoken/session/SaSessionForSnack4Customized.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.session;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.noear.snack4.ONode;\n\n/**\n * Fastjson 定制版 SaSession，重写类型转换API\n * \n * @author click33\n * @author noear\n * @since 1.34.0\n */\npublic class SaSessionForSnack4Customized extends SaSession {\n\n    private static final long serialVersionUID = -7600983549653130681L;\n\n    /**\n     * 构建一个 SaSession 对象\n     */\n    public SaSessionForSnack4Customized() {\n        super();\n    }\n\n    /**\n     * 构建一个 SaSession 对象\n     *\n     * @param id Session 的 id\n     */\n    public SaSessionForSnack4Customized(String id) {\n        super(id);\n    }\n\n    /**\n     * 取值 (指定转换类型)\n     *\n     * @param <T> 泛型\n     * @param key key\n     * @param cs  指定转换类型\n     * @return 值\n     */\n    @Override\n    public <T> T getModel(String key, Class<T> cs) {\n        // 如果是想取出为基础类型\n        Object value = get(key);\n        if (SaFoxUtil.isBasicType(cs)) {\n            return SaFoxUtil.getValueByType(value, cs);\n        }\n        // 为空提前返回\n        if (valueIsNull(value)) {\n            return null;\n        }\n        // 如果是 JSONObject 类型直接转，否则先转为 String 再转\n        if (value instanceof ONode) {\n            ONode jo = (ONode) value;\n            return jo.toBean(cs);\n        } else if (value instanceof String) {\n            return ONode.deserialize((String) value, cs);\n        } else {\n            //有可能是 Map\n            return ONode.ofBean(value).toBean(cs);\n        }\n    }\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-snack4/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForSnack4"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-aop/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-aop</name>\n    <artifactId>sa-token-spring-aop</artifactId>\n\t<description>sa-token authentication by spring-aop</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- spring-boot-starter-aop -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAopPointcutAdvisorBeanRegister.java",
    "content": "package cn.dev33.satoken.aop;\n\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport org.springframework.aop.aspectj.AspectJExpressionPointcut;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Sa-Token AOP 环绕切入 Bean 注册\n * <p>\n *    参考资料：<br>\n *    https://www.jb51.net/program/297714rev.htm    <br>\n *    https://www.bilibili.com/video/BV1WZ421W7Qx   <br>\n *    https://blog.csdn.net/Tomwildboar/article/details/139199801   <br>\n * </p>\n *\n * @author click33\n * @since 2024/8/3\n */\n@Configuration\npublic class SaAopPointcutAdvisorBeanRegister {\n\n    /**\n     * Advisor 静态全局引用\n     */\n    public static SaAroundAnnotationPointcutAdvisor saAroundAnnoAdvisor;\n\n    @Bean\n    public SaAroundAnnotationPointcutAdvisor saAroundAnnotationHandlePointcutAdvisor (List<SaAnnotationHandlerInterface<?>> handlerList) {\n        SaAroundAnnotationPointcutAdvisor advisor = new SaAroundAnnotationPointcutAdvisor();\n\n        // 定义切入规则\n        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();\n        String expression = calcExpression(handlerList);\n        pointcut.setExpression(expression);\n        advisor.setPointcut(pointcut);\n\n        // 定义执行的方法 \n        advisor.setAdvice(new SaAroundAnnotationMethodInterceptor());\n\n        // 保存全局引用\n        SaAopPointcutAdvisorBeanRegister.saAroundAnnoAdvisor = advisor;\n        return advisor;\n    }\n\n    /**\n     * 计算切入表达式\n     * @param appendHandlerList 追加的 SaAnnotationAbstractHandler 处理器\n     * @return /\n     */\n    public static String calcExpression(List<SaAnnotationHandlerInterface<?>> appendHandlerList) {\n\n        // 框架内置的\n        List<Class<?>> list = new ArrayList<>(SaAnnotationStrategy.instance.annotationHandlerMap.keySet());\n\n        // 额外追加的\n        if(appendHandlerList != null) {\n            for (SaAnnotationHandlerInterface<?> handler : appendHandlerList) {\n                Class<?> cls = handler.getHandlerAnnotationClass();\n                if(!list.contains(cls)) {\n                    list.add(handler.getHandlerAnnotationClass());\n                }\n            }\n        }\n\n        // 计算\n        return calcClassListExpression(list);\n    }\n\n    /**\n     * 计算 class 列表的切入表达式，\n     * 最终样例形如：\n     <pre>\n         public static final String POINTCUT_SIGN =\n         \"@within(cn.dev33.satoken.annotation.SaCheckLogin) || @annotation(cn.dev33.satoken.annotation.SaCheckLogin) || \"\n         + \"@within(cn.dev33.satoken.annotation.SaCheckRole) || @annotation(cn.dev33.satoken.annotation.SaCheckRole) || \"\n         + \"@within(cn.dev33.satoken.annotation.SaCheckPermission) || @annotation(cn.dev33.satoken.annotation.SaCheckPermission)\";\n     </pre>\n     * @param list /\n     * @return /\n     */\n    public static String calcClassListExpression(List<Class<?>> list) {\n        String pointcutExpression = \"\";\n        for (Class<?> cls : list) {\n            if(!pointcutExpression.isEmpty()) {\n                pointcutExpression += \" || \";\n            }\n            pointcutExpression += \"@within(\" + cls.getName() + \") || @annotation(\" + cls.getName() + \")\";\n        }\n        return pointcutExpression;\n    }\n\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationMethodInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.aop;\n\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport org.aopalliance.intercept.MethodInterceptor;\nimport org.aopalliance.intercept.MethodInvocation;\n\nimport java.lang.reflect.Method;\n\n/**\n * Sa-Token 注解方法拦截器 AOP环绕切入\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaAroundAnnotationMethodInterceptor implements MethodInterceptor {\n\n    @Override\n    public Object invoke(MethodInvocation invocation) throws Throwable {\n        // 注解鉴权\n        try{\n            Method method = invocation.getMethod();\n            SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method);\n        } catch (StopMatchException ignored) {\n        }\n        // 执行原有防范\n        return invocation.proceed();\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-aop/src/main/java/cn/dev33/satoken/aop/SaAroundAnnotationPointcutAdvisor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.aop;\n\nimport org.springframework.aop.support.DefaultPointcutAdvisor;\n\n/**\n * Sa-Token 注解方法 Advisor AOP环绕切入\n *\n * @author click33\n * @since 1.39.0\n */\npublic class SaAroundAnnotationPointcutAdvisor extends DefaultPointcutAdvisor {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.aop.SaAopPointcutAdvisorBeanRegister"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-aop/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncn.dev33.satoken.aop.SaAopPointcutAdvisorBeanRegister"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-el/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-el</name>\n    <artifactId>sa-token-spring-el</artifactId>\n\t<description>sa-token authentication by spring-el</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- spring-boot-starter-aop -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/annotation/SaCheckEL.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 注解鉴权：根据 EL 表达式执行鉴权\n *\n * <p> 可标注在方法、类上（效果等同于标注在此类的所有方法上）\n *\n * @author click33\n * @since 1.40.0\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD, ElementType.TYPE })\npublic @interface SaCheckEL {\n\n    /**\n     * 需要执行的 EL 表达式\n     *\n     * @return /\n     */\n    String value() default \"\";\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELAspect.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.aop;\n\nimport cn.dev33.satoken.annotation.SaCheckEL;\nimport cn.dev33.satoken.annotation.SaIgnore;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport org.aspectj.lang.JoinPoint;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.aspectj.lang.annotation.Before;\nimport org.aspectj.lang.reflect.MethodSignature;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.BeanFactory;\nimport org.springframework.beans.factory.BeanFactoryAware;\nimport org.springframework.context.expression.BeanFactoryResolver;\nimport org.springframework.context.expression.MapAccessor;\nimport org.springframework.context.expression.MethodBasedEvaluationContext;\nimport org.springframework.core.DefaultParameterNameDiscoverer;\nimport org.springframework.core.ParameterNameDiscoverer;\nimport org.springframework.expression.ExpressionParser;\nimport org.springframework.expression.spel.standard.SpelExpressionParser;\nimport org.springframework.util.ObjectUtils;\n\nimport java.lang.reflect.Method;\n\n/**\n * Sa-Token 注解鉴权 EL 表达式 AOP 切入 (用于处理 @SaCheckEL 注解)\n *\n * @author click33\n * @since 1.40.0\n */\n@Aspect\npublic class SaCheckELAspect implements BeanFactoryAware {\n\n    /**\n     * 表达式解析器 (用于解析 EL 表达式)\n     */\n    private final ExpressionParser parser = new SpelExpressionParser();\n\n    /**\n     * 参数名发现器 (用于获取方法参数名)\n     */\n    private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();\n\n    /**\n     * Spring Bean 工厂 (用于解析 Spring 容器中的 Bean 对象)\n     */\n    private BeanFactory beanFactory;\n\n    @Override\n    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {\n        this.beanFactory = beanFactory;\n    }\n\n    /**\n     * 前置通知 (所有被 SaCheckEL 注解修饰的方法或类)\n     *\n     * @param joinPoint /\n     */\n    @Before(\"@within(cn.dev33.satoken.annotation.SaCheckEL) || @annotation(cn.dev33.satoken.annotation.SaCheckEL)\")\n    public void atBefore(JoinPoint joinPoint) {\n\n        // 获取方法签名与参数列表\n        MethodSignature signature = (MethodSignature) joinPoint.getSignature();\n        Method method = signature.getMethod();\n        Object[] args = joinPoint.getArgs();\n\n        // 如果标注了 @SaIgnore 注解，则跳过，代表不进行校验\n        if(SaAnnotationStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) {\n            return;\n        }\n\n        // 1、根数据对象构建\n        //      构建校验上下文根数据对象\n        SaCheckELRootMap rootMap = new SaCheckELRootMap(method, extractArgs(method, args), joinPoint.getTarget() );\n\n        //      添加 this 指针指向注解函数所在类，使之可以在表达式中通过 this.xx 访问类的属性和方法 (与Target一致，此处只是为了更加语义化)\n        rootMap.put(SaCheckELRootMap.KEY_THIS, joinPoint.getTarget());\n\n        //      添加全局默认的 StpLogic 对象，使之可以在表达式中通过 stp.checkLogin() 方式调用校验方法\n        rootMap.put(SaCheckELRootMap.KEY_STP, StpUtil.getStpLogic());\n\n        //      添加 JoinPoint 对象，使开发者在扩展时可以根据 JoinPoint 对象获取更多信息\n        rootMap.put(SaCheckELRootMap.KEY_JOIN_POINT, joinPoint);\n\n        //      执行开发者自定义的增强策略\n        SaAnnotationStrategy.instance.checkELRootMapExtendFunction.accept(rootMap);\n\n        // 2、表达式解析方案构建\n        //      创建表达式解析上下文\n        MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(rootMap, method, args, pnd);\n\n        //      添加属性访问器，使之可以解析 Map 对象的属性作为根上下文\n        context.addPropertyAccessor(new MapAccessor());\n\n        //      设置 Bean 解析器，使之可以在表达式中引用 Spring 容器管理的所有 Bean 对象\n        context.setBeanResolver(new BeanFactoryResolver(beanFactory));\n\n        // 3、开始校验\n        //      先校验 Method 所属 Class 上的注解表达式\n        SaCheckEL ofClass = (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method.getDeclaringClass(), SaCheckEL.class);\n        if (ofClass != null) {\n            parser.parseExpression(ofClass.value()).getValue(context);\n        }\n\n        //      再校验 Method 上的注解表达式\n        SaCheckEL ofMethod =  (SaCheckEL) SaAnnotationStrategy.instance.getAnnotation.apply(method, SaCheckEL.class);\n        if (ofMethod != null) {\n            parser.parseExpression(ofMethod.value()).getValue(context);\n        }\n\n    }\n\n    /**\n     * 如果是可变长参数，则展开并返回，否则原样返回\n     *\n     * @param method /\n     * @param args /\n     * @return /\n     */\n    private Object[] extractArgs(Method method, Object[] args) {\n        if (!method.isVarArgs()) {\n            return args;\n        } else {\n            Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);\n            Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];\n            System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);\n            System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);\n            return combinedArgs;\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-el/src/main/java/cn/dev33/satoken/aop/SaCheckELRootMap.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.aop;\n\nimport cn.dev33.satoken.error.SaErrorCode;\nimport cn.dev33.satoken.exception.SaTokenException;\n\nimport java.lang.reflect.Method;\nimport java.util.HashMap;\n\n/**\n * Sa-Token 注解鉴权 EL 表达式解析器的根数据对象\n *\n * @author click33\n * @since 1.40.0\n */\npublic class SaCheckELRootMap extends HashMap<String, Object> {\n\n    /**\n     * KEY标记：被切入的函数\n     */\n    public static final String KEY_METHOD = \"method\";\n\n    /**\n     * KEY标记：被切入的函数参数\n     */\n    public static final String KEY_ARGS = \"args\";\n\n    /**\n     * KEY标记：被切入的目标对象\n     */\n    public static final String KEY_TARGET = \"target\";\n\n    /**\n     * KEY标记：注解所在类对象引用\n     */\n    public static final String KEY_THIS = \"this\";\n\n    /**\n     * KEY标记：全局默认 StpLogic 对象\n     */\n    public static final String KEY_STP = \"stp\";\n\n    /**\n     * KEY标记：本次切入的 JoinPoint 对象\n     */\n    public static final String KEY_JOIN_POINT = \"joinPoint\";\n\n    public SaCheckELRootMap(Method method, Object[] args, Object target) {\n        this.put(KEY_METHOD, method);\n        this.put(KEY_ARGS, args);\n        this.put(KEY_TARGET, target);\n    }\n\n    /**\n     * 获取 被切入的函数\n     *\n     * @return method 被切入的函数\n     */\n    public Method getMethod() {\n        return (Method) this.get(KEY_METHOD);\n    }\n\n    /**\n     * 获取 被切入的函数参数\n     *\n     * @return args 被切入的函数参数\n     */\n    public Object[] getArgs() {\n        return (Object[]) this.get(KEY_ARGS);\n    }\n\n    /**\n     * 获取 被切入的目标对象\n     *\n     * @return target 被切入的目标对象\n     */\n    public Object getTarget() {\n        return this.get(KEY_TARGET);\n    }\n\n    /**\n     * 获取 注解所在类对象引用\n     *\n     * @return this 注解所在类对象引用\n     */\n    public Object getThis() {\n        return this.get(KEY_THIS);\n    }\n\n    /**\n     * 获取本次切入的 JoinPoint 对象\n     */\n    public Object getJoinPoint() {\n        return this.get(KEY_JOIN_POINT);\n    }\n\n    /**\n     * 断言函数, 表达式执行结果为true才能通过\n     *\n     * @param flag 执行结果\n     */\n    public void NEED(boolean flag) {\n        NEED(flag, SaErrorCode.CODE_UNDEFINED, \"未通过 EL 表达式校验\");\n    }\n\n    /**\n     * 断言函数, 表达式执行结果为true才能通过，并在未通过时抛出 SaTokenException 异常，异常描述信息为 errorMessage\n     *\n     * @param flag 执行结果\n     */\n    public void NEED(boolean flag, String errorMessage) {\n        NEED(flag, SaErrorCode.CODE_UNDEFINED, errorMessage);\n    }\n\n    /**\n     * 断言函数, 表达式执行结果为true才能通过，并在未通过时抛出 SaTokenException 异常，异常码为 errorCode，异常描述信息为 errorMessage\n     *\n     * @param flag 执行结果\n     */\n    public void NEED(boolean flag, int errorCode, String errorMessage) {\n        if(!flag) {\n            throw new SaTokenException(errorCode, errorMessage);\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.aop.SaCheckELAspect"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-el/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncn.dev33.satoken.aop.SaCheckELAspect"
  },
  {
    "path": "sa-token-plugin/sa-token-spring-el/src/main/resources/spel-extension.json",
    "content": "{\n  \"cn.dev33.satoken.annotation.SaCheckEL@value\": {\n    \"method\": {\n      \"parameters\": true,\n      \"parametersPrefix\": [\n        \"p\",\n        \"a\"\n      ]\n    },\n    \"fields\": {\n      \"root\": \"cn.dev33.satoken.aop.SaCheckELRootMap\",\n      \"stp\": \"cn.dev33.satoken.stp.StpLogic\"\n    }\n  }\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-sso</name>\n    <artifactId>sa-token-sso</artifactId>\n\t<description>sa-token realization sso</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <!-- sa-token-sign -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sign</artifactId>\n        </dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/SaSsoManager.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso;\n\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\n\n/**\n * Sa-Token-SSO 模块 总控类\n *\n * @author click33\n * @since 1.30.0\n */\npublic class SaSsoManager {\n\n\t/**\n\t * Sso Server 端 配置 Bean\n\t */\n\tprivate volatile static SaSsoServerConfig serverConfig;\n\tpublic static SaSsoServerConfig getServerConfig() {\n\t\tif (serverConfig == null) {\n\t\t\tsynchronized (SaSsoManager.class) {\n\t\t\t\tif (serverConfig == null) {\n\t\t\t\t\tsetServerConfig(new SaSsoServerConfig());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn serverConfig;\n\t}\n\tpublic static void setServerConfig(SaSsoServerConfig serverConfig) {\n\t\tSaSsoManager.serverConfig = serverConfig;\n\t\t// 如果配置了 is-check-sign=false，则打印一条警告日志\n\t\tif ( ! serverConfig.getIsCheckSign()) {\n\t\t\tprintNoCheckSignWarningByStartup();\n\t\t}\n\t}\n\n\t/**\n\t * Sso Client 端 配置 Bean\n\t */\n\tprivate volatile static SaSsoClientConfig clientConfig;\n\tpublic static SaSsoClientConfig getClientConfig() {\n\t\tif (clientConfig == null) {\n\t\t\tsynchronized (SaSsoManager.class) {\n\t\t\t\tif (clientConfig == null) {\n\t\t\t\t\tsetClientConfig(new SaSsoClientConfig());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn clientConfig;\n\t}\n\tpublic static void setClientConfig(SaSsoClientConfig clientConfig) {\n\t\tSaSsoManager.clientConfig = clientConfig;\n\t\t// 如果配置了 is-check-sign=false，则打印一条警告日志\n\t\tif ( ! clientConfig.getIsCheckSign()) {\n\t\t\tprintNoCheckSignWarningByStartup();\n\t\t}\n\t}\n\n\t// 在启动时检测到 sa-token.sso-[server/client].is-check-sign=false 时，输出警告信息\n\tpublic static void printNoCheckSignWarningByStartup() {\n\t\tSystem.err.println(\"-----------------------------------------------------------------------------\");\n\t\tSystem.err.println(\"警告信息：\");\n\t\tSystem.err.println(\"当前配置项 sa-token.sso-[server/client].is-check-sign=false 代表跳过 SSO 参数签名校验\");\n\t\tSystem.err.println(\"此模式仅为方便本地调试使用，生产环境下请务必配置为 true （配置项默认为true）\");\n\t\tSystem.err.println(\"-----------------------------------------------------------------------------\");\n\t}\n\n\t// 在运行时检测到 sa-token.sso-[server/client].is-check-sign=false 时，输出警告信息\n\tpublic static void printNoCheckSignWarningByRuntime() {\n\t\tSystem.err.println(\"警告信息：当前配置项 sa-token.sso-[server/client].is-check-sign=false 已跳过参数签名校验，\" +\n\t\t\t\t\"此模式仅为方便本地调试使用，生产环境下请务必配置为 true （配置项默认为true）\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.config;\n\n\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\n\n/**\n * Sa-Token SSO Client 端 配置类\n *\n * @author click33\n * @since 1.30.0\n */\npublic class SaSsoClientConfig implements Serializable {\n\n    private static final long serialVersionUID = -6541180061782004705L;\n\n    /**\n     * 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\n     */\n    public String mode = \"\";\n\n    /**\n     * 当前 Client 标识（非必填，不填时代表当前应用是一个匿名应用）\n     */\n    public String client;\n\n    /**\n     * 配置 SSO Server 端主机总地址\n     */\n    public String serverUrl;\n\n    /**\n     * 单独配置 Server 端：单点登录授权地址\n     */\n    public String authUrl = \"/sso/auth\";\n\n    /**\n     * 单独配置 Server 端：单点注销地址\n     */\n    public String signoutUrl = \"/sso/signout\";\n\n    /**\n     * 单独配置 Server 端：推送消息地址\n     */\n    public String pushUrl = \"/sso/pushS\";\n\n    /**\n     * 单独配置 Server 端：查询数据 getData 地址\n     */\n    public String getDataUrl = \"/sso/getData\";\n\n    /**\n     * 配置当前 Client 端的登录地址（为空时自动获取）\n     */\n    public String currSsoLogin;\n\n    /**\n     * 配置当前 Client 端的单点注销回调URL （为空时自动获取）\n     */\n    public String currSsoLogoutCall;\n\n    /**\n     * 是否打开模式三（此值为 true 时将使用 http 请求校验 ticket 值）\n     */\n    public Boolean isHttp = false;\n\n    /**\n     * 是否打开单点注销功能 (为 true 时，开放 /sso/logout 接口，以及接收单点注销回调消息推送)\n     */\n    public Boolean isSlo = true;\n\n    /**\n     * 是否注册单点登录注销回调 (为 true 时，登录时附带单点登录回调地址，并且开放 /sso/logoutCall 地址)\n     */\n    public Boolean regLogoutCall = false;\n\n    /**\n     * API 调用签名秘钥\n     */\n    public String secretKey;\n\n    /**\n     * 是否校验参数签名（为 false 时暂时关闭参数签名校验，此为方便本地调试用的一个配置项，生产环境请务必为true）\n     */\n    public Boolean isCheckSign = true;\n\n\n    // 额外添加的一些函数\n\n    /**\n     * @return 获取拼接 url：Server 端单点登录授权地址\n     */\n    public String splicingAuthUrl() {\n        return SaFoxUtil.spliceTwoUrl(getServerUrl(), getAuthUrl());\n    }\n\n    /**\n     * @return 获取拼接 url：Server 端查询数据 getData 地址\n     */\n    public String splicingGetDataUrl() {\n        return SaFoxUtil.spliceTwoUrl(getServerUrl(), getGetDataUrl());\n    }\n\n    /**\n     * @return 获取拼接 url：Server 端单点注销地址\n     */\n    public String splicingSignoutUrl() {\n        return SaFoxUtil.spliceTwoUrl(getServerUrl(), getSignoutUrl());\n    }\n\n    /**\n     * @return 获取拼接 url：单独配置 Server 端推送消息地址\n     */\n    public String splicingPushUrl() {\n        return SaFoxUtil.spliceTwoUrl(getServerUrl(), getPushUrl());\n    }\n\n\n    // get set\n\n    /**\n     * 获取 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\n     *\n     * @return /\n     */\n    public String getMode() {\n        return this.mode;\n    }\n\n    /**\n     * 设置 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\n     *\n     * @param mode /\n     */\n    public void setMode(String mode) {\n        this.mode = mode;\n    }\n\n    /**\n     * @return 是否打开单点注销功能  (为 true 时，开放 /sso/logout 接口，以及接收单点注销回调消息推送)\n     */\n    public Boolean getIsSlo() {\n        return isSlo;\n    }\n\n    /**\n     * @param isSlo 是否打开单点注销功能 (为 true 时，开放 /sso/logout 接口，以及接收单点注销回调消息推送)\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setIsSlo(Boolean isSlo) {\n        this.isSlo = isSlo;\n        return this;\n    }\n\n    /**\n     * @return isHttp 是否打开模式三（此值为 true 时将使用 http 请求校验 ticket 值）\n     */\n    public Boolean getIsHttp() {\n        return isHttp;\n    }\n\n    /**\n     * @param isHttp 是否打开模式三（此值为 true 时将使用 http 请求校验 ticket 值）\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setIsHttp(Boolean isHttp) {\n        this.isHttp = isHttp;\n        return this;\n    }\n\n    /**\n     * 当前 Client 标识（非必填，不填时代表当前应用是一个匿名应用）\n     *\n     * @return /\n     */\n    public String getClient() {\n        return client;\n    }\n\n    /**\n     * 当前 Client 标识（非必填，不填时代表当前应用是一个匿名应用）\n     *\n     * @param client /\n     */\n    public SaSsoClientConfig setClient(String client) {\n        this.client = client;\n        return this;\n    }\n\n    /**\n     * @return 单独配置 Server 端：单点登录授权地址\n     */\n    public String getAuthUrl() {\n        return authUrl;\n    }\n\n    /**\n     * @param authUrl 单独配置 Server 端：单点登录授权地址\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setAuthUrl(String authUrl) {\n        this.authUrl = authUrl;\n        return this;\n    }\n\n    /**\n     * @return 单独配置 Server 端：查询数据 getData 地址\n     */\n    public String getGetDataUrl() {\n        return getDataUrl;\n    }\n\n    /**\n     * @param getDataUrl 单独配置 Server 端：查询数据 getData 地址\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setGetDataUrl(String getDataUrl) {\n        this.getDataUrl = getDataUrl;\n        return this;\n    }\n\n    /**\n     * @return 单独配置 Server 端：单点注销地址\n     */\n    public String getSignoutUrl() {\n        return signoutUrl;\n    }\n\n    /**\n     * @param signoutUrl 单独配置 Server 端：单点注销地址\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setSignoutUrl(String signoutUrl) {\n        this.signoutUrl = signoutUrl;\n        return this;\n    }\n\n    /**\n     * 获取 单独配置 Server 端：推送消息地址\n     *\n     * @return /\n     */\n    public String getPushUrl() {\n        return this.pushUrl;\n    }\n\n    /**\n     * 设置 单独配置 Server 端：推送消息地址\n     *\n     * @param pushUrl /\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setPushUrl(String pushUrl) {\n        this.pushUrl = pushUrl;\n        return this;\n    }\n\n    /**\n     * @return 配置当前 Client 端的登录地址（为空时自动获取）\n     */\n    public String getCurrSsoLogin() {\n        return currSsoLogin;\n    }\n\n    /**\n     * @param currSsoLogin 配置当前 Client 端的登录地址（为空时自动获取）\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setCurrSsoLogin(String currSsoLogin) {\n        this.currSsoLogin = currSsoLogin;\n        return this;\n    }\n\n    /**\n     * @return 配置当前 Client 端的单点注销回调URL （为空时自动获取）\n     */\n    public String getCurrSsoLogoutCall() {\n        return currSsoLogoutCall;\n    }\n\n    /**\n     * @param currSsoLogoutCall 配置当前 Client 端的单点注销回调URL （为空时自动获取）\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setCurrSsoLogoutCall(String currSsoLogoutCall) {\n        this.currSsoLogoutCall = currSsoLogoutCall;\n        return this;\n    }\n\n    /**\n     * 配置 SSO Server 端主机总地址\n     *\n     * @return /\n     */\n    public String getServerUrl() {\n        return serverUrl;\n    }\n\n    /**\n     * 配置 SSO Server 端主机总地址\n     *\n     * @param serverUrl /\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setServerUrl(String serverUrl) {\n        this.serverUrl = serverUrl;\n        return this;\n    }\n\n    /**\n     * 获取 API 调用签名秘钥\n     *\n     * @return /\n     */\n    public String getSecretKey() {\n        return this.secretKey;\n    }\n\n    /**\n     * 设置 API 调用签名秘钥\n     *\n     * @param secretKey /\n     * @return 对象自身\n     */\n    public SaSsoClientConfig setSecretKey(String secretKey) {\n        this.secretKey = secretKey;\n        return this;\n    }\n\n    /**\n     * 获取 是否校验参数签名（为 false 时暂时关闭参数签名校验，此为方便本地调试用的一个配置项，生产环境请务必为true）\n     *\n     * @return isCheckSign 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\n     */\n    public Boolean getIsCheckSign() {\n        return this.isCheckSign;\n    }\n\n    /**\n     * 设置 是否校验参数签名（为 false 时暂时关闭参数签名校验，此为方便本地调试用的一个配置项，生产环境请务必为true）\n     *\n     * @param isCheckSign 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\n     */\n    public SaSsoClientConfig setIsCheckSign(Boolean isCheckSign) {\n        this.isCheckSign = isCheckSign;\n        return this;\n    }\n\n    /**\n     * 获取 是否注册单点登录注销回调 (为 true 时，登录时附带单点登录回调地址，并且开放 /sso/logoutCall 地址)\n     *\n     * @return /\n     */\n    public Boolean getRegLogoutCall() {\n        return this.regLogoutCall;\n    }\n\n    /**\n     * 设置 是否注册单点登录注销回调 (为 true 时，登录时附带单点登录回调地址，并且开放 /sso/logoutCall 地址)\n     *\n     * @param regLogoutCall /\n     * @return /\n     */\n    public SaSsoClientConfig setRegLogoutCall(Boolean regLogoutCall) {\n        this.regLogoutCall = regLogoutCall;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"SaSsoClientConfig [\"\n                + \"mode=\" + mode\n                + \", client=\" + client\n                + \", serverUrl=\" + serverUrl\n                + \", authUrl=\" + authUrl\n                + \", signoutUrl=\" + signoutUrl\n                + \", pushUrl=\" + pushUrl\n                + \", getDataUrl=\" + getDataUrl\n                + \", currSsoLogin=\" + currSsoLogin\n                + \", currSsoLogoutCall=\" + currSsoLogoutCall\n                + \", isHttp=\" + isHttp\n                + \", isSlo=\" + isSlo\n                + \", regLogoutCall=\" + regLogoutCall\n                + \", secretKey=\" + secretKey\n                + \", isCheckSign=\" + isCheckSign\n                + \"]\";\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoClientModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.config;\n\n\nimport cn.dev33.satoken.sso.error.SaSsoErrorCode;\nimport cn.dev33.satoken.sso.exception.SaSsoException;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n/**\n * Sa-Token SSO 客户端信息配置 （在 Server 端配置允许接入的 Client 信息）\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoClientModel implements Serializable {\n\n    private static final long serialVersionUID = -6541180061782004705L;\n\n    /**\n     * Client 名称标识\n     */\n    public String client;\n\n    /**\n     * 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket )\n     */\n    public String allowUrl = \"\";\n\n    /**\n     * 是否接收推送消息\n     */\n    public Boolean isPush = false;\n\n    /**\n     * 是否打开单点注销功能\n     */\n    public Boolean isSlo = true;\n\n    /**\n     * API 调用签名秘钥\n     */\n    public String secretKey;\n\n    /**\n     * 此 Client 端主机总地址\n     */\n    public String serverUrl;\n\n    /**\n     * 此 Client 端推送消息的地址 (如不配置，默认根据 serverUrl + '/sso/pushC' 进行拼接)\n     */\n    public String pushUrl = \"/sso/pushC\";\n\n\n    // 额外添加的一些函数\n\n    /**\n     * 以数组形式写入允许的授权回调地址\n     * @param url 所有集合\n     * @return 对象自身\n     */\n    public SaSsoClientModel setAllow(String ...url) {\n        this.setAllowUrl(SaFoxUtil.arrayJoin(url));\n        return this;\n    }\n\n    /**\n     * 获取拼接 url：此 Client 端推送消息的地址\n     *\n     * @return /\n     */\n    public String splicingPushUrl() {\n        String _pushUrl = SaFoxUtil.spliceTwoUrl(getServerUrl(), getPushUrl());\n        if ( ! SaFoxUtil.isUrl(_pushUrl)) {\n            throw new SaSsoException(\"应用 [\" + getClient() + \"] 推送地址无效：\" + _pushUrl).setCode(SaSsoErrorCode.CODE_30023);\n        }\n        return _pushUrl;\n    }\n\n\n    // get set\n\n    /**\n     * @return Client 名称标识\n     */\n    public String getClient() {\n        return client;\n    }\n\n    /**\n     * @param client Client 名称标识\n     */\n    public SaSsoClientModel setClient(String client) {\n        this.client = client;\n        return this;\n    }\n\n    /**\n     * @return 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket )\n     */\n    public String getAllowUrl() {\n        return allowUrl;\n    }\n\n    /**\n     * @param allowUrl 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的 URL 将禁止下放 ticket )\n     * @return 对象自身\n     */\n    public SaSsoClientModel setAllowUrl(String allowUrl) {\n        // 提前校验一下配置的 allowUrl 是否合法，让开发者尽早发现错误\n        if(SaFoxUtil.isNotEmpty(allowUrl)) {\n            List<String> allowUrlList = SaFoxUtil.convertStringToList(allowUrl);\n            SaSsoServerTemplate.checkAllowUrlListStaticMethod(allowUrlList);\n        }\n        this.allowUrl = allowUrl;\n        return this;\n    }\n\n    /**\n     * @return isHttp 是否打开模式三\n     */\n    public Boolean getIsPush() {\n        return isPush;\n    }\n\n    /**\n     * @param isPush 是否打开模式三\n     * @return 对象自身\n     */\n    public SaSsoClientModel setIsPush(Boolean isPush) {\n        this.isPush = isPush;\n        return this;\n    }\n\n    /**\n     * @return 是否打开单点注销功能\n     */\n    public Boolean getIsSlo() {\n        return isSlo;\n    }\n\n    /**\n     * @param isSlo 是否打开单点注销功能\n     * @return 对象自身\n     */\n    public SaSsoClientModel setIsSlo(Boolean isSlo) {\n        this.isSlo = isSlo;\n        return this;\n    }\n\n    /**\n     * 获取 API 调用签名秘钥\n     *\n     * @return /\n     */\n    public String getSecretKey() {\n        return this.secretKey;\n    }\n\n    /**\n     * 设置 API 调用签名秘钥\n     *\n     * @param secretKey /\n     * @return 对象自身\n     */\n    public SaSsoClientModel setSecretKey(String secretKey) {\n        this.secretKey = secretKey;\n        return this;\n    }\n\n    /**\n     * 获取 此 Client 端主机总地址\n     *\n     * @return serverUrl 此 Client 端主机总地址\n     */\n    public String getServerUrl() {\n        return this.serverUrl;\n    }\n\n    /**\n     * 设置 此 Client 端主机总地址\n     *\n     * @param serverUrl 此 Client 端主机总地址\n     * @return 对象自身\n     */\n    public SaSsoClientModel setServerUrl(String serverUrl) {\n        this.serverUrl = serverUrl;\n        return this;\n    }\n\n    /**\n     * 获取 此 Client 端推送消息的地址 (如不配置，默认根据 serverUrl + '/sso/pushC' 进行拼接)\n     *\n     * @return /\n     */\n    public String getPushUrl() {\n        return this.pushUrl;\n    }\n\n    /**\n     * 设置 此 Client 端推送消息的地址 (如不配置，默认根据 serverUrl + '/sso/pushC' 进行拼接)\n     *\n     * @param pushUrl 此 Client 端推送消息的地址\n     * @return 对象自身\n     */\n    public SaSsoClientModel setPushUrl(String pushUrl) {\n        this.pushUrl = pushUrl;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"SaSsoClientModel [\"\n                + \"client=\" + client\n                + \", allowUrl=\" + allowUrl\n                + \", isSlo=\" + isSlo\n                + \", isPush=\" + isPush\n                + \", secretKey=\" + secretKey\n                + \", serverUrl=\" + serverUrl\n                + \", pushUrl=\" + pushUrl\n                + \"]\";\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/config/SaSsoServerConfig.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.config;\n\n\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Sa-Token SSO Server 端 配置类\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoServerConfig implements Serializable {\n\n    private static final long serialVersionUID = -6541180061782004705L;\n\n\n    // ----------------- Server端相关配置\n\n    /**\n     * 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\n     */\n    public String mode = \"\";\n\n    /**\n     * ticket 有效期 (单位: 秒)\n     */\n    public long ticketTimeout = 60 * 5;\n\n    /**\n     * 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\n     */\n    public String homeRoute;\n\n    /**\n     * 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息)\n     */\n    public Boolean isSlo = true;\n\n    /**\n     * 是否在每次下发 ticket 时，自动续期 token 的有效期（根据全局 timeout 值）\n     */\n    public Boolean autoRenewTimeout = false;\n\n    /**\n     * 在 Account-Session 上记录 Client 信息的最高数量（-1=无限），超过此值将进行自动清退处理，先进先出\n     */\n    public int maxRegClient = 32;\n\n    /**\n     * 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\n     */\n    public Boolean isCheckSign = true;\n\n    /**\n     * Client 信息配置列表\n     */\n    public Map<String, SaSsoClientModel> clients = new LinkedHashMap<>();\n\n    // 匿名 Client 相关配置\n\n    /**\n     * 是否允许匿名 Client 接入\n     */\n    public Boolean allowAnonClient = false;\n\n    /**\n     * 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用)\n     */\n    public String allowUrl = \"\";\n\n    /**\n     * API 调用签名秘钥 (全局默认 + 匿名 client 使用)\n     */\n    public String secretKey;\n\n\n    // 额外方法\n\n    /**\n     * 以数组形式写入允许的授权回调地址 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用)\n     * @param url 所有集合\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setAllow(String ...url) {\n        this.setAllowUrl(SaFoxUtil.arrayJoin(url));\n        return this;\n    }\n\n    /**\n     * 添加一个应用\n     * @param client /\n     * @return 对象自身\n     */\n    public SaSsoServerConfig addClient(SaSsoClientModel client) {\n        this.clients.put(client.getClient(), client);\n        return this;\n    }\n\n\n    // get set\n\n    /**\n     * 获取 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\n     *\n     * @return /\n     */\n    public String getMode() {\n        return this.mode;\n    }\n\n    /**\n     * 设置 指定当前系统集成 SSO 时使用的模式（约定型配置项，不对代码逻辑产生任何影响）\n     *\n     * @param mode /\n     */\n    public void setMode(String mode) {\n        this.mode = mode;\n    }\n\n    /**\n     * @return ticket 有效期 (单位: 秒)\n     */\n    public long getTicketTimeout() {\n        return ticketTimeout;\n    }\n\n    /**\n     * @param ticketTimeout ticket 有效期 (单位: 秒)\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setTicketTimeout(long ticketTimeout) {\n        this.ticketTimeout = ticketTimeout;\n        return this;\n    }\n\n    /**\n     * @return 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用)\n     */\n    public String getAllowUrl() {\n        return allowUrl;\n    }\n\n    /**\n     * @param allowUrl 所有允许的授权回调地址，多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) (匿名 client 使用)\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setAllowUrl(String allowUrl) {\n        // 提前校验一下配置的 allowUrl 是否合法，让开发者尽早发现错误\n        if(SaFoxUtil.isNotEmpty(allowUrl)) {\n            List<String> allowUrlList = SaFoxUtil.convertStringToList(allowUrl);\n            SaSsoServerTemplate.checkAllowUrlListStaticMethod(allowUrlList);\n        }\n        this.allowUrl = allowUrl;\n        return this;\n    }\n\n    /**\n     * @return 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\n     */\n    public String getHomeRoute() {\n        return homeRoute;\n    }\n\n    /**\n     * @param homeRoute 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setHomeRoute(String homeRoute) {\n        this.homeRoute = homeRoute;\n        return this;\n    }\n\n    /**\n     * @return 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息)\n     */\n    public Boolean getIsSlo() {\n        return isSlo;\n    }\n\n    /**\n     * @param isSlo 是否打开单点注销功能 (为 true 时接收 client 端推送的单点注销消息)\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setIsSlo(Boolean isSlo) {\n        this.isSlo = isSlo;\n        return this;\n    }\n\n    /**\n     * @return 是否在每次下发 ticket 时，自动续期 token 的有效期（根据全局 timeout 值）\n     */\n    public Boolean getAutoRenewTimeout() {\n        return autoRenewTimeout;\n    }\n\n    /**\n     * @param autoRenewTimeout 是否在每次下发 ticket 时，自动续期 token 的有效期（根据全局 timeout 值）\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setAutoRenewTimeout(Boolean autoRenewTimeout) {\n        this.autoRenewTimeout = autoRenewTimeout;\n        return this;\n    }\n\n    /**\n     * @return maxLoginClient 在 Account-Session 上记录 Client 信息的最高数量（-1=无限），超过此值将进行自动清退处理，先进先出\n     */\n    public int getMaxRegClient() {\n        return maxRegClient;\n    }\n\n    /**\n     * @param maxRegClient 在 Account-Session 上记录 Client 信息的最高数量（-1=无限），超过此值将进行自动清退处理，先进先出\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setMaxRegClient(int maxRegClient) {\n        this.maxRegClient = maxRegClient;\n        return this;\n    }\n\n    /**\n     * 获取 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\n     *\n     * @return isCheckSign 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\n     */\n    public Boolean getIsCheckSign() {\n        return this.isCheckSign;\n    }\n\n    /**\n     * 设置 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\n     *\n     * @param isCheckSign 是否校验参数签名（方便本地调试用的一个配置项，生产环境请务必为true）\n     */\n    public SaSsoServerConfig setIsCheckSign(Boolean isCheckSign) {\n        this.isCheckSign = isCheckSign;\n        return this;\n    }\n\n    /**\n     * 获取 是否允许匿名 Client 接入\n     *\n     * @return /\n     */\n    public Boolean getAllowAnonClient() {\n        return this.allowAnonClient;\n    }\n\n    /**\n     * 设置 是否允许匿名 Client 接入\n     *\n     * @param allowAnonClient /\n     */\n    public SaSsoServerConfig setAllowAnonClient(Boolean allowAnonClient) {\n        this.allowAnonClient = allowAnonClient;\n        return this;\n    }\n\n    /**\n     * 获取 API 调用签名秘钥 (全局默认 + 匿名 client 使用)\n     *\n     * @return /\n     */\n    public String getSecretKey() {\n        return this.secretKey;\n    }\n\n    /**\n     * 设置 API 调用签名秘钥 (全局默认 + 匿名 client 使用)\n     *\n     * @param secretKey /\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setSecretKey(String secretKey) {\n        this.secretKey = secretKey;\n        return this;\n    }\n\n    /**\n     * 获取 Client 信息配置列表\n     *\n     * @return clients Client 信息配置列表\n     */\n    public Map<String, SaSsoClientModel> getClients() {\n        return this.clients;\n    }\n\n    /**\n     * 设置 Client 信息配置列表\n     *\n     * @param clients Client 信息配置列表\n     * @return 对象自身\n     */\n    public SaSsoServerConfig setClients(Map<String, SaSsoClientModel> clients) {\n        this.clients = clients;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"SaSsoServerConfig [\"\n                + \"mode=\" + mode\n                + \", ticketTimeout=\" + ticketTimeout\n                + \", allowUrl=\" + allowUrl\n                + \", homeRoute=\" + homeRoute\n                + \", isSlo=\" + isSlo\n                + \", autoRenewTimeout=\" + autoRenewTimeout\n                + \", maxRegClient=\" + maxRegClient\n                + \", isCheckSign=\" + isCheckSign\n                + \", allowAnonClient=\" + allowAnonClient\n                + \", secretKey=\" + secretKey\n                + \", clients=\" + clients\n                + \"]\";\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/error/SaSsoErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.error;\n\n/**\n * 定义 sa-token-sso 所有异常细分状态码 \n * \n * @author click33\n * @since 1.33.0\n */\npublic interface SaSsoErrorCode {\n\n\t/** redirect 重定向 url 是一个无效地址 */\n\tint CODE_30001 = 30001;\n\n\t/** redirect 重定向 url 不在 allowUrl 允许的范围内 */\n\tint CODE_30002 = 30002;\n\n\t/** 接口调用方提供的 secretkey 秘钥无效 */\n\tint CODE_30003 = 30003;\n\n\t/** 提供的 ticket 是无效的 */\n\tint CODE_30004 = 30004;\n\n\t/** 在模式三下，sso-client 调用 sso-server 端 校验ticket接口 时，得到的响应是校验失败 */\n\tint CODE_30005 = 30005;\n\n\t/** 在模式三下，sso-client 调用 sso-server 端 单点注销接口 时，得到的响应是注销失败 */\n\tint CODE_30006 = 30006;\n\n\t/** http 请求调用 提供的 timestamp 与当前时间的差距超出允许的范围 */\n\tint CODE_30007 = 30007;\n\n\t/** http 请求调用 提供的 sign 无效 */\n\tint CODE_30008 = 30008;\n\n\t/** 本地系统没有配置 secretkey 字段 */\n\tint CODE_30009 = 30009;\n\n\t/** 本地系统没有配置 http 请求处理器 */\n\tint CODE_30010 = 30010;\n\n\t/** 该 ticket 不属于当前 client */\n\tint CODE_30011 = 30011;\n\n\t/** 当前缺少配置 server-url 地址 */\n\tint CODE_30012 = 30012;\n\n\t/** 提供的 client 参数值无效 */\n\tint CODE_30013 = 30013;\n\n\t/** 在 /sso/auth 既没有指定 redirect 参数，也没有配置 homeRoute 路由 */\n\tint CODE_30014 = 30014;\n\n\t/** 无效的 allow-url 配置 */\n\tint CODE_30015 = 30015;\n\n\t/** 未能找到指定类型的消息处理器 */\n\tint CODE_30021 = 30021;\n\n\t/** 消息类型不能为空 */\n\tint CODE_30022 = 30022;\n\n\t/** 无效的消息推送地址 */\n\tint CODE_30023 = 30023;\n\n\t/** SSO 消息里缺少指定的参数 */\n\tint CODE_30024 = 30024;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/exception/SaSsoException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.exception;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n\n/**\n * 一个异常：代表 SSO 认证流程错误 \n * \n * @author click33\n * @since 1.30.0\n */\npublic class SaSsoException extends SaTokenException {\n\n\t/**\n\t * 序列化版本号\n\t */\n\tprivate static final long serialVersionUID = 6806129545290130114L;\n\t\n\t/**\n\t * 一个异常：代表 SSO 认证流程错误 \n\t * @param message 异常描述 \n\t */\n\tpublic SaSsoException(String message) {\n\t\tsuper(message);\n\t}\n\n\t/**\n\t * 一个异常：代表 SSO 认证流程错误 \n\t * @param code 异常细分状态码 \n\t * @param message 异常描述 \n\t */\n\tpublic SaSsoException(int code, String message) {\n\t\tsuper(code, message);\n\t}\n\n\t/**\n\t * 写入异常细分状态码 \n\t * @param code 异常细分状态码\n\t * @return 对象自身 \n\t */\n\tpublic SaSsoException setCode(int code) {\n\t\tsuper.setCode(code);\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * 断言 flag 不为 true，否则抛出 message 异常\n\t * @param flag 标记\n\t * @param message 异常信息\n\t * @param code 异常细分状态码\n\t */\n\tpublic static void notTrue(boolean flag, String message, int code) {\n\t\tif(flag) {\n\t\t\tthrow new SaSsoException(message).setCode(code);\n\t\t}\n\t}\n\n\t/**\n\t * 断言 value 不为空，否则抛出 message 异常\n\t * @param value 值\n\t * @param message 异常信息\n\t * @param code 异常细分状态码\n\t */\n\tpublic static void notEmpty(Object value, String message, int code) {\n\t\tif(SaFoxUtil.isEmpty(value)) {\n\t\t\tthrow new SaSsoException(message).setCode(code);\n\t\t}\n\t}\n\n\t/**\n\t * 如果flag==true，则抛出message异常\n\t * @param flag 标记\n\t * @param message 异常信息\n\t */\n\t@Deprecated\n\tpublic static void throwBy(boolean flag, String message) {\n\t\tif(flag) {\n\t\t\tthrow new SaSsoException(message);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/CheckTicketAppendDataFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.function;\n\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：sso-server 端：在校验 ticket 后，给 sso-client 端追加返回信息的函数\n *\n * <p>  参数：loginId, SaResult 响应参数对象  </p>\n * <p>  返回：SaResult 响应参数对象  </p>\n *\n * @author click33\n * @since 1.38.0\n */\n@FunctionalInterface\npublic interface CheckTicketAppendDataFunction extends BiFunction<Object, SaResult, SaResult> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/DoLoginHandleFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.function;\n\nimport java.util.function.BiFunction;\n\n/**\n * 函数式接口：sso-server 端：登录处理函数\n *\n * <p>  参数：账号、密码  </p>\n * <p>  返回：登录结果  </p>\n *\n * @author click33\n * @since 1.38.0\n */\n@FunctionalInterface\npublic interface DoLoginHandleFunction extends BiFunction<String, String, Object> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/NotLoginViewFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.function;\n\nimport java.util.function.Supplier;\n\n/**\n * 函数式接口：sso-server 端：未登录时返回的 View\n *\n * <p>  参数：无  </p>\n * <p>  返回：未登录时的 View 视图  </p>\n *\n * @author click33\n * @since 1.38.0\n */\n@FunctionalInterface\npublic interface NotLoginViewFunction extends Supplier<Object> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SaSsoMessageHandleFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.function;\n\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\n\n/**\n * 函数式接口：处理 SSO 消息的函数式接口\n *\n * <p>  参数：ssoTemplate 模板对象, 要处理的 message 消息  </p>\n * <p>  返回：任意值   </p>\n *\n * @author click33\n * @since 1.38.0\n */\n@FunctionalInterface\npublic interface SaSsoMessageHandleFunction {\n\n    Object execute(SaSsoTemplate ssoTemplate, SaSsoMessage message);\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/SendRequestFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.function;\n\nimport java.util.function.Function;\n\n/**\n * 函数式接口：发送 Http 请求的处理函数\n *\n * <p>  参数：要请求的url  </p>\n * <p>  返回：请求结果  </p>\n *\n * @author click33\n * @since 1.38.0\n */\n@FunctionalInterface\npublic interface SendRequestFunction extends Function<String, String> {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/function/TicketResultHandleFunction.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.function;\n\nimport cn.dev33.satoken.sso.model.SaCheckTicketResult;\n\n/**\n * 函数式接口：sso-client 端：自定义校验 ticket 返回值的处理逻辑 （每次从认证中心获取校验 ticket 的结果后调用）\n *\n * <p>  参数：loginId, back  </p>\n * <p>  返回：返回给前端的值  </p>\n *\n * @author click33\n * @since 1.38.0\n */\n@FunctionalInterface\npublic interface TicketResultHandleFunction {\n\n    Object run(SaCheckTicketResult ctr, String back);\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessage.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.message;\n\n\nimport cn.dev33.satoken.application.SaSetValueInterface;\nimport cn.dev33.satoken.sso.error.SaSsoErrorCode;\nimport cn.dev33.satoken.sso.exception.SaSsoException;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * SSO 消息 Model\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoMessage extends LinkedHashMap<String, Object> implements SaSetValueInterface, Serializable {\n\n    /**\n     *\n     */\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * KEY：TYPE\n     */\n    public static final String MSG_TYPE = \"msgType\";\n\n    public SaSsoMessage() {\n\n    }\n\n    /**\n     * 构造函数\n     * @param type 消息类型\n     */\n    public SaSsoMessage(String type) {\n        setType(type);\n    }\n\n    /**\n     * 构造函数\n     * @param map 消息参数\n     */\n    public SaSsoMessage(Map<String, ?> map) {\n        this.putAll(map);\n    }\n\n    /**\n     * 获取消息类型\n     * @return /\n     */\n    public String getType() {\n        return getString(MSG_TYPE);\n    }\n\n    /**\n     * 设置消息类型\n     * @param type /\n     * @return /\n     */\n    public SaSsoMessage setType(String type) {\n        return set(MSG_TYPE, type);\n    }\n\n    /**\n     * 校验消息类型\n     */\n    public void checkType() {\n        if(SaFoxUtil.isEmpty(getString(MSG_TYPE))) {\n            throw new SaSsoException(\"消息类型不可为空\").setCode(SaSsoErrorCode.CODE_30022);\n        }\n    }\n\n    // -----------\n\n    @Override\n    public Object get(String key) {\n        return super.get(key);\n    }\n\n    @Override\n    public SaSsoMessage set(String key, Object value) {\n        super.put(key, value);\n        return this;\n    }\n\n    @Override\n    public SaSsoMessage delete(String key) {\n        super.remove(key);\n        return this;\n    }\n\n    // -----------\n\n    /**\n     * 获取一个值 （此值必须存在，否则抛出异常 ）\n     * @param key 键\n     * @return 参数值\n     */\n    public Object getValueNotNull(String key) {\n        Object value = get(key);\n        if(SaFoxUtil.isEmpty(value)) {\n            throw new SaSsoException(\"缺少参数：\" + key).setCode(SaSsoErrorCode.CODE_30024);\n        }\n        return value;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/SaSsoMessageHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.message;\n\n\nimport cn.dev33.satoken.sso.error.SaSsoErrorCode;\nimport cn.dev33.satoken.sso.exception.SaSsoException;\nimport cn.dev33.satoken.sso.function.SaSsoMessageHandleFunction;\nimport cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle;\nimport cn.dev33.satoken.sso.message.handle.SaSsoMessageSimpleHandle;\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * SSO 消息处理器 - 持有器\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoMessageHolder {\n\n    /**\n     * 所有已注册的消息处理器\n     */\n    public final Map<String, SaSsoMessageHandle> messageHandleMap = new LinkedHashMap<>();\n\n    /**\n     * 判断是否具有指定类型的消息处理器\n     *\n     * @param type 消息类型\n     * @return /\n     */\n    public boolean hasHandle(String type) {\n        return messageHandleMap.containsKey(type);\n    }\n\n    /**\n     * 删除指定类型的消息处理器\n     *\n     * @param type 消息类型\n     */\n    public SaSsoMessageHolder removeHandle(String type) {\n        messageHandleMap.remove(type);\n        return this;\n    }\n\n    /**\n     * 添加指定类型的消息处理器\n     *\n     * @param handle /\n     * @return 对象自身\n     */\n    public SaSsoMessageHolder addHandle(SaSsoMessageHandle handle) {\n        messageHandleMap.put(handle.getHandlerType(), handle);\n        return this;\n    }\n\n    /**\n     * 添加指定类型的简单消息处理器\n     *\n     * @param type 要处理的消息类型\n     * @param handle 要执行的方法\n     * @return 对象自身\n     */\n    public SaSsoMessageHolder addHandle(String type, SaSsoMessageHandleFunction handle) {\n        messageHandleMap.put(type, new SaSsoMessageSimpleHandle(type, handle));\n        return this;\n    }\n\n    /**\n     * 获取指定类型的消息处理器\n     *\n     * @param type /\n     */\n    public SaSsoMessageHandle getHandle(String type) {\n        return messageHandleMap.get(type);\n    }\n\n    /**\n     * 处理指定消息\n     *\n     * @param ssoTemplate /\n     * @param message /\n     * @return 处理结果\n     */\n    public Object handleMessage(SaSsoTemplate ssoTemplate, SaSsoMessage message) {\n        SaSsoMessageHandle handle = messageHandleMap.get(message.getType());\n        if(handle == null) {\n            throw new SaSsoException(\"未能找到消息处理器: \" + message.getType()).setCode(SaSsoErrorCode.CODE_30021);\n        }\n        return handle.handle(ssoTemplate, message);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageHandle.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.message.handle;\n\n\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\n\n/**\n * SSO 消息处理器 - 父接口\n *\n * @author click33\n * @since 1.43.0\n */\npublic interface SaSsoMessageHandle {\n\n    /**\n     * 获取所要处理的消息类型\n     *\n     * @return /\n     */\n    String getHandlerType();\n\n    /**\n     * 具体要执行的处理方法\n     *\n     * @param ssoTemplate /\n     * @param message /\n     * @return /\n     */\n    Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message);\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/SaSsoMessageSimpleHandle.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.message.handle;\n\n\nimport cn.dev33.satoken.sso.function.SaSsoMessageHandleFunction;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\n\n/**\n * SSO 消息处理器 - 简单实现，方便 lambda 表达式编程\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoMessageSimpleHandle implements SaSsoMessageHandle{\n\n    public String type;\n\n    public SaSsoMessageHandleFunction handle;\n\n    /**\n     * SSO 消息处理器 - 简单实现，方便 lambda 表达式编程\n     * @param type 要处理的消息类型\n     * @param handle 要执行的方法\n     */\n    public SaSsoMessageSimpleHandle(String type, SaSsoMessageHandleFunction handle) {\n        this.type = type;\n        this.handle = handle;\n    }\n\n    /**\n     * 获取所要处理的消息类型\n     *\n     * @return /\n     */\n    @Override\n    public String getHandlerType() {\n        return type;\n    }\n\n    /**\n     * 具体要执行的处理方法\n     *\n     * @param ssoTemplate /\n     * @param message /\n     * @return /\n     */\n    @Override\n    public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message){\n        return handle.execute(ssoTemplate, message);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/client/SaSsoMessageLogoutCallHandle.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.message.handle.client;\n\n\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle;\nimport cn.dev33.satoken.sso.name.ParamName;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * SSO 消息处理器 - sso-client 端：处理 单点注销回调 的请求\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoMessageLogoutCallHandle implements SaSsoMessageHandle {\n\n    /**\n     * 获取所要处理的消息类型\n     *\n     * @return /\n     */\n    public String getHandlerType() {\n        return SaSsoConsts.MESSAGE_LOGOUT_CALL;\n    }\n\n    /**\n     * 执行方法\n     *\n     * @param ssoTemplate /\n     * @param message /\n     * @return /\n     */\n    public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) {\n\n        // 1、获取对象\n        SaSsoClientTemplate ssoClientTemplate = (SaSsoClientTemplate) ssoTemplate;\n        StpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal();\n        ParamName paramName = ssoClientTemplate.paramName;\n\n        // 2、判断当前应用是否开启单点注销功能\n        if( ! ssoClientTemplate.getClientConfig().getIsSlo()) {\n            return SaResult.error(\"当前 sso-client 端未开启单点注销功能\");\n        }\n\n        // 3、获取参数\n        Object loginId = message.getValueNotNull(paramName.loginId);\n        loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(loginId);\n        String deviceId = message.getString(paramName.deviceId);\n\n        // 4、注销当前应用端会话\n        stpLogic.logout(loginId, new SaLogoutParameter()\n                .setDeviceId(deviceId)\n        );\n\n        // 5、响应\n        return SaResult.ok(\"单点注销回调成功\");\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageCheckTicketHandle.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.message.handle.server;\n\n\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle;\nimport cn.dev33.satoken.sso.model.TicketModel;\nimport cn.dev33.satoken.sso.name.ParamName;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * SSO 消息处理器 - sso-server 端：处理校验 ticket 的请求\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoMessageCheckTicketHandle implements SaSsoMessageHandle {\n\n    /**\n     * 获取所要处理的消息类型\n     *\n     * @return /\n     */\n    public String getHandlerType() {\n        return SaSsoConsts.MESSAGE_CHECK_TICKET;\n    }\n\n    /**\n     * 执行方法\n     *\n     * @param ssoTemplate /\n     * @param message /\n     * @return /\n     */\n    public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) {\n\n        // 1、获取对象\n        SaSsoServerTemplate ssoServerTemplate = (SaSsoServerTemplate) ssoTemplate;\n        ParamName paramName = ssoServerTemplate.paramName;\n        StpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal();\n        String client = message.getString(paramName.client);\n        String ticket = message.getValueNotNull(paramName.ticket).toString();\n        String sloCallback = message.getString(paramName.ssoLogoutCall);\n\n        // 2、校验ticket，获取 loginId\n        TicketModel ticketModel = ssoServerTemplate.checkTicketParamAndDelete(ticket, client);\n        Object loginId = ticketModel.getLoginId();\n\n        // 3、注册此客户端的登录信息\n        ssoServerTemplate.registerSloCallbackUrl(loginId, client, sloCallback);\n\n        // 4、给 client 端响应结果\n        SaResult result = SaResult.ok();\n        result.setData(loginId); // 兼容历史版本\n        result.set(paramName.loginId, loginId);\n        result.set(paramName.tokenValue, ticketModel.getTokenValue());\n        result.set(paramName.deviceId, stpLogic.getLoginDeviceIdByToken(ticketModel.getTokenValue()));\n        result.set(paramName.remainTokenTimeout, stpLogic.getTokenTimeout(ticketModel.getTokenValue()));\n        result.set(paramName.remainSessionTimeout, stpLogic.getSessionTimeoutByLoginId(loginId));\n        result = ssoServerTemplate.strategy.checkTicketAppendData.apply(loginId, result);\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/message/handle/server/SaSsoMessageSignoutHandle.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.message.handle.server;\n\n\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.message.handle.SaSsoMessageHandle;\nimport cn.dev33.satoken.sso.name.ParamName;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * SSO 消息处理器 - sso-server 端：处理 单点注销 的请求\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoMessageSignoutHandle implements SaSsoMessageHandle {\n\n    /**\n     * 获取所要处理的消息类型\n     *\n     * @return /\n     */\n    public String getHandlerType() {\n        return SaSsoConsts.MESSAGE_SIGNOUT;\n    }\n\n    /**\n     * 执行方法\n     *\n     * @param ssoTemplate /\n     * @param message /\n     * @return /\n     */\n    public Object handle(SaSsoTemplate ssoTemplate, SaSsoMessage message) {\n\n        // 1、获取对象\n        SaSsoServerTemplate ssoServerTemplate = (SaSsoServerTemplate) ssoTemplate;\n        ParamName paramName = ssoServerTemplate.paramName;\n\n        // 2、判断当前是否开启了全局单点注销功能\n        if( ! ssoServerTemplate.getServerConfig().getIsSlo()) {\n            return SaResult.error(\"当前 sso-server 端未开启单点注销功能\");\n        }\n\n        // 3、获取参数\n        String client = message.getString(paramName.client);\n        Object loginId = message.get(paramName.loginId);\n        String deviceId = message.getString(paramName.deviceId);\n\n        // 4、单点注销\n        SaLogoutParameter logoutParameter = ssoServerTemplate.getStpLogicOrGlobal().createSaLogoutParameter().setDeviceId(deviceId);\n        ssoServerTemplate.ssoLogout(loginId, logoutParameter, client);\n\n        // 5、响应\n        return SaResult.ok();\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaCheckTicketResult.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.model;\n\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.io.Serializable;\n\n/**\n * 校验 ticket 返回 loginId 等结果的参数封装\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaCheckTicketResult implements Serializable {\n\n    private static final long serialVersionUID = 1406115065849845073L;\n\n    /** 账号id */\n    public Object loginId;\n\n    /** 在 sso-server 端的 token 值 */\n    public String tokenValue;\n\n    /** 登录设备 id */\n    public String deviceId;\n\n    /** 此账号 token 剩余有效期 */\n    public Long remainTokenTimeout;\n\n    /** 此账号会话剩余有效期 */\n    public Long remainSessionTimeout;\n\n    /** 此账号在认证中心的 loginId */\n    public Object centerId;\n\n    /** 从 sso-server 返回的原生所有参数 */\n    public SaResult result;\n\n    @Override\n    public String toString() {\n        return \"SaCheckTicketResult{\" +\n                \"loginId=\" + loginId +\n                \", tokenValue='\" + tokenValue + '\\'' +\n                \", deviceId='\" + deviceId + '\\'' +\n                \", remainTokenTimeout=\" + remainTokenTimeout +\n                \", remainSessionTimeout=\" + remainSessionTimeout +\n                \", centerId=\" + centerId +\n                \", result=\" + result +\n                '}';\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientInfo.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.model;\n\n\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\n\nimport java.io.Serializable;\n\n/**\n * Sa-Token SSO 应用信息（注册在 SaSession 上的已登录应用信息列表）\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoClientInfo implements Serializable {\n\n    private static final long serialVersionUID = 1406115065849845073L;\n\n    /*\n     * 只能记录模式三登录的 client 信息，模式一和模式二的信息即使记录上，也无法完成单点注销操作，遂不记录\n     * 所以：mode、tokenValue 字段，仅留作扩展，暂时无用\n     */\n\n    /**\n     * 此 client 登录模式（1=模式一，2=模式二，3=模式三）\n     */\n    public int mode;\n\n    /**\n     * 客户端标识\n     */\n    public String client;\n\n    /**\n     * 单点注销回调 url\n     */\n    public String sloCallbackUrl;\n\n    /**\n     * 此 client 注册信息的时间，13位时间戳\n     */\n    public long regTime;\n\n    /**\n     * 此账号有记录以来为第几次登录，默认从0开始递增\n     */\n    public int index;\n\n    public SaSsoClientInfo() {\n    }\n\n    /**\n     * 模式三构建\n     */\n    public SaSsoClientInfo(String client, String sloCallbackUrl, int index) {\n        this.mode = SaSsoConsts.SSO_MODE_3;\n        this.client = client;\n        this.sloCallbackUrl = sloCallbackUrl;\n        this.regTime = System.currentTimeMillis();\n        this.index = index;\n    }\n\n\n    // get set\n\n    /**\n     * 获取 此 client 登录模式（1=模式一，2=模式二，3=模式三）\n     *\n     * @return mode 此 client 登录模式（1=模式一，2=模式二，3=模式三）\n     */\n    public int getMode() {\n        return this.mode;\n    }\n\n    /**\n     * 设置 此 client 登录模式（1=模式一，2=模式二，3=模式三）\n     *\n     * @param mode 此 client 登录模式（1=模式一，2=模式二，3=模式三）\n     * @return /\n     */\n    public SaSsoClientInfo setMode(int mode) {\n        this.mode = mode;\n        return this;\n    }\n\n    /**\n     * 获取 客户端标识\n     *\n     * @return client 客户端标识\n     */\n    public String getClient() {\n        return this.client;\n    }\n\n    /**\n     * 设置 客户端标识\n     *\n     * @param client 客户端标识\n     * @return /\n     */\n    public SaSsoClientInfo setClient(String client) {\n        this.client = client;\n        return this;\n    }\n\n    /**\n     * 获取 单点注销回调url\n     *\n     * @return ssoLogoutCall 单点注销回调url\n     */\n    public String getSloCallbackUrl() {\n        return this.sloCallbackUrl;\n    }\n\n    /**\n     * 设置 单点注销回调url\n     *\n     * @param sloCallbackUrl 单点注销回调url\n     * @return /\n     */\n    public SaSsoClientInfo setSloCallbackUrl(String sloCallbackUrl) {\n        this.sloCallbackUrl = sloCallbackUrl;\n        return this;\n    }\n\n    /**\n     * 获取 此 client 注册信息的时间，13位时间戳\n     *\n     * @return regTime 此 client 注册信息的时间，13位时间戳\n     */\n    public long getRegTime() {\n        return this.regTime;\n    }\n\n    /**\n     * 设置 此 client 注册信息的时间，13位时间戳\n     *\n     * @param regTime 此 client 注册信息的时间，13位时间戳\n     * @return /\n     */\n    public SaSsoClientInfo setRegTime(long regTime) {\n        this.regTime = regTime;\n        return this;\n    }\n\n    /**\n     * 获取 此账号有记录以来为第几次登录，默认从0开始递增\n     *\n     * @return regTime 此账号有记录以来为第几次登录，默认从0开始递增\n     */\n    public long getIndex() {\n        return this.index;\n    }\n\n    /**\n     * 设置 此账号有记录以来为第几次登录，默认从0开始递增\n     *\n     * @param index 此账号有记录以来为第几次登录，默认从0开始递增\n     * @return /\n     */\n    public SaSsoClientInfo setIndex(int index) {\n        this.index = index;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return \"SaSsoClientModel{\" +\n                \"mode=\" + mode +\n                \", client='\" + client + '\\'' +\n                \", sloCallbackUrl='\" + sloCallbackUrl + '\\'' +\n                \", regTime=\" + regTime +\n                \", index=\" + index +\n                '}';\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/SaSsoClientModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.model;\n\n\n/**\n * Sa-Token SSO Model\n *\n * @author click33\n * @since 1.38.0\n */\n@Deprecated\npublic class SaSsoClientModel extends SaSsoClientInfo {\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/model/TicketModel.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.model;\n\nimport java.io.Serializable;\n\n/**\n * Model: Ticket 码\n *\n * @author click33\n * @since 1.43.0\n */\npublic class TicketModel implements Serializable {\n\n\tprivate static final long serialVersionUID = -6541180061782004705L;\n\n\t/**\n\t * ticket 码\n\t */\n\tpublic String ticket;\n\n\t/**\n\t * 应用标识\n\t */\n\tpublic String client;\n\n\t/**\n\t * 对应 loginId\n\t */\n\tpublic Object loginId;\n\n\t/**\n\t * 会话 token\n\t */\n\tpublic String tokenValue;\n\n\t/**\n\t * 创建时间，13位时间戳\n\t */\n\tpublic long createTime;\n\n\t/**\n\t * 构建一个\n\t */\n\tpublic TicketModel() {\n\t\tthis.createTime = System.currentTimeMillis();\n\t}\n\n\t/**\n\t * 构建一个\n\t * @param ticket 授权码\n\t * @param client 应用id\n\t * @param loginId 对应的账号id\n\t * @param tokenValue 会话 token\n\t */\n\tpublic TicketModel(String ticket, String client, Object loginId, String tokenValue) {\n\t\tthis();\n\t\tthis.ticket = ticket;\n\t\tthis.client = client;\n\t\tthis.loginId = loginId;\n\t\tthis.tokenValue = tokenValue;\n\t}\n\n\n\t// get set\n\n\t/**\n\t * 获取 ticket 码\n\t *\n\t * @return /\n\t */\n\tpublic String getTicket() {\n\t\treturn this.ticket;\n\t}\n\n\t/**\n\t * 设置 ticket 码\n\t *\n\t * @param ticket /\n\t * @return 对象自身\n\t */\n\tpublic TicketModel setTicket(String ticket) {\n\t\tthis.ticket = ticket;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 应用标识\n\t *\n\t * @return /\n\t */\n\tpublic String getClient() {\n\t\treturn this.client;\n\t}\n\n\t/**\n\t * 设置 应用标识\n\t *\n\t * @param client /\n\t * @return 对象自身\n\t */\n\tpublic TicketModel setClient(String client) {\n\t\tthis.client = client;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 对应 loginId\n\t *\n\t * @return /\n\t */\n\tpublic Object getLoginId() {\n\t\treturn this.loginId;\n\t}\n\n\t/**\n\t * 设置 对应 loginId\n\t *\n\t * @param loginId /\n\t * @return 对象自身\n\t */\n\tpublic TicketModel setLoginId(Object loginId) {\n\t\tthis.loginId = loginId;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 会话 token\n\t *\n\t * @return tokenValue 会话 token\n\t */\n\tpublic String getTokenValue() {\n\t\treturn this.tokenValue;\n\t}\n\n\t/**\n\t * 设置 会话 token\n\t *\n\t * @param tokenValue 会话 token\n\t * @return 对象自身\n\t */\n\tpublic TicketModel setTokenValue(String tokenValue) {\n\t\tthis.tokenValue = tokenValue;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 创建时间，13位时间戳\n\t *\n\t * @return /\n\t */\n\tpublic long getCreateTime() {\n\t\treturn this.createTime;\n\t}\n\n\t/**\n\t * 设置 创建时间，13位时间戳\n\t *\n\t * @param createTime /\n\t * @return 对象自身\n\t */\n\tpublic TicketModel setCreateTime(long createTime) {\n\t\tthis.createTime = createTime;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"TicketModel{\" +\n\t\t\t\t\"ticket='\" + ticket + '\\'' +\n\t\t\t\t\", client='\" + client + '\\'' +\n\t\t\t\t\", loginId=\" + loginId +\n\t\t\t\t\", tokenValue=\" + tokenValue +\n\t\t\t\t\", createTime=\" + createTime +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.name;\n\n/**\n * SSO 模块所有 API 路由名称定义 \n * \n * @author click33\n * @since 1.32.0\n */\npublic class ApiName {\n\n\t/** SSO-Server端：授权地址 */ \n\tpublic String ssoAuth = \"/sso/auth\";\n\n\t/** SSO-Server端：RestAPI 登录接口 */ \n\tpublic String ssoDoLogin = \"/sso/doLogin\";\n\n\t/** SSO-Server端：校验ticket 获取账号id */ \n\tpublic String ssoCheckTicket = \"/sso/checkTicket\";\n\n\t/** SSO-Server端：接收推送消息 */\n\tpublic String ssoPushS = \"/sso/pushS\";\n\n\t/** SSO-Server端：获取userinfo  */ \n\tpublic String ssoUserinfo = \"/sso/userinfo\";\n\n\t/** SSO-Server端：单点注销地址 */ \n\tpublic String ssoSignout = \"/sso/signout\";\n\n\t/** SSO-Client端：登录地址 */ \n\tpublic String ssoLogin = \"/sso/login\";\n\n\t/** SSO-Client端：单点注销地址 */ \n\tpublic String ssoLogout = \"/sso/logout\";\n\n\t/** SSO-Client端：判断当前是否登录地址 */\n\tpublic String ssoIsLogin = \"/sso/isLogin\";\n\n\t/** SSO-Client端：单点注销的回调 */ \n\tpublic String ssoLogoutCall = \"/sso/logoutCall\";\n\n\t/** SSO-Client端：接收推送消息 */\n\tpublic String ssoPushC = \"/sso/pushC\";\n\n\t/**\n\t * 批量修改 path，新增固定前缀\n\t * @param prefix 示例值：/sso-user、/sso-admin\n\t * @return 对象自身 \n\t */\n\tpublic ApiName addPrefix(String prefix) {\n\t\tthis.ssoAuth = prefix + this.ssoAuth;\n\t\tthis.ssoDoLogin = prefix + this.ssoDoLogin;\n\t\tthis.ssoCheckTicket = prefix + this.ssoCheckTicket;\n\t\tthis.ssoPushS = prefix + this.ssoPushS;\n\t\tthis.ssoUserinfo = prefix + this.ssoUserinfo;\n\t\tthis.ssoSignout  = prefix + this.ssoSignout;\n\t\tthis.ssoLogin = prefix + this.ssoLogin;\n\t\tthis.ssoLogout = prefix + this.ssoLogout;\n\t\tthis.ssoIsLogin = prefix + this.ssoIsLogin;\n\t\tthis.ssoPushC = prefix + this.ssoPushC;\n\t\tthis.ssoLogoutCall = prefix + this.ssoLogoutCall;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 批量修改 path，替换掉 /sso 固定前缀\n\t * @param prefix 示例值：/sso-user、/sso-admin\n\t * @return 对象自身 \n\t */\n\tpublic ApiName replacePrefix(String prefix) {\n\t\tString oldPrefix = \"/sso\";\n\t\tthis.ssoAuth = this.ssoAuth.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoDoLogin = this.ssoDoLogin.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoCheckTicket = this.ssoCheckTicket.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoPushS = this.ssoPushS.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoUserinfo = this.ssoUserinfo.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoSignout = this.ssoSignout.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoLogin = this.ssoLogin.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoLogout = this.ssoLogout.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoIsLogin = this.ssoIsLogin.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoPushC = this.ssoPushC.replaceFirst(oldPrefix, prefix);\n\t\tthis.ssoLogoutCall = this.ssoLogoutCall.replaceFirst(oldPrefix, prefix);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"ApiName{\" +\n\t\t\t\t\"ssoAuth='\" + ssoAuth + '\\'' +\n\t\t\t\t\", ssoDoLogin='\" + ssoDoLogin + '\\'' +\n\t\t\t\t\", ssoCheckTicket='\" + ssoCheckTicket + '\\'' +\n\t\t\t\t\", ssoPushS='\" + ssoPushS + '\\'' +\n\t\t\t\t\", ssoUserinfo='\" + ssoUserinfo + '\\'' +\n\t\t\t\t\", ssoSignout='\" + ssoSignout + '\\'' +\n\t\t\t\t\", ssoIsLogin='\" + ssoIsLogin + '\\'' +\n\t\t\t\t\", ssoLogin='\" + ssoLogin + '\\'' +\n\t\t\t\t\", ssoLogout='\" + ssoLogout + '\\'' +\n\t\t\t\t\", ssoLogoutCall='\" + ssoLogoutCall + '\\'' +\n\t\t\t\t\", ssoPushC='\" + ssoPushC + '\\'' +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ParamName.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.name;\n\n/**\n * SSO 模块所有参数名称定义 \n * \n * @author click33\n * @since 1.32.0\n */\npublic class ParamName {\n\n\t/** redirect 参数名称 */\n\tpublic String redirect = \"redirect\";\n\t\n\t/** ticket 参数名称 */\n\tpublic String ticket = \"ticket\";\n\n\t/** back 参数名称 */\n\tpublic String back = \"back\";\n\n\t/** mode 参数名称 */\n\tpublic String mode = \"mode\";\n\t\n\t/** 账号 id */\n\tpublic String loginId = \"loginId\";\n\n\t/** client 应用标识 */\n\tpublic String client = \"client\";\n\n\t/** token 名称 */\n\tpublic String tokenName = \"tokenName\";\n\n\t/** token 值 */\n\tpublic String tokenValue = \"tokenValue\";\n\n\t/** 设备 id */\n\tpublic String deviceId = \"deviceId\";\n\n\t/** 接口参数签名秘钥 */\n\tpublic String secretkey = \"secretkey\";\n\t\n\t/** Client 端单点注销时 - 回调 URL 参数名称 */\n\tpublic String ssoLogoutCall = \"ssoLogoutCall\";\n\n\t/** 是否为超过 maxRegClient 触发的自动注销 */\n\tpublic String autoLogout = \"autoLogout\";\n\n\tpublic String name = \"name\";\n\tpublic String pwd = \"pwd\";\n\t\n\tpublic String timestamp = \"timestamp\";\n\tpublic String nonce = \"nonce\";\n\tpublic String sign = \"sign\";\n\n\t/** Session 剩余有效期 参数名称 */\n\tpublic String remainSessionTimeout = \"remainSessionTimeout\";\n\n\t/** token 剩余有效期 参数名称 */\n\tpublic String remainTokenTimeout = \"remainTokenTimeout\";\n\n\t/** 是否单设备 id 注销 */\n\tpublic String singleDeviceIdLogout = \"singleDeviceIdLogout\";\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoClientProcessor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.processor;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.error.SaSsoErrorCode;\nimport cn.dev33.satoken.sso.exception.SaSsoException;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.model.SaCheckTicketResult;\nimport cn.dev33.satoken.sso.model.TicketModel;\nimport cn.dev33.satoken.sso.name.ApiName;\nimport cn.dev33.satoken.sso.name.ParamName;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.Map;\n\n/**\n * SSO 请求处理器 （Client端）\n * \n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoClientProcessor {\n\n\t/**\n\t * 全局默认实例\n\t */\n\tpublic static SaSsoClientProcessor instance = new SaSsoClientProcessor();\n\n\t/**\n\t * 底层 SaSsoClientTemplate 对象\n\t */\n\tpublic SaSsoClientTemplate ssoClientTemplate = new SaSsoClientTemplate();\n\n\t// ----------- SSO-Client 端路由分发 -----------\n\n\t/**\n\t * 分发 Client 端所有请求\n\t * @return 处理结果\n\t */\n\tpublic Object dister() {\n\t\tApiName apiName = ssoClientTemplate.apiName;\n\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaSsoClientConfig cfg = ssoClientTemplate.getClientConfig();\n\n\t\t// ------------------ 路由分发 ------------------\n\n\t\t// sso-client：登录地址\n\t\tif(req.isPath(apiName.ssoLogin)) {\n\t\t\treturn ssoLogin();\n\t\t}\n\n\t\t// sso-client：单点注销\n\t\tif(req.isPath(apiName.ssoLogout)) {\n\t\t\treturn ssoLogout();\n\t\t}\n\n\t\t// sso-client：接收消息推送\n\t\tif(req.isPath(apiName.ssoPushC)) {\n\t\t\treturn ssoPushC();\n\t\t}\n\n\t\t// sso-client：单点注销的回调\n\t\tif(req.isPath(apiName.ssoLogoutCall) && cfg.getRegLogoutCall()) {\n\t\t\treturn ssoLogoutCall();\n\t\t}\n\n\t\t// 默认返回\n\t\treturn SaSsoConsts.NOT_HANDLE;\n\t}\n\n\t/**\n\t * SSO-Client端：登录地址\n\t * @return 处理结果\n\t */\n\tpublic Object ssoLogin() {\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tParamName paramName = ssoClientTemplate.paramName;\n\t\tString ticket = req.getParam(paramName.ticket);\n\n\t\t/*\n\t\t * 此时有两种情况:\n\t\t * \t\t情况1：ticket 无值，说明此请求是 sso-client 端访问，需要重定向至 sso-server 认证中心\n\t\t * \t\t情况2：ticket 有值，说明此请求从 sso-server 认证中心重定向而来，需要根据 ticket 进行登录\n\t\t */\n\t\tif(ticket == null) {\n\t\t\treturn _goServerAuth();\n\t\t} else {\n\t\t\treturn _loginByTicket();\n\t\t}\n\t}\n\n\t/**\n\t * SSO-Client端：单点注销\n\t * @return 处理结果\n\t */\n\tpublic Object ssoLogout() {\n\t\t// 获取对象\n\t\tSaSsoClientConfig cfg = ssoClientTemplate.getClientConfig();\n\n\t\t// 无论登录时选择的是模式二还是模式三\n\t\t// \t\t在注销时都应该按照模式三的方法，通过 http 请求调用 sso-server 的单点注销接口来做到全端下线\n\t\t//\t\t如果按照模式二的方法注销，则会导致按照模式三登录的应用无法参与到单点注销环路中来\n\t\tif(cfg.getIsSlo()) {\n\t\t\treturn _ssoLogoutByMode3();\n\t\t}\n\n\t\t// 默认返回\n\t\treturn SaSsoConsts.NOT_HANDLE;\n\t}\n\n\t/**\n\t * SSO-Client端：接收推送消息\n\t *\n\t * @return 处理结果\n\t */\n\tpublic Object ssoPushC() {\n\t\tSaSsoClientConfig ssoClientConfig = ssoClientTemplate.getClientConfig();\n\n\t\t// 1、校验签名\n\t\tMap<String, String> paramMap = SaHolder.getRequest().getParamMap();\n\t\tif(ssoClientConfig.getIsCheckSign()) {\n\t\t\tssoClientTemplate.getSignTemplate().checkParamMap(paramMap);\n\t\t} else {\n\t\t\tSaSsoManager.printNoCheckSignWarningByRuntime();\n\t\t}\n\n\t\t// 2、处理消息\n\t\tSaSsoMessage message = new SaSsoMessage(paramMap);\n\t\treturn ssoClientTemplate.handleMessage(message);\n\t}\n\n\t/**\n\t * SSO-Client端：单点注销的回调 [模式三]\n\t * @return 处理结果\n\t */\n\tpublic Object ssoLogoutCall() {\n\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tStpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal();\n\t\tParamName paramName = ssoClientTemplate.paramName;\n\t\tSaSsoClientConfig ssoConfig = ssoClientTemplate.getClientConfig();\n\n\t\t// 获取参数\n\t\tObject loginId = req.getParamNotNull(paramName.loginId);\n\t\tloginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(loginId);\n\t\tString deviceId = req.getParam(paramName.deviceId);\n\n\t\t// 校验参数签名\n\t\tif(ssoConfig.getIsCheckSign()) {\n\t\t\tssoClientTemplate.getSignTemplate().checkRequest(req);\n\t\t} else {\n\t\t\tSaSsoManager.printNoCheckSignWarningByRuntime();\n\t\t}\n\n\t\t// 注销当前应用端会话\n\t\tSaLogoutParameter logoutParameter = ssoClientTemplate.getStpLogicOrGlobal().createSaLogoutParameter();\n\t\tstpLogic.logout(loginId, logoutParameter.setDeviceId(deviceId));\n\n\t\t// 响应\n\t\treturn SaResult.ok(\"单点注销回调成功\");\n\t}\n\n\t// 次级方法\n\n\t/**\n\t * 跳转去 sso-server 认证中心\n\t * @return /\n\t */\n\tpublic Object _goServerAuth() {\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaResponse res = SaHolder.getResponse();\n\t\tSaSsoClientConfig cfg = ssoClientTemplate.getClientConfig();\n\t\tStpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal();\n\t\tParamName paramName = ssoClientTemplate.paramName;\n\n\t\t// 获取参数\n\t\tString back = req.getParam(paramName.back, \"/\");\n\n\t\t// 如果当前 sso-client 端已经登录，则无需访问 SSO 认证中心，可以直接返回\n\t\tif(stpLogic.isLogin()) {\n\t\t\treturn res.redirect(back);\n\t\t}\n\n\t\t// 获取当前项目的 sso 登录中转页地址，形如：http://sso-client.com/sso/login\n\t\t// \t\t全局配置了就是用全局的，否则使用当前请求的地址\n\t\tString currSsoLoginUrl = cfg.getCurrSsoLogin();\n\t\tif(SaFoxUtil.isEmpty(currSsoLoginUrl)) {\n\t\t\tcurrSsoLoginUrl = SaHolder.getRequest().getUrl();\n\t\t}\n\t\t// 构建最终授权地址 url，形如：http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com\n\t\tString serverAuthUrl = ssoClientTemplate.buildServerAuthUrl(currSsoLoginUrl, back);\n\t\treturn res.redirect(serverAuthUrl);\n\t}\n\n\t/**\n\t * 根据认证中心回传的 ticket 进行登录\n\t * @return /\n\t */\n\tpublic Object _loginByTicket() {\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaResponse res = SaHolder.getResponse();\n\t\tStpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal();\n\t\tParamName paramName = ssoClientTemplate.paramName;\n\t\tApiName apiName = ssoClientTemplate.apiName;\n\n\t\t// 获取参数\n\t\tString back = req.getParam(paramName.back, \"/\");\n\t\tString ticket = req.getParam(paramName.ticket);\n\n\t\t// 1、校验 ticket，获取 loginId 等数据\n\t\tSaCheckTicketResult ctr = checkTicket(ticket, apiName.ssoLogin);\n\n\t\t// 2、如果开发者自定义了 ticket 结果值处理函数，则使用自定义的函数\n\t\tif(ssoClientTemplate.strategy.ticketResultHandle != null) {\n\t\t\treturn ssoClientTemplate.strategy.ticketResultHandle.run(ctr, back);\n\t\t}\n\n\t\t// 3、登录并重定向至back地址\n\t\tstpLogic.login(ctr.loginId, new SaLoginParameter()\n\t\t\t\t.setTimeout(ctr.remainTokenTimeout)\n\t\t\t\t.setDeviceId(ctr.deviceId)\n\t\t);\n\t\treturn res.redirect(back);\n\t}\n\n\t/**\n\t * SSO-Client端：单点注销 [模式三]\n\t * @return 处理结果\n\t */\n\tpublic Object _ssoLogoutByMode3() {\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaResponse res = SaHolder.getResponse();\n\t\tStpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal();\n\t\tboolean singleDeviceIdLogout = req.isParam(ssoClientTemplate.paramName.singleDeviceIdLogout, \"true\");\n\n\t\t// 如果未登录，则无需注销\n\t\tif( ! stpLogic.isLogin()) {\n\t\t\treturn _ssoLogoutBack(req, res);\n\t\t}\n\n\t\t// 向 sso-server 认证中心推送消息：单点注销\n\t\tSaLogoutParameter logoutParameter = stpLogic.createSaLogoutParameter();\n\t\tif(singleDeviceIdLogout) {\n\t\t\tlogoutParameter.setDeviceId(stpLogic.getLoginDeviceId());\n\t\t}\n\t\tObject loginId = stpLogic.getLoginId();\n\t\tObject centerId = ssoClientTemplate.strategy.convertLoginIdToCenterId.run(loginId);\n\t\tSaSsoMessage message = ssoClientTemplate.buildSignoutMessage(centerId, logoutParameter);\n\t\tSaResult result = ssoClientTemplate.pushMessageAsSaResult(message);\n\n\t\t// 如果 sso-server 响应的状态码非200，代表业务失败，将回应的 msg 字段作为异常抛出\n\t\tif(result.getCode() == null || SaResult.CODE_SUCCESS != result.getCode()) {\n\t\t\tthrow new SaSsoException(result.getMsg()).setCode(SaSsoErrorCode.CODE_30006);\n\t\t}\n\n\t\t// 极端场景下，sso-server 中心的单点注销可能并不会通知到当前 client 端，所以这里需要再补一刀\n\t\tif(stpLogic.isLogin()) {\n\t\t\tstpLogic.logout(loginId, logoutParameter);\n\t\t}\n\t\treturn _ssoLogoutBack(req, res);\n\t}\n\n\t/**\n\t * 封装：校验ticket，取出loginId，如果 ticket 无效则抛出异常 （适用于模式二或模式三）\n\t *\n\t * @param ticket ticket码\n\t * @return SaCheckTicketResult\n\t */\n\tpublic SaCheckTicketResult checkTicket(String ticket) {\n\t\treturn checkTicket(ticket, null);\n\t}\n\n\t/**\n\t * 封装：校验ticket，取出loginId，如果 ticket 无效则抛出异常 （适用于模式二或模式三）\n\t *\n\t * @param ticket ticket码\n\t * @param currUri 当前路由的uri，用于计算单点注销回调地址 （如果是使用模式二，可以填写null）\n\t * @return SaCheckTicketResult\n\t */\n\tpublic SaCheckTicketResult checkTicket(String ticket, String currUri) {\n\t\tSaSsoClientConfig cfg = ssoClientTemplate.getClientConfig();\n\n\t\t// 两种模式：\n\t\t//\t\tisHttp=true：模式三，使用 http 请求从认证中心校验ticket\n\t\t//\t\tisHttp=false：模式二，直连 redis 中校验 ticket\n\t\tif(cfg.getIsHttp()) {\n\t\t\treturn _checkTicketByHttp(ticket, currUri);\n\t\t} else {\n\t\t\treturn _checkTicketByRedis(ticket);\n\t\t}\n\t}\n\n\t/**\n\t * 校验 ticket，http 请求方式\n\t * @param ticket /\n\t * @param currUri /\n\t * @return /\n\t */\n\tpublic SaCheckTicketResult _checkTicketByHttp(String ticket, String currUri) {\n\t\tSaSsoClientConfig cfg = ssoClientTemplate.getClientConfig();\n\t\tApiName apiName = ssoClientTemplate.apiName;\n\t\tParamName paramName = ssoClientTemplate.paramName;\n\n\t\t// 计算当前 sso-client 的单点注销回调地址\n\t\tString ssoLogoutCall = null;\n\t\tif(cfg.getRegLogoutCall()) {\n\t\t\t// 如果配置了回调地址，就使用配置的值：\n\t\t\tif(SaFoxUtil.isNotEmpty(cfg.getCurrSsoLogoutCall())) {\n\t\t\t\tssoLogoutCall = cfg.getCurrSsoLogoutCall();\n\t\t\t}\n\t\t\t// 如果提供了当前 uri，则根据此值来计算：\n\t\t\telse if(SaFoxUtil.isNotEmpty(currUri)) {\n\t\t\t\tssoLogoutCall = SaHolder.getRequest().getUrl().replace(currUri, apiName.ssoLogoutCall);\n\t\t\t}\n\t\t\t// 否则视为不注册单点注销回调地址\n\t\t\telse {\n\t\t\t}\n\t\t}\n\n\t\t// 发起请求\n\t\tSaSsoMessage message = ssoClientTemplate.buildCheckTicketMessage(ticket, ssoLogoutCall);\n\t\tSaResult result = ssoClientTemplate.pushMessageAsSaResult(message);\n\n\t\t// 如果 sso-server 响应的状态码非200，代表业务失败，将回应的 msg 字段作为异常抛出\n\t\tif(result.getCode() == null || result.getCode() != SaResult.CODE_SUCCESS) {\n\t\t\tthrow new SaSsoException(result.getMsg()).setCode(SaSsoErrorCode.CODE_30005);\n\t\t}\n\n\t\t// 构建返回结果\n\t\tSaCheckTicketResult ctr = new SaCheckTicketResult();\n\t\tctr.loginId = result.get(paramName.loginId);\n\t\tctr.tokenValue = result.get(paramName.tokenValue, String.class);\n\t\tctr.deviceId = result.get(paramName.deviceId, String.class);\n\t\tctr.remainTokenTimeout = result.get(paramName.remainTokenTimeout, Long.class);\n\t\tctr.remainSessionTimeout = result.get(paramName.remainSessionTimeout, Long.class);\n\t\tctr.result = result;\n\n\t\t// 转换 loginId 和 centerId\n\t\tctr.centerId = ctr.loginId;\n\t\tctr.loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(ctr.centerId);\n\n\t\treturn ctr;\n\t}\n\n\t/**\n\t * 校验 ticket，直连 redis 方式\n\t * @param ticket /\n\t * @return /\n\t */\n\tpublic SaCheckTicketResult _checkTicketByRedis(String ticket) {\n\t\t// 直连 redis 校验 ticket\n\t\t// \t\t注意此处调用了 SaSsoServerProcessor 处理器里的方法，\n\t\t// \t\t这意味着如果你的 sso-server 端重写了 SaSsoServerProcessor 里的部分方法，\n\t\t// \t\t而在当前 sso-client 没有按照相应格式重写 SaSsoClientProcessor 里的方法，\n\t\t// \t\t可能会导致调用失败（注意是可能，而非一定，主要取决于你是否改变了数据读写格式），\n\t\t// \t\t解决方案为：在当前 sso-client 端也按照 sso-server 端的格式重写 SaSsoClientProcessor 里的方法\n\n\t\tStpLogic stpLogic = ssoClientTemplate.getStpLogicOrGlobal();\n\t\tTicketModel ticketModel = SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, ssoClientTemplate.getClient());\n\n\t\tSaCheckTicketResult ctr = new SaCheckTicketResult();\n\t\tctr.loginId = ticketModel.getLoginId();\n\t\tctr.tokenValue = ticketModel.getTokenValue();\n\t\tctr.deviceId = stpLogic.getLoginDeviceIdByToken(ticketModel.getTokenValue());\n\t\tctr.remainTokenTimeout = stpLogic.getTokenTimeout(ticketModel.getTokenValue());\n\t\tctr.remainSessionTimeout = stpLogic.getSessionTimeoutByLoginId(ticketModel.getLoginId());\n\t\tctr.result = null;\n\n\t\t// 转换 loginId 和 centerId\n\t\tctr.centerId = ctr.loginId;\n\t\tctr.loginId = ssoClientTemplate.strategy.convertCenterIdToLoginId.run(ctr.centerId);\n\n\t\treturn ctr;\n\t}\n\n\t/**\n\t * 封装：单点注销成功后返回结果\n\t * @param req SaRequest对象\n\t * @param res SaResponse对象\n\t * @return 返回结果\n\t */\n\tpublic Object _ssoLogoutBack(SaRequest req, SaResponse res) {\n\t\treturn SaSsoProcessorHelper.ssoLogoutBack(req, res, ssoClientTemplate.paramName);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoProcessorHelper.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.processor;\n\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.sso.name.ParamName;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * SSO 请求处理器，辅助方法\n * \n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoProcessorHelper {\n\n\t/**\n\t * 封装：单点注销成功后返回结果\n\t * @param req SaRequest对象\n\t * @param res SaResponse对象\n\t * @return 返回结果\n\t */\n\tpublic static Object ssoLogoutBack(SaRequest req, SaResponse res, ParamName paramName) {\n\n\t\t/*\n\t\t * 三种情况：\n\t\t * \t1. 有back参数，值为SELF -> 回退一级并刷新\n\t\t * \t2. 有back参数，值为url -> 跳转到此url地址\n\t\t * \t3. 无back参数 -> 返回json数据\n\t\t */\n\t\tString back = req.getParam(paramName.back);\n\t\tif(SaFoxUtil.isNotEmpty(back)) {\n\t\t\tif(back.equals(SaSsoConsts.SELF)) {\n\t\t\t\tres.setHeader(\"Content-Type\", \"text/html; charset=utf-8\");\n\t\t\t\treturn \"<script>if(document.referrer != location.href){ location.replace(document.referrer || '/'); }</script>\";\n\t\t\t}\n\t\t\treturn res.redirect(back);\n\t\t} else {\n\t\t\treturn SaResult.ok(\"单点注销成功\");\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/processor/SaSsoServerProcessor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.processor;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.error.SaSsoErrorCode;\nimport cn.dev33.satoken.sso.exception.SaSsoException;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.name.ApiName;\nimport cn.dev33.satoken.sso.name.ParamName;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport cn.dev33.satoken.util.SaSugar;\n\nimport java.util.Map;\n\n/**\n * SSO 请求处理器 （Server端）\n * \n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoServerProcessor {\n\n\t/**\n\t * 全局默认实例\n\t */\n\tpublic static SaSsoServerProcessor instance = new SaSsoServerProcessor();\n\n\t/**\n\t * 底层 SaSsoServerTemplate 对象\n\t */\n\tpublic SaSsoServerTemplate ssoServerTemplate =  new SaSsoServerTemplate();\n\n\t// ----------- SSO-Server 端路由分发 -----------\n\n\t/**\n\t * 分发 Server 端所有请求\n\t *\n\t * @return 处理结果\n\t */\n\tpublic Object dister() {\n\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tApiName apiName = ssoServerTemplate.apiName;\n\n\t\t// ------------------ 路由分发 ------------------\n\n\t\t// sso-server：授权地址\n\t\tif(req.isPath(apiName.ssoAuth)) {\n\t\t\treturn ssoAuth();\n\t\t}\n\n\t\t// sso-server：RestAPI 登录接口\n\t\tif(req.isPath(apiName.ssoDoLogin)) {\n\t\t\treturn ssoDoLogin();\n\t\t}\n\n\t\t// sso-server：单点注销\n\t\tif(req.isPath(apiName.ssoSignout)) {\n\t\t\treturn ssoSignout();\n\t\t}\n\n\t\t// sso-server：接收推送消息\n\t\tif(req.isPath(apiName.ssoPushS)) {\n\t\t\treturn ssoPushS();\n\t\t}\n\n\t\t// 默认返回\n\t\treturn SaSsoConsts.NOT_HANDLE;\n\t}\n\n\t/**\n\t * SSO-Server端：授权地址\n\t * @return 处理结果\n\t */\n\tpublic Object ssoAuth() {\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaResponse res = SaHolder.getResponse();\n\t\tSaSsoServerConfig cfg = ssoServerTemplate.getServerConfig();\n\t\tStpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal();\n\t\tParamName paramName = ssoServerTemplate.paramName;\n\n\t\t// 两种情况：\n\t\t// \t\t情况1：在 SSO 认证中心尚未登录，需要显示登录视图，去登录\n\t\t// \t\t情况2：在 SSO 认证中心已经登录，需要重定向回 Client 端\n\n\t\t// 情况1，显示登录视图\n\t\tif( ! stpLogic.isLogin()) {\n\t\t\treturn ssoServerTemplate.strategy.notLoginView.get();\n\t\t}\n\n\t\t// 情况2，开始跳转\n\t\tString mode = req.getParam(paramName.mode, SaSsoConsts.MODE_TICKET);\n\t\tString redirect = req.getParam(paramName.redirect);\n\t\tString client = req.getParam(paramName.client);\n\n\t\t// 构建最终重定向地址\n\t\tString redirectUrl = SaSugar.get(() -> {\n\n\t\t\t// 若 redirect 参数为空，说明用户并不是从 client 重定向来的，而是直接访问的 http://sso-server.com/sso/auth 地址\n\t\t\t// 此时需要跳转到配置的 homeRoute 路由上，\n\t\t\t// 若 homeRoute 也为空，则没有明确的跳转地址了，需要抛出异常\n\t\t\tif(SaFoxUtil.isEmpty(redirect)) {\n\t\t\t\tif(SaFoxUtil.isEmpty(cfg.getHomeRoute())) {\n\t\t\t\t\tthrow new SaSsoException(\"未指定 redirect 参数，也未配置 homeRoute 路由，无法完成重定向操作\").setCode(SaSsoErrorCode.CODE_30014);\n\t\t\t\t}\n\t\t\t\treturn cfg.getHomeRoute();\n\t\t\t}\n\n\t\t\t// 方式1：直接重定向回Client端 (mode=simple，一般是模式一)\n\t\t\tif(mode.equals(SaSsoConsts.MODE_SIMPLE)) {\n\t\t\t\tssoServerTemplate.checkRedirectUrl(client, redirect);\n\t\t\t\treturn redirect;\n\t\t\t} else {\n\t\t\t\t// 方式2：带着 ticket 参数重定向回Client端 (mode=ticket，一般是模式二、三)\n\n\t\t\t\t// 构建并跳转\n\t\t\t\tString _redirectUrl = ssoServerTemplate.buildRedirectUrl(client, redirect, stpLogic.getLoginId(), stpLogic.getTokenValue());\n\n\t\t\t\t// 构建成功，说明 redirect 地址合法，此时需要更新一下当前 token 有效期\n\t\t\t\tif(cfg.getAutoRenewTimeout()) {\n\t\t\t\t\tstpLogic.renewTimeout(stpLogic.getConfigOrGlobal().getTimeout());\n\t\t\t\t}\n\t\t\t\treturn _redirectUrl;\n\t\t\t}\n\t\t});\n\n\t\t// 跳转\n\t\tssoServerTemplate.strategy.jumpToRedirectUrlNotice.run(redirectUrl);\n\t\treturn res.redirect(redirectUrl);\n\t}\n\n\t/**\n\t * SSO-Server端：RestAPI 登录接口\n\t * @return 处理结果\n\t */\n\tpublic Object ssoDoLogin() {\n\t\t// 获取参数\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tParamName paramName = ssoServerTemplate.paramName;\n\t\tString name = req.getParam(paramName.name);\n\t\tString pwd = req.getParam(paramName.pwd);\n\n\t\t// 处理\n\t\treturn ssoServerTemplate.strategy.doLoginHandle.apply(name, pwd);\n\t}\n\n\t/**\n\t * SSO-Server端：单点注销\n\t * @return 处理结果\n\t */\n\tpublic Object ssoSignout() {\n\t\t// 获取对象\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tSaResponse res = SaHolder.getResponse();\n\t\tStpLogic stpLogic = ssoServerTemplate.getStpLogicOrGlobal();\n\t\tObject loginId = stpLogic.getLoginIdDefaultNull();\n\t\tboolean singleDeviceIdLogout = req.isParam(ssoServerTemplate.paramName.singleDeviceIdLogout, \"true\");\n\n\t\t// 单点注销\n\t\tif(SaFoxUtil.isNotEmpty(loginId)) {\n\t\t\tSaLogoutParameter logoutParameter = stpLogic.createSaLogoutParameter();\n\t\t\tif(singleDeviceIdLogout) {\n\t\t\t\tlogoutParameter.setDeviceId(stpLogic.getLoginDeviceId());\n\t\t\t}\n\t\t\tssoServerTemplate.ssoLogout(loginId, logoutParameter, null);\n\t\t}\n\n\t\t// 完成\n\t\treturn _ssoLogoutBack(req, res);\n\t}\n\n\t/**\n\t * SSO-Server端：接收推送消息\n\t *\n\t * @return 处理结果\n\t */\n\tpublic Object ssoPushS() {\n\t\tParamName paramName = ssoServerTemplate.paramName;\n\t\tSaSsoServerConfig ssoServerConfig = ssoServerTemplate.getServerConfig();\n\n\t\t// 1、获取参数\n\t\tSaRequest req = SaHolder.getRequest();\n\t\tString client = req.getParam(paramName.client);\n\n\t\t// 2、校验提供的client是否为非法字符\n\t\tif(SaSsoConsts.CLIENT_WILDCARD.equals(client)) {\n\t\t\treturn SaResult.error(\"无效 client 标识：\" + client);\n\t\t}\n\n\t\t// 3、校验参数签名\n\t\tMap<String, String> paramMap = req.getParamMap();\n\t\tif(ssoServerConfig.getIsCheckSign()) {\n\t\t\tssoServerTemplate.getSignTemplate(client).checkParamMap(paramMap);\n\t\t} else {\n\t\t\tSaSsoManager.printNoCheckSignWarningByRuntime();\n\t\t}\n\n\t\t// 4、处理消息\n\t\tSaSsoMessage message = new SaSsoMessage(paramMap);\n\t\tif( ! ssoServerTemplate.messageHolder.hasHandle(message.getType())) {\n\t\t\treturn SaResult.error(\"未能找到消息处理器：\" + message.getType());\n\t\t}\n\t\treturn ssoServerTemplate.handleMessage(message);\n\t}\n\n\t/**\n\t * 封装：单点注销成功后返回结果\n\t * @param req SaRequest对象\n\t * @param res SaResponse对象\n\t * @return 返回结果\n\t */\n\tpublic Object _ssoLogoutBack(SaRequest req, SaResponse res) {\n\t\treturn SaSsoProcessorHelper.ssoLogoutBack(req, res, ssoServerTemplate.paramName);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoClientStrategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.strategy;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.SaParamRetFunction;\nimport cn.dev33.satoken.sso.function.SendRequestFunction;\nimport cn.dev33.satoken.sso.function.TicketResultHandleFunction;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.Map;\n\n/**\n * Sa-Token SSO Client 相关策略\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoClientStrategy {\n\n    /**\n     * 发送 Http 请求的处理函数\n     */\n    public SendRequestFunction sendRequest = url -> {\n        return SaManager.getSaHttpTemplate().get(url);\n    };\n\n    /**\n     * 自定义校验 ticket 返回值的处理逻辑 （每次从认证中心获取校验 ticket 的结果后调用）\n     * <p> 参数：loginId, back\n     * <p> 返回值：返回给前端的值\n     */\n    public TicketResultHandleFunction ticketResultHandle = null;\n\n    /**\n     * 转换：认证中心 centerId > 本地 loginId\n     *\n     * <p> 参数：认证中心 centerId\n     * <p> 返回值：本地 loginId\n     */\n    public SaParamRetFunction<Object, Object> convertCenterIdToLoginId = (centerId) -> {\n        return centerId;\n    };\n\n    /**\n     * 转换：本地 loginId > 认证中心 centerId\n     *\n     * <p> 参数：本地 loginId\n     * <p> 返回值：认证中心 centerId\n     */\n    public SaParamRetFunction<Object, Object> convertLoginIdToCenterId = (loginId) -> {\n        return loginId;\n    };\n\n    /**\n     * 发送 Http 请求，并将响应结果转换为 SaResult\n     *\n     * @param url 请求地址\n     * @return 返回的结果\n     */\n    public SaResult requestAsSaResult(String url) {\n        String body = sendRequest.apply(url);\n        Map<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(body);\n        return new SaResult(map);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/strategy/SaSsoServerStrategy.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.strategy;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.sso.function.CheckTicketAppendDataFunction;\nimport cn.dev33.satoken.sso.function.DoLoginHandleFunction;\nimport cn.dev33.satoken.sso.function.NotLoginViewFunction;\nimport cn.dev33.satoken.sso.function.SendRequestFunction;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.Map;\n\n/**\n * Sa-Token SSO Server 相关策略\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoServerStrategy {\n\n    /**\n     * 发送 Http 请求的处理函数\n     */\n    public SendRequestFunction sendRequest = url -> {\n        return SaManager.getSaHttpTemplate().get(url);\n    };\n\n    /**\n     * 使用异步模式执行一个任务\n     */\n    public SaParamFunction<SaFunction> asyncRun = fun -> {\n        new Thread(() -> {\n            fun.run();\n        }).start();\n    };\n\n    /**\n     * 未登录时返回的 View\n     */\n    public NotLoginViewFunction notLoginView = () -> {\n        return \"当前会话在 SSO-Server 认证中心尚未登录（当前未配置登录视图）\";\n    };\n\n    /**\n     * SSO-Server端：登录函数\n     */\n    public DoLoginHandleFunction doLoginHandle = (name, pwd) -> {\n        return SaResult.error();\n    };\n\n    /**\n     * SSO-Server端：在授权重定向之前的通知\n     */\n    public SaParamFunction<String> jumpToRedirectUrlNotice = (redirectUrl) -> {\n\n    };\n\n    /**\n     * SSO-Server端：在校验 ticket 后，给 sso-client 端追加返回信息的函数\n     */\n    public CheckTicketAppendDataFunction checkTicketAppendData = (loginId, result) -> {\n        return result;\n    };\n\n    /**\n     * 发送 Http 请求，并将响应结果转换为 SaResult\n     *\n     * @param url 请求地址\n     * @return 返回的结果\n     */\n    public SaResult requestAsSaResult(String url) {\n        String body = sendRequest.apply(url);\n        Map<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(body);\n        return new SaResult(map);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.template;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.template.SaSignTemplate;\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.error.SaSsoErrorCode;\nimport cn.dev33.satoken.sso.exception.SaSsoException;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.message.handle.client.SaSsoMessageLogoutCallHandle;\nimport cn.dev33.satoken.sso.strategy.SaSsoClientStrategy;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.Map;\n\n/**\n * SSO 模板方法类 （Client端）\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoClientTemplate extends SaSsoTemplate {\n\n    /**\n     * Client 相关策略\n     */\n    public SaSsoClientStrategy strategy = new SaSsoClientStrategy();\n\n    public SaSsoClientTemplate() {\n        super.messageHolder.addHandle(new SaSsoMessageLogoutCallHandle());\n    }\n\n\n    // ------------------- getData 相关 -------------------\n\n    /**\n     * 根据配置的 getData 地址，查询数据\n     *\n     * @param paramMap 查询参数\n     * @return 查询结果\n     */\n    public Object getData(Map<String, Object> paramMap) {\n        String getDataUrl = getClientConfig().splicingGetDataUrl();\n        return getData(getDataUrl, paramMap);\n    }\n\n    /**\n     * 根据自定义 path 地址，查询数据 （此方法需要配置 sa-token.sso.server-url 地址）\n     *\n     * @param path 自定义 path\n     * @param paramMap 查询参数\n     * @return 查询结果\n     */\n    public Object getData(String path, Map<String, Object> paramMap) {\n        String url = buildCustomPathUrl(path, paramMap);\n        return strategy.sendRequest.apply(url);\n    }\n\n    /**\n     * 构建URL：Server端 getData 地址，带签名等参数\n     * @param paramMap 查询参数\n     * @return /\n     */\n    public String buildGetDataUrl(Map<String, Object> paramMap) {\n        String getDataUrl = getClientConfig().getGetDataUrl();\n        return buildCustomPathUrl(getDataUrl, paramMap);\n    }\n\n    /**\n     * 构建URL：Server 端自定义 path 地址，带签名等参数 （此方法需要配置 sa-token.sso.server-url 地址）\n     * @param paramMap 请求参数\n     * @return /\n     */\n    public String buildCustomPathUrl(String path, Map<String, Object> paramMap) {\n        SaSsoClientConfig ssoConfig = getClientConfig();\n\n        // 构建 url\n        // 如果 path 不是以 http 开头，那么就拼接上 serverUrl\n        String url = path;\n        if( ! url.startsWith(\"http\") ) {\n            String serverUrl = ssoConfig.getServerUrl();\n            SaSsoException.notEmpty(serverUrl, \"请先配置 sa-token.sso-client.server-url 地址\", SaSsoErrorCode.CODE_30012);\n            url = SaFoxUtil.spliceTwoUrl(serverUrl, path);\n        }\n\n        // 构建参数字符串\n        paramMap.put(paramName.client, getClient());\n        String signParamsStr = getSignTemplate().addSignParamsAndJoin(paramMap);\n\n        // 拼接\n        return SaFoxUtil.joinParam(url, signParamsStr);\n    }\n\n\n    // ---------------------- 构建交互地址 ----------------------\n\n    /**\n     * 构建URL：Server端 单点登录授权地址，\n     * <br/> 形如：http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com\n     * @param clientLoginUrl Client端登录地址\n     * @param back 回调路径\n     * @return [SSO-Server端-认证地址 ]\n     */\n    public String buildServerAuthUrl(String clientLoginUrl, String back) {\n        SaSsoClientConfig ssoConfig = getClientConfig();\n\n        // 服务端认证地址\n        String serverUrl = ssoConfig.splicingAuthUrl();\n\n        // 拼接客户端标识\n        String client = getClient();\n        if(SaFoxUtil.isNotEmpty(client)) {\n            serverUrl = SaFoxUtil.joinParam(serverUrl, paramName.client, client);\n        }\n\n        // 对back地址编码\n        back = (back == null ? \"\" : back);\n        back = SaFoxUtil.encodeUrl(back);\n\n        // 开始拼接 sso 统一认证地址，形如：serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com\n\n        /*\n         * 部分 Servlet 版本 request.getRequestURL() 返回的 url 带有 query 参数，形如：http://domain.com?id=1，\n         * 如果不加判断会造成最终生成的 serverAuthUrl 带有双 back 参数 ，这个 if 判断正是为了解决此问题\n         */\n        if( ! clientLoginUrl.contains(paramName.back + \"=\") ) {\n            clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, paramName.back, back);\n        }\n\n        // 返回\n        return SaFoxUtil.joinParam(serverUrl, paramName.redirect, clientLoginUrl);\n    }\n\n\n    // ------------------- 消息推送 -------------------\n\n    /**\n     * 向 sso-server 推送消息\n     *\n     * @param message /\n     * @return /\n     */\n    public String pushMessage(SaSsoMessage message) {\n        SaSsoClientConfig ssoConfig = getClientConfig();\n\n        // 拼接 push-url 地址\n        String pushUrl = ssoConfig.splicingPushUrl();\n        SaSsoException.notTrue(! SaFoxUtil.isUrl(pushUrl), \"无效 push-url 地址：\" + pushUrl, SaSsoErrorCode.CODE_30023);\n\n        // 组织参数\n        message.set(paramName.client, getClient());\n        message.checkType();\n        String paramsStr = getSignTemplate().addSignParamsAndJoin(message);\n\n        // 发起请求\n        String finalUrl = SaFoxUtil.joinParam(pushUrl, paramsStr);\n        return strategy.sendRequest.apply(finalUrl);\n    }\n\n    /**\n     * 向 sso-server 推送消息，并将返回值转为 SaResult\n     *\n     * @param message /\n     * @return /\n     */\n    public SaResult pushMessageAsSaResult(SaSsoMessage message) {\n        String res = pushMessage(message);\n        Map<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(res);\n        return new SaResult(map);\n    }\n\n    /**\n     * 构建消息：校验 ticket\n     *\n     * @param ticket ticket码\n     * @param ssoLogoutCallUrl 单点注销时的回调URL\n     * @return 构建完毕的URL\n     */\n    public SaSsoMessage buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl) {\n        SaSsoClientConfig ssoConfig = getClientConfig();\n        SaSsoMessage message = new SaSsoMessage();\n        message.setType(SaSsoConsts.MESSAGE_CHECK_TICKET);\n        message.set(paramName.client, getClient());\n        message.set(paramName.ticket, ticket);\n        message.set(paramName.ssoLogoutCall, ssoLogoutCallUrl);\n        return message;\n    }\n\n    /**\n     * 构建消息：单点注销\n     *\n     * @param loginId 要注销的账号 id\n     * @param logoutParameter 单点注销\n     * @return 单点注销URL\n     */\n    public SaSsoMessage buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter) {\n        SaSsoMessage message = new SaSsoMessage();\n        message.setType(SaSsoConsts.MESSAGE_SIGNOUT);\n        message.set(paramName.client, getClient());\n        message.set(paramName.loginId, loginId);\n        message.set(paramName.deviceId, logoutParameter.getDeviceId());\n        return message;\n    }\n\n\n    // ------------------- Bean 对象获取 -------------------\n\n    /**\n     * 获取底层使用的SsoClient配置对象\n     * @return /\n     */\n    public SaSsoClientConfig getClientConfig() {\n        return SaSsoManager.getClientConfig();\n    }\n\n    /**\n     * 获取当前项目 client 标识\n     * @return /\n     */\n    public String getClient() {\n        return getClientConfig().getClient();\n    }\n\n    /**\n     * 获取底层使用的 API 签名对象\n     *\n     * @return /\n     */\n    public SaSignTemplate getSignTemplate() {\n        SaSignConfig signConfig = SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().copy();\n\n        // 使用 secretKey 的优先级：SSO 模块全局配置 > sign 模块默认配置\n        String secretKey = getClientConfig().getSecretKey();\n        if(SaFoxUtil.isEmpty(secretKey)) {\n            secretKey = signConfig.getSecretKey();\n        }\n        signConfig.setSecretKey(secretKey);\n\n        return new SaSignTemplate(signConfig);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.template;\n\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.Map;\n\n/**\n * SSO 模板方法类 （Client端）\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoClientUtil {\n\n    private SaSsoClientUtil() {\n    }\n\n    /**\n     * 返回底层使用的 SaSsoClientTemplate 对象\n     * @return /\n     */\n    public static SaSsoClientTemplate getSsoTemplate() {\n        return SaSsoClientProcessor.instance.ssoClientTemplate;\n    }\n\n\n    // ------------------- getData 相关 -------------------\n\n    /**\n     * 根据配置的 getData 地址，查询数据\n     *\n     * @param paramMap 查询参数\n     * @return 查询结果\n     */\n    public static Object getData(Map<String, Object> paramMap) {\n        return SaSsoClientProcessor.instance.ssoClientTemplate.getData(paramMap);\n    }\n\n    /**\n     * 根据自定义 path 地址，查询数据 （此方法需要配置 sa-token.sso.server-url 地址）\n     *\n     * @param path 自定义 path\n     * @param paramMap 查询参数\n     * @return 查询结果\n     */\n    public static Object getData(String path, Map<String, Object> paramMap) {\n        return SaSsoClientProcessor.instance.ssoClientTemplate.getData(path, paramMap);\n    }\n\n\n    // ---------------------- 构建交互地址 ----------------------\n\n    /**\n     * 构建URL：Server端 单点登录授权地址，\n     * <br/> 形如：http://sso-server.com/sso/auth?redirectUrl=http://sso-client.com/sso/login?back=http://sso-client.com\n     * @param clientLoginUrl Client端登录地址\n     * @param back 回调路径\n     * @return [SSO-Server端-认证地址 ]\n     */\n    public static String buildServerAuthUrl(String clientLoginUrl, String back) {\n        return SaSsoClientProcessor.instance.ssoClientTemplate.buildServerAuthUrl(clientLoginUrl, back);\n    }\n\n\n    // ------------------- 消息推送 -------------------\n\n    /**\n     * 向 sso-server 推送消息\n     *\n     * @param message /\n     * @return /\n     */\n    public static String pushMessage(SaSsoMessage message) {\n        return SaSsoClientProcessor.instance.ssoClientTemplate.pushMessage(message);\n    }\n\n    /**\n     * 向 sso-server 推送消息，并将返回值转为 SaResult\n     *\n     * @param message /\n     * @return /\n     */\n    public static SaResult pushMessageAsSaResult(SaSsoMessage message) {\n        return SaSsoClientProcessor.instance.ssoClientTemplate.pushMessageAsSaResult(message);\n    }\n\n    /**\n     * 构建消息：校验 ticket\n     *\n     * @param ticket ticket码\n     * @param ssoLogoutCallUrl 单点注销时的回调URL\n     * @return 构建完毕的URL\n     */\n    public static SaSsoMessage buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl) {\n        return SaSsoClientProcessor.instance.ssoClientTemplate.buildCheckTicketMessage(ticket, ssoLogoutCallUrl);\n    }\n\n    /**\n     * 构建消息：单点注销\n     *\n     * @param loginId 要注销的账号 id\n     * @param logoutParameter 单点注销\n     * @return 单点注销URL\n     */\n    public static SaSsoMessage buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter) {\n        return SaSsoClientProcessor.instance.ssoClientTemplate.buildSignoutMessage(loginId, logoutParameter);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.template;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.template.SaSignTemplate;\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientModel;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.error.SaSsoErrorCode;\nimport cn.dev33.satoken.sso.exception.SaSsoException;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.message.handle.server.SaSsoMessageCheckTicketHandle;\nimport cn.dev33.satoken.sso.message.handle.server.SaSsoMessageSignoutHandle;\nimport cn.dev33.satoken.sso.model.SaSsoClientInfo;\nimport cn.dev33.satoken.sso.model.TicketModel;\nimport cn.dev33.satoken.sso.strategy.SaSsoServerStrategy;\nimport cn.dev33.satoken.sso.util.SaSsoConsts;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.*;\n\n/**\n * SSO 模板方法类 （Server端）\n *\n * @author click33\n * @since 1.38.0\n */\npublic class SaSsoServerTemplate extends SaSsoTemplate {\n\n    /**\n     * Server 相关策略\n     */\n    public SaSsoServerStrategy strategy = new SaSsoServerStrategy();\n\n    public SaSsoServerTemplate() {\n        super.messageHolder.addHandle(new SaSsoMessageCheckTicketHandle());\n        super.messageHolder.addHandle(new SaSsoMessageSignoutHandle());\n    }\n\n    // ---------------------- Ticket 操作 ----------------------\n\n    // 增删改\n\n    /**\n     * 保存 Ticket\n     * @param ticketModel /\n     */\n    public void saveTicket(TicketModel ticketModel) {\n        long ticketTimeout = getServerConfig().getTicketTimeout();\n        SaManager.getSaTokenDao().setObject(splicingTicketModelSaveKey(ticketModel.getTicket()), ticketModel, ticketTimeout);\n    }\n\n    /**\n     * 删除 Ticket\n     * @param ticket Ticket码\n     */\n    public void deleteTicket(String ticket) {\n        if(ticket == null) {\n            return;\n        }\n        SaManager.getSaTokenDao().deleteObject(splicingTicketModelSaveKey(ticket));\n    }\n\n    /**\n     * 根据参数创建一个 ticket 码\n     *\n     * @param client 客户端标识\n     * @param loginId 账号 id\n     * @param tokenValue 会话 Token\n     * @return Ticket码\n     */\n    public TicketModel createTicket(String client, Object loginId, String tokenValue) {\n        TicketModel ticketModel = new TicketModel();\n        ticketModel.setTicket(randomTicket(loginId));\n        ticketModel.setClient(client);\n        ticketModel.setLoginId(loginId);\n        ticketModel.setTokenValue(tokenValue);\n        return ticketModel;\n    }\n\n    /**\n     * 根据参数创建一个 ticket 码，并保存\n     *\n     * @param client 客户端标识\n     * @param loginId 账号 id\n     * @param tokenValue 会话 Token\n     * @return Ticket码\n     */\n    public String createTicketAndSave(String client, Object loginId, String tokenValue) {\n        // 创建\n        TicketModel ticketModel = createTicket(client, loginId, tokenValue);\n\n        // 保存\n        saveTicket(ticketModel);\n        saveTicketIndex(client, loginId, ticketModel.getTicket());\n\n        // 返回\n        return ticketModel.getTicket();\n    }\n\n    /**\n     * 随机一个 Ticket 码\n     * @param loginId 账号id\n     * @return Ticket 码\n     */\n    public String randomTicket(Object loginId) {\n        return SaFoxUtil.getRandomString(64);\n    }\n\n    // 查\n\n    /**\n     * 查询 ticket ，如果 ticket 无效则返回 null\n     *\n     * @param ticket Ticket码\n     * @return 账号id\n     */\n    public TicketModel getTicket(String ticket) {\n        if(SaFoxUtil.isEmpty(ticket)) {\n            return null;\n        }\n        return SaManager.getSaTokenDao().getObject(splicingTicketModelSaveKey(ticket), TicketModel.class);\n    }\n\n    /**\n     * 查询 ticket 指向的 loginId，如果 ticket 码无效则返回 null\n     * @param ticket Ticket码\n     * @return 账号id\n     */\n    public Object getLoginId(String ticket) {\n        TicketModel ticketModel = getTicket(ticket);\n        if(ticketModel == null) {\n            return null;\n        }\n        return ticketModel.getLoginId();\n    }\n\n    /**\n     * 查询 ticket 指向的 loginId，并转换为指定类型\n     * @param <T> 要转换的类型\n     * @param ticket Ticket码\n     * @param cs 要转换的类型\n     * @return 账号id\n     */\n    public <T> T getLoginId(String ticket, Class<T> cs) {\n        return SaFoxUtil.getValueByType(getLoginId(ticket), cs);\n    }\n\n    // 校验\n\n    /**\n     * 校验 Ticket，无效 ticket 会抛出异常\n     *\n     * @param ticket Ticket码\n     * @return /\n     */\n    public TicketModel checkTicket(String ticket) {\n        TicketModel ticketModel = getTicket(ticket);\n        if(ticketModel == null) {\n            throw new SaSsoException(\"无效 ticket : \" + ticket).setCode(SaSsoErrorCode.CODE_30004);\n        }\n        return ticketModel;\n    }\n\n    /**\n     * 校验 Ticket 码，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\n     * @param ticket Ticket码\n     * @return 账号id\n     */\n    public TicketModel checkTicketParamAndDelete(String ticket) {\n        return checkTicketParamAndDelete(ticket, SaSsoConsts.CLIENT_WILDCARD);\n    }\n\n    /**\n     * 校验 Ticket，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\n     *\n     * @param ticket Ticket码\n     * @param client client 标识\n     * @return /\n     */\n    public TicketModel checkTicketParamAndDelete(String ticket, String client) {\n        TicketModel ticketModel = checkTicket(ticket);\n\n        // 校验 client 参数是否正确，即：创建 ticket 的 client 和当前校验 ticket 的 client 是否一致\n        String ticketClient = ticketModel.getClient();\n        if(SaSsoConsts.CLIENT_WILDCARD.equals(client)) {\n            // 如果提供的是通配符，直接越过 client 校验\n        } else if (SaFoxUtil.isEmpty(client) && SaFoxUtil.isEmpty(ticketClient)) {\n            // 如果提供的和期望的两者均为空，则通过校验\n        } else {\n            // 开始详细比对\n            if(SaFoxUtil.notEquals(client, ticketClient)) {\n                throw new SaSsoException(\"该 ticket 不属于 client=\" + client + \", ticket 值: \" + ticket).setCode(SaSsoErrorCode.CODE_30011);\n            }\n        }\n\n        // 删除 ticket 信息，使其只有一次性有效\n        deleteTicket(ticket);\n        deleteTicketIndex(client, ticketModel.getLoginId());\n\n        //\n        return ticketModel;\n    }\n\n    // ticket 索引\n\n    /**\n     * 保存 Ticket 索引 （id 反查 ticket）\n     *\n     * @param client 应用端\n     * @param ticket ticket码\n     * @param loginId 账号id\n     */\n    public void saveTicketIndex(String client, Object loginId, String ticket) {\n        long ticketTimeout = getServerConfig().getTicketTimeout();\n        SaManager.getSaTokenDao().set(splicingTicketIndexKey(client, loginId), String.valueOf(ticket), ticketTimeout);\n    }\n\n    /**\n     * 删除 Ticket 索引\n     *\n     * @param client 应用标识\n     * @param loginId 账号id\n     */\n    public void deleteTicketIndex(String client, Object loginId) {\n        if(loginId == null) {\n            return;\n        }\n        SaManager.getSaTokenDao().delete(splicingTicketIndexKey(client, loginId));\n    }\n\n    /**\n     * 查询 指定 client、loginId 其所属的 ticket 值\n     *\n     * @param client 应用\n     * @param loginId 账号id\n     * @return Ticket值\n     */\n    public String getTicketValue(String client, Object loginId) {\n        if(loginId == null) {\n            return null;\n        }\n        return SaManager.getSaTokenDao().get(splicingTicketIndexKey(client, loginId));\n    }\n\n\n    // ---------------------- Client 信息获取 ----------------------\n\n    /**\n     * 获取所有 Client\n     *\n     * @return /\n     */\n    public List<SaSsoClientModel> getClients() {\n        return new ArrayList<>(getServerConfig().getClients().values());\n    }\n\n    /**\n     * 获取应用信息，无效 client 返回 null\n     *\n     * @param client /\n     * @return /\n     */\n    public SaSsoClientModel getClient(String client) {\n        return getServerConfig().getClients().get(client);\n    }\n\n    /**\n     * 获取应用信息，无效 client 则抛出异常\n     *\n     * @param client /\n     * @return /\n     */\n    public SaSsoClientModel getClientNotNull(String client) {\n        if(SaFoxUtil.isEmpty(client)) {\n            if(getConfigOfAllowAnonClient()) {\n                return getAnonClient();\n            } else {\n                throw new SaSsoException(\"client 标识不可为空\");\n            }\n        } else {\n            SaSsoClientModel scm = getClient(client);\n            if(scm == null) {\n                throw new SaSsoException(\"未能获取应用信息，client=\" + client).setCode(SaSsoErrorCode.CODE_30013);\n            }\n            return scm;\n        }\n    }\n\n    /**\n     * 获取配置项：是否允许匿名 client 接入\n     *\n     * @return /\n     */\n    public boolean getConfigOfAllowAnonClient() {\n        return getServerConfig().getAllowAnonClient();\n    }\n\n    /**\n     * 获取匿名 client 配置信息\n     *\n     * @return /\n     */\n    public SaSsoClientModel getAnonClient() {\n        SaSsoServerConfig serverConfig = getServerConfig();\n        SaSsoClientModel scm = new SaSsoClientModel();\n        scm.setAllowUrl(serverConfig.getAllowUrl());\n        scm.setIsSlo(serverConfig.getIsSlo());\n        scm.setSecretKey(serverConfig.getSecretKey());\n        if(SaFoxUtil.isEmpty(scm.getSecretKey())) {\n            scm.setSecretKey(SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().getSecretKey());\n        }\n        return scm;\n    }\n\n    /**\n     * 获取所有需要接收消息推送的 Client\n     *\n     * @return /\n     */\n    public List<SaSsoClientModel> getNeedPushClients() {\n        List<SaSsoClientModel> list = new ArrayList<>();\n        List<SaSsoClientModel> clients = getClients();\n        for(SaSsoClientModel scm : clients) {\n            if (scm.getIsPush()) {\n                list.add(scm);\n            }\n        }\n        return list;\n    }\n\n\n    // ------------------- 重定向 URL 构建与校验 -------------------\n\n    /**\n     * 构建 URL：sso-server 端向 sso-client 下放 ticket 的地址\n     *\n     * @param client 客户端标识\n     * @param redirect sso-client 端的重定向地址\n     * @param loginId 账号 id\n     * @param tokenValue 会话 token\n     * @return /\n     */\n    public String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) {\n\n        // 校验 重定向地址 是否合法\n        checkRedirectUrl(client, redirect);\n\n        // 删掉 旧Ticket\n        deleteTicket(getTicketValue(client, loginId));\n\n        // 创建 新Ticket\n        String ticket = createTicketAndSave(client, loginId, tokenValue);\n\n        // 构建 授权重定向地址 （Server端 根据此地址向 Client端 下放 Ticket）\n        return SaFoxUtil.joinParam(encodeBackParam(redirect), paramName.ticket, ticket);\n    }\n\n    /**\n     * 对 url 中的 back 参数进行 URL 编码, 解决超链接重定向后参数丢失的 bug\n     *\n     * @param url url\n     * @return 编码过后的url\n     */\n    public String encodeBackParam(String url) {\n\n        // 获取back参数所在位置\n        int index = url.indexOf(\"?\" + paramName.back + \"=\");\n        if(index == -1) {\n            index = url.indexOf(\"&\" + paramName.back + \"=\");\n            if(index == -1) {\n                return url;\n            }\n        }\n\n        // 开始编码\n        int length = paramName.back.length() + 2;\n        String back = url.substring(index + length);\n        back = SaFoxUtil.encodeUrl(back);\n\n        // 放回url中\n        url = url.substring(0, index + length) + back;\n        return url;\n    }\n\n    /**\n     * 校验重定向 url 合法性\n     *\n     * @param client 应用标识\n     * @param url 下放ticket的url地址\n     */\n    public void checkRedirectUrl(String client, String url) {\n\n        // 1、是否是一个有效的url\n        if( ! SaFoxUtil.isUrl(url) ) {\n            throw new SaSsoException(\"无效redirect：\" + url).setCode(SaSsoErrorCode.CODE_30001);\n        }\n\n        // 2、截取掉?后面的部分\n        int qIndex = url.indexOf(\"?\");\n        if(qIndex != -1) {\n            url = url.substring(0, qIndex);\n        }\n\n        // 3、不允许出现@字符\n        if(url.contains(\"@\")) {\n            //  为什么不允许出现 @ 字符呢，因为这有可能导致 redirect 参数绕过 AllowUrl 列表的校验\n            //\n            //  举个例子 配置文件：\n            //       sa-token.sso-server.allow-url=http://sa-sso-client1.com*\n            //\n            //  开发者原意是为了允许 sa-sso-client1.com 下的所有地址都可以下放ticket\n            //\n            //  但是如果攻击者精心构建一个url：\n            //       http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com@sa-token.cc\n            //\n            //  那么这个url就会绕过 allow-url 的校验，ticket 被下发到了第三方服务器地址：\n            //       http://sa-token.cc/?ticket=i8vDfbpqBViMe01QoLY1kHROJWYvv9plBtvTZ6kk77KK0e0U4Xj99NPfSZEYjRul\n            //\n            //  造成了ticket 参数劫持\n            //  所以此处需要禁止在 url 中出现 @ 字符\n            //\n            //  这么一刀切的做法，可能会导致一些特殊的正常url也无法通过校验，例如：\n            //       http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-sso-client1.com:9003/@getInfo\n            //\n            //  但是为了安全起见，这么做还是有必要的\n            throw new SaSsoException(\"无效redirect（不允许出现@字符）：\" + url).setCode(SaSsoErrorCode.CODE_30001);\n        }\n\n        // 4、判断是否在 [ 允许的地址列表 ] 之中\n        String allowUrlString = getClientNotNull(client).getAllowUrl();\n        List<String> allowUrlList = Arrays.asList(allowUrlString.replaceAll(\" \", \"\").split(\",\"));\n        checkAllowUrlList(allowUrlList);\n        if( ! SaStrategy.instance.hasElement.apply(allowUrlList, url) ) {\n            throw new SaSsoException(\"非法redirect：\" + url).setCode(SaSsoErrorCode.CODE_30002);\n        }\n\n        // 校验通过 √\n    }\n\n    /**\n     * 校验配置的 AllowUrl 是否合规，如果不合规则抛出异常\n     * @param allowUrlList 待校验的 allow-url 地址列表 \n     */\n    public void checkAllowUrlList(List<String> allowUrlList){\n        checkAllowUrlListStaticMethod(allowUrlList);\n    }\n\n    /**\n     * 校验配置的 AllowUrl 是否合规，如果不合规则抛出异常\n     * @param allowUrlList 待校验的 allow-url 地址列表\n     */\n    public static void checkAllowUrlListStaticMethod(List<String> allowUrlList){\n        for (String url : allowUrlList) {\n            int index = url.indexOf(\"*\");\n            // 如果配置了 * 字符，则必须出现在最后一位，否则属于无效配置项\n            if(index != -1 && index != url.length() - 1) {\n                //  为什么不允许 * 字符出现在中间位置呢，因为这有可能导致 redirect 参数绕过 allow-url 列表的校验\n                //\n                //  举个例子 配置文件：\n                //      sa-token.sso-server.allow-url=http://*.sa-sso-client1.com\n                //\n                //  开发者原意是为了允许 sa-sso-client1.com 下的所有子域名都可以下放ticket\n                //      例如：http://shop.sa-sso-client1.com\n                //\n                //  但是如果攻击者精心构建一个url：\n                //       http://sa-sso-server.com:9000/sso/auth?redirect=http://sa-token.cc/a.sa-sso-client1.com/sso/login\n                //\n                //  那么这个 url 就会绕过 allow-url 的校验，ticket 被下发到了第三方服务器地址：\n                //       http://sa-token.cc/a.sa-sso-client1.com/sso/login?ticket=v2KKMUFK7dDsMMzXLQ3aWGsyGUjrA0dBB2jeOWrpCnC8b5ScmXXQSv20mIwPK7Cx\n                //\n                //  造成了 ticket 参数劫持\n                //  所以此处需要禁止 allow-url 配置项的中间位置出现 * 字符（出现在末尾是没有问题的）\n                //\n                //  这么一刀切的做法，可能会导致正常场景下的子域名url也无法通过校验，例如：\n                //       http://sa-sso-server.com:9000/sso/auth?redirect=http://shop.sa-sso-client1.com/sso/login\n                //\n                //  但是为了安全起见，这么做还是有必要的\n                throw new SaSsoException(\"无效的 allow-url 配置（*通配符只允许出现在最后一位）：\" + url).setCode(SaSsoErrorCode.CODE_30015);\n            }\n        }\n    }\n\n\n    // ------------------- 单点注销 -------------------\n\n    /**\n     * 为指定账号 id 注册应用接入信息（模式三）\n     *\n     * @param loginId 账号id\n     * @param client 指定客户端标识，可为null\n     * @param sloCallbackUrl 单点注销时的回调URL\n     */\n    public void registerSloCallbackUrl(Object loginId, String client, String sloCallbackUrl) {\n        // 如果提供的参数是空值，则直接返回，不进行任何操作\n        if(SaFoxUtil.isEmpty(loginId)) {\n            return;\n        }\n\n        SaSession session = getStpLogicOrGlobal().getSessionByLoginId(loginId);\n\n        // 取出原来的\n        List<SaSsoClientInfo> scmList = session.get(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, ArrayList::new);\n\n        // 将 新登录client 加入到集合中\n        SaSsoClientInfo scm = new SaSsoClientInfo(client, sloCallbackUrl, calcNextIndex(scmList));\n        scmList.add(scm);\n\n        // 如果登录的client数量超过了限制，则从最早的一个登录开始清退\n        int maxRegClient = getServerConfig().maxRegClient;\n        if(maxRegClient != -1)  {\n            for (;;) {\n                if(scmList.size() > maxRegClient) {\n                    SaSsoClientInfo removeScm = scmList.remove(0);\n                    strategy.asyncRun.run(() -> {\n                        notifyClientLogout(loginId, null, removeScm, true, true);\n                    });\n                } else {\n                    break;\n                }\n            }\n        }\n\n        // 存入持久库\n        session.set(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, scmList);\n    }\n\n    /**\n     * 计算下一个 index 值\n     * @param scmList /\n     * @return /\n     */\n    public int calcNextIndex(List<SaSsoClientInfo> scmList) {\n        // 如果目前还没有任何登录记录，则直接返回0\n        if(scmList == null || scmList.isEmpty()) {\n            return 0;\n        }\n        // 获取目前最大的index值\n        int maxIndex = scmList.get(scmList.size() - 1).index;\n\n        // 如果已经是 int 最大值了，则直接返回0\n        if(maxIndex == Integer.MAX_VALUE) {\n            return 0;\n        }\n\n        // 否则返回最大值+1\n        maxIndex++;\n        return maxIndex;\n    }\n\n    /**\n     * 指定账号单点注销\n     *\n     * @param loginId 指定账号\n     */\n    public void ssoLogout(Object loginId) {\n        ssoLogout(loginId, getStpLogicOrGlobal().createSaLogoutParameter(), null);\n    }\n\n    /**\n     * 指定账号单点注销\n     *\n     * @param loginId 指定账号\n     * @param logoutParameter 注销参数\n     * @param ignoreClient 要被忽略掉的 client，填 null 代表不忽略\n     */\n    public void ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) {\n\n        // 1、消息推送：单点注销\n        pushToAllClientByLogoutCall(loginId, logoutParameter, ignoreClient);\n\n        // 2、SaSession 挂载的 Client 端注销会话\n        SaSession session = getStpLogicOrGlobal().getSessionByLoginId(loginId, false);\n        if(session == null) {\n            return;\n        }\n        List<SaSsoClientInfo> scmList = session.get(SaSsoConsts.SSO_CLIENT_MODEL_LIST_KEY_, ArrayList::new);\n        scmList.forEach(scm -> {\n            strategy.asyncRun.run(() -> {\n                notifyClientLogout(loginId, logoutParameter.getDeviceId(), scm, false, false);\n            });\n        });\n\n        // 3、Server 端本身注销\n        getStpLogicOrGlobal().logout(loginId, logoutParameter);\n    }\n\n    /**\n     * 通知指定账号的指定客户端注销\n     *\n     * @param loginId 指定账号\n     * @param deviceId 指定设备 id\n     * @param scm 客户端信息对象\n     * @param autoLogout 是否为超过 maxRegClient 的自动注销\n     * @param isPushWork 如果该 client 没有注册注销回调地址，是否使用 push 消息的方式进行注销回调通知\n     *\n     * @return /\n     */\n    public String notifyClientLogout(Object loginId, String deviceId, SaSsoClientInfo scm, boolean autoLogout, boolean isPushWork) {\n\n        // 如果给个null值，不进行任何操作\n        if(scm == null || scm.mode != SaSsoConsts.SSO_MODE_3) {\n            return null;\n        }\n\n        // 如果此 Client 并没有注册 单点注销 回调地址\n        String sloCallUrl = scm.getSloCallbackUrl();\n        if(SaFoxUtil.isEmpty(sloCallUrl)) {\n            if(isPushWork && SaFoxUtil.isNotEmpty(scm.getClient())) {\n                SaSsoClientModel client = getClient(scm.getClient());\n                return pushToClientByLogoutCall(client, loginId, true, getStpLogicOrGlobal().createSaLogoutParameter());\n            }\n            return null;\n        }\n\n        // 参数\n        Map<String, Object> paramsMap = new TreeMap<>();\n        paramsMap.put(paramName.client, scm.getClient());\n        paramsMap.put(paramName.loginId, loginId);\n        paramsMap.put(paramName.deviceId, deviceId);\n        paramsMap.put(paramName.autoLogout, autoLogout);\n        String signParamsStr = getSignTemplate(scm.getClient()).addSignParamsAndJoin(paramsMap);\n\n        // 拼接\n        String finalUrl = SaFoxUtil.joinParam(sloCallUrl, signParamsStr);\n\n        // 发起请求\n        return strategy.sendRequest.apply(finalUrl);\n    }\n\n\n    // ------------------- 消息推送 -------------------\n\n    /**\n     * 向指定 Client 推送消息\n     * @param clientModel /\n     * @param message /\n     * @return /\n     */\n    public String pushMessage(SaSsoClientModel clientModel, SaSsoMessage message) {\n        message.checkType();\n        String noticeUrl = clientModel.splicingPushUrl();\n        String paramsStr = getSignTemplate(clientModel.getClient()).addSignParamsAndJoin(message);\n        String finalUrl = SaFoxUtil.joinParam(noticeUrl, paramsStr);\n        return strategy.sendRequest.apply(finalUrl);\n    }\n\n    /**\n     * 向指定 client 推送消息，并将返回值转为 SaResult\n     *\n     * @param clientModel /\n     * @param message /\n     * @return /\n     */\n    public SaResult pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message) {\n        String res = pushMessage(clientModel, message);\n        Map<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(res);\n        return new SaResult(map);\n    }\n\n    /**\n     * 向指定 Client 推送消息\n     * @param client /\n     * @param message /\n     * @return /\n     */\n    public String pushMessage(String client, SaSsoMessage message) {\n        return pushMessage(getClientNotNull(client), message);\n    }\n\n    /**\n     * 向指定 client 推送消息，并将返回值转为 SaResult\n     *\n     * @param client /\n     * @param message /\n     * @return /\n     */\n    public SaResult pushMessageAsSaResult(String client, SaSsoMessage message) {\n        String res = pushMessage(client, message);\n        Map<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(res);\n        return new SaResult(map);\n    }\n\n    /**\n     * 向所有 Client 推送消息\n     *\n     * @param message /\n     */\n    public void pushToAllClient(SaSsoMessage message) {\n        pushToAllClient(message, null);\n    }\n\n    /**\n     * 向所有 Client 推送消息，并忽略掉某个 client\n     *\n     * @param ignoreClient 要被忽略掉的 client，填 null 代表不忽略\n     * @param message /\n     */\n    public void pushToAllClient(SaSsoMessage message, String ignoreClient) {\n        List<SaSsoClientModel> needPushClients = getNeedPushClients();\n        for (SaSsoClientModel client : needPushClients) {\n            if(SaFoxUtil.isNotEmpty(ignoreClient) && ignoreClient.equals(client.getClient())) {\n                continue;\n            }\n            strategy.asyncRun.run(() -> pushMessage(client, message));\n        }\n    }\n\n    /**\n     * 向所有 Client 推送消息：单点注销回调\n     *\n     * @param loginId /\n     * @param logoutParameter 注销参数\n     * @param ignoreClient 要被忽略掉的 client，填 null 代表不忽略\n     */\n    public void pushToAllClientByLogoutCall(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) {\n        List<SaSsoClientModel> npClients = getNeedPushClients();\n        for (SaSsoClientModel client : npClients) {\n            if(SaFoxUtil.isNotEmpty(ignoreClient) && ignoreClient.equals(client.getClient())) {\n                continue;\n            }\n            if(client.getIsSlo()) {\n                strategy.asyncRun.run(() -> {\n                    pushToClientByLogoutCall(client, loginId, false, logoutParameter);\n                });\n            }\n        }\n    }\n\n    /**\n     * 向指定 Client 推送消息：单点注销回调\n     *\n     * @param client 应用\n     * @param loginId /\n     * @param autoLogout 是否为超过 maxRegClient 的自动注销\n     * @param logoutParameter 注销参数\n     * @return /\n     */\n    public String pushToClientByLogoutCall(SaSsoClientModel client, Object loginId, boolean autoLogout, SaLogoutParameter logoutParameter) {\n        SaSsoMessage message = new SaSsoMessage();\n        message.setType(SaSsoConsts.MESSAGE_LOGOUT_CALL);\n        message.set(paramName.loginId, loginId);\n        message.set(paramName.autoLogout, autoLogout);\n        message.set(paramName.deviceId, logoutParameter.getDeviceId());\n        return pushMessage(client, message);\n    }\n\n\n    // ------------------- Bean 获取 -------------------\n\n    /**\n     * 获取底层使用的SsoServer配置对象\n     * @return /\n     */\n    public SaSsoServerConfig getServerConfig() {\n        return SaSsoManager.getServerConfig();\n    }\n\n    /**\n     * 获取底层使用的 API 签名对象\n     * @param client 指定客户端标识，填 null 代表获取默认的\n     * @return /\n     */\n    public SaSignTemplate getSignTemplate(String client) {\n        SaSignConfig signConfig = SaSignManager.getSaSignTemplate().getSignConfigOrGlobal().copy();\n        SaSsoClientModel clientModel = getClientNotNull(client);\n\n        // 使用 secretKey 的优先级：client 单独配置 > SSO 模块全局配置 > sign 模块默认配置\n        String secretKey = clientModel.getSecretKey();\n        if (SaFoxUtil.isEmpty(secretKey) && SaFoxUtil.isNotEmpty(client)) {\n            secretKey = getServerConfig().getSecretKey();\n        }\n        if(SaFoxUtil.isEmpty(secretKey)) {\n            secretKey = signConfig.getSecretKey();\n        }\n        signConfig.setSecretKey(secretKey);\n\n        return new SaSignTemplate(signConfig);\n    }\n\n\n    // ------------------- 返回相应key -------------------\n\n    /**\n     * 拼接key：TicketModel\n     * @param ticket ticket值\n     * @return key\n     */\n    public String splicingTicketModelSaveKey(String ticket) {\n        return getStpLogicOrGlobal().getConfigOrGlobal().getTokenName() + \":ticket:\" + ticket;\n    }\n\n    /**\n     * 拼接key：Ticket 索引\n     *\n     * @param client 应用标识\n     * @param id 账号id\n     * @return key\n     */\n    public String splicingTicketIndexKey(String client, Object id) {\n        if(SaFoxUtil.isEmpty(client) || SaSsoConsts.CLIENT_WILDCARD.equals(client)) {\n            client = SaSsoConsts.CLIENT_ANON;\n        }\n        return getStpLogicOrGlobal().getConfigOrGlobal().getTokenName() + \":ticket-index:\" + client + \":\" + id;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.template;\n\nimport cn.dev33.satoken.sso.config.SaSsoClientModel;\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.model.TicketModel;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.stp.parameter.SaLogoutParameter;\nimport cn.dev33.satoken.util.SaResult;\n\nimport java.util.List;\n\n/**\n * SSO 工具类 （Server端）\n *\n * @author click33\n * @since 1.43.0\n */\npublic class SaSsoServerUtil {\n\n    private SaSsoServerUtil() {\n    }\n\n    /**\n     * 返回底层使用的 SaSsoServerTemplate 对象\n     * @return /\n     */\n    public static SaSsoServerTemplate getSsoTemplate() {\n        return SaSsoServerProcessor.instance.ssoServerTemplate;\n    }\n\n\n    // ---------------------- Ticket 操作 ----------------------\n\n    // 增删改\n\n    /**\n     * 删除 Ticket\n     * @param ticket Ticket码\n     */\n    public static void deleteTicket(String ticket) {\n        SaSsoServerProcessor.instance.ssoServerTemplate.deleteTicket(ticket);\n    }\n\n    /**\n     * 根据参数创建一个 ticket 码，并保存\n     *\n     * @param client 客户端标识\n     * @param loginId 账号 id\n     * @param tokenValue 会话 Token\n     * @return Ticket码\n     */\n    public static String createTicketAndSave(String client, Object loginId, String tokenValue) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.createTicketAndSave(client, loginId, tokenValue);\n    }\n\n    // 查\n\n    /**\n     * 查询 ticket ，如果 ticket 无效则返回 null\n     *\n     * @param ticket Ticket码\n     * @return 账号id\n     */\n    public static TicketModel getTicket(String ticket) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getTicket(ticket);\n    }\n\n    /**\n     * 查询 ticket 指向的 loginId，如果 ticket 码无效则返回 null\n     * @param ticket Ticket码\n     * @return 账号id\n     */\n    public static Object getLoginId(String ticket) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket);\n    }\n\n    /**\n     * 查询 ticket 指向的 loginId，并转换为指定类型\n     * @param <T> 要转换的类型\n     * @param ticket Ticket码\n     * @param cs 要转换的类型\n     * @return 账号id\n     */\n    public static <T> T getLoginId(String ticket, Class<T> cs) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket, cs);\n    }\n\n    // 校验\n\n    /**\n     * 校验 Ticket，无效 ticket 会抛出异常\n     *\n     * @param ticket Ticket码\n     * @return /\n     */\n    public static TicketModel checkTicket(String ticket) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicket(ticket);\n    }\n\n    /**\n     * 校验 Ticket 码，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\n     * @param ticket Ticket码\n     * @return 账号id\n     */\n    public static TicketModel checkTicketParamAndDelete(String ticket) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket);\n    }\n\n    /**\n     * 校验 Ticket，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\n     *\n     * @param ticket Ticket码\n     * @param client client 标识\n     * @return /\n     */\n    public static TicketModel checkTicketParamAndDelete(String ticket, String client) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, client);\n    }\n\n    // ticket 索引\n\n    /**\n     * 查询 指定 client、loginId 其所属的 ticket 值\n     *\n     * @param client 应用\n     * @param loginId 账号id\n     * @return Ticket值\n     */\n    public static String getTicketValue(String client, Object loginId) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getTicketValue(client, loginId);\n    }\n\n\n    // ---------------------- Client 信息获取 ----------------------\n\n    /**\n     * 获取所有 Client\n     *\n     * @return /\n     */\n    public static List<SaSsoClientModel> getClients() {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getClients();\n    }\n\n    /**\n     * 获取应用信息，无效 client 返回 null\n     *\n     * @param client /\n     * @return /\n     */\n    public static SaSsoClientModel getClient(String client) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getClient(client);\n    }\n\n    /**\n     * 获取应用信息，无效 client 则抛出异常\n     *\n     * @param client /\n     * @return /\n     */\n    public static SaSsoClientModel getClientNotNull(String client) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getClientNotNull(client);\n    }\n\n    /**\n     * 获取匿名 client 信息\n     *\n     * @return /\n     */\n    public static SaSsoClientModel getAnonClient() {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getAnonClient();\n    }\n\n    /**\n     * 获取所有需要接收消息推送的 Client\n     *\n     * @return /\n     */\n    public static List<SaSsoClientModel> getNeedPushClients() {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.getNeedPushClients();\n    }\n\n\n    // ------------------- 重定向 URL 构建与校验 -------------------\n\n    /**\n     * 构建 URL：sso-server 端向 sso-client 下放 ticket 的地址\n     *\n     * @param client 客户端标识\n     * @param redirect sso-client 端的重定向地址\n     * @param loginId 账号 id\n     * @param tokenValue 会话 token\n     * @return /\n     */\n    public static String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.buildRedirectUrl(client, redirect, loginId, tokenValue);\n    }\n\n    /**\n     * 校验重定向 url 合法性\n     *\n     * @param client 应用标识\n     * @param url 下放ticket的url地址\n     */\n    public static void checkRedirectUrl(String client, String url) {\n        SaSsoServerProcessor.instance.ssoServerTemplate.checkRedirectUrl(client, url);\n    }\n\n\n    // ------------------- 单点注销 -------------------\n\n    /**\n     * 指定账号单点注销\n     *\n     * @param loginId 指定账号\n     */\n    public static void ssoLogout(Object loginId) {\n        SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId);\n    }\n\n    /**\n     * 指定账号单点注销\n     *\n     * @param loginId 指定账号\n     * @param logoutParameter 注销参数\n     * @param ignoreClient 要被忽略掉的 client，填 null 代表不忽略\n     */\n    public static void ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient) {\n        SaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId, logoutParameter, ignoreClient);\n    }\n\n\n    // ------------------- 消息推送 -------------------\n\n    /**\n     * 向指定 Client 推送消息\n     * @param clientModel /\n     * @param message /\n     * @return /\n     */\n    public static String pushMessage(SaSsoClientModel clientModel, SaSsoMessage message) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessage(clientModel, message);\n    }\n\n    /**\n     * 向指定 client 推送消息，并将返回值转为 SaResult\n     *\n     * @param clientModel /\n     * @param message /\n     * @return /\n     */\n    public static SaResult pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessageAsSaResult(clientModel, message);\n    }\n\n    /**\n     * 向指定 Client 推送消息\n     * @param client /\n     * @param message /\n     * @return /\n     */\n    public static String pushMessage(String client, SaSsoMessage message) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessage(client, message);\n    }\n\n    /**\n     * 向指定 client 推送消息，并将返回值转为 SaResult\n     *\n     * @param client /\n     * @param message /\n     * @return /\n     */\n    public static SaResult pushMessageAsSaResult(String client, SaSsoMessage message) {\n        return SaSsoServerProcessor.instance.ssoServerTemplate.pushMessageAsSaResult(client, message);\n    }\n\n    /**\n     * 向所有 Client 推送消息\n     *\n     * @param message /\n     */\n    public static void pushToAllClient(SaSsoMessage message) {\n        SaSsoServerProcessor.instance.ssoServerTemplate.pushToAllClient(message);\n    }\n\n    /**\n     * 向所有 Client 推送消息，并忽略掉某个 client\n     *\n     * @param ignoreClient 要被忽略掉的 client，填 null 代表不忽略\n     * @param message /\n     */\n    public static void pushToAllClient(SaSsoMessage message, String ignoreClient) {\n        SaSsoServerProcessor.instance.ssoServerTemplate.pushToAllClient(message, ignoreClient);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.template;\n\nimport cn.dev33.satoken.sso.message.SaSsoMessage;\nimport cn.dev33.satoken.sso.message.SaSsoMessageHolder;\nimport cn.dev33.satoken.sso.name.ApiName;\nimport cn.dev33.satoken.sso.name.ParamName;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\n\n/**\n * SSO 模板方法类 （公共端）\n *\n * @author click33\n * @since 1.30.0\n */\npublic class SaSsoTemplate {\n\n\t// ---------------------- 全局配置 ---------------------- \n\n\t/**\n\t * 所有 API 名称 \n\t */\n\tpublic ApiName apiName = new ApiName();\n\t\n\t/**\n\t * 所有参数名称 \n\t */\n\tpublic ParamName paramName = new ParamName();\n\n\t/**\n\t * @param paramName 替换 paramName 对象 \n\t * @return 对象自身\n\t */\n\tpublic SaSsoTemplate setParamName(ParamName paramName) {\n\t\tthis.paramName = paramName;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * @param apiName 替换 apiName 对象 \n\t * @return 对象自身\n\t */\n\tpublic SaSsoTemplate setApiName(ApiName apiName) {\n\t\tthis.apiName = apiName;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 底层使用的 StpLogic 对象\n\t */\n\tStpLogic stpLogic;\n\n\t/**\n\t * 写入底层使用的会话对象\n\t *\n\t * @param stpLogic /\n\t * @return /\n\t */\n\tpublic SaSsoTemplate setStpLogic(StpLogic stpLogic) {\n\t\tthis.stpLogic = stpLogic;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取底层使用的会话对象\n\t * @return /\n\t */\n\tpublic StpLogic getStpLogic() {\n\t\treturn this.stpLogic;\n\t}\n\n\t/**\n\t * 获取底层使用的会话对象，如果没有配置则返回全局默认 StpLogic\n\t * @return /\n\t */\n\tpublic StpLogic getStpLogicOrGlobal() {\n\t\tStpLogic stpLogic = getStpLogic();\n\t\tif (stpLogic == null) {\n\t\t\treturn StpUtil.stpLogic;\n\t\t}\n\t\treturn stpLogic;\n\t}\n\n\t// ----------- 消息处理\n\n\t/**\n\t * SSO 消息处理器 - 持有器\n\t */\n\tpublic SaSsoMessageHolder messageHolder = new SaSsoMessageHolder();\n\n\t/**\n\t * 处理指定消息\n\t *\n\t * @param message /\n\t */\n\tpublic Object handleMessage(SaSsoMessage message) {\n\t\treturn messageHolder.handleMessage(this, message);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.template;\n\nimport cn.dev33.satoken.sso.model.TicketModel;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\n\nimport java.util.Map;\n\n/**\n * Sa-Token-SSO 单点登录模块 工具类\n *\n * <h2> 请更换为 SaSsoServerUtil 或 SaSsoClientUtil <h2/>\n * \n * @author click33\n * @since 1.30.0\n */\n@Deprecated\npublic class SaSsoUtil {\n\n\t// ---------------------- Ticket 操作 ---------------------- \n\n\t/**\n\t * 根据参数创建一个 ticket 码\n\t *\n\t * @param client 客户端标识\n\t * @param loginId 账号 id\n\t * @param deviceId 设备 id\n\t * @return Ticket码\n\t */\n\tpublic static String createTicket(String client, Object loginId, String deviceId) {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate.createTicketAndSave(client, loginId, deviceId);\n\t}\n\t\n\t/**\n\t * 删除 Ticket \n\t * @param ticket Ticket码\n\t */\n\tpublic static void deleteTicket(String ticket) {\n\t\tSaSsoServerProcessor.instance.ssoServerTemplate.deleteTicket(ticket);\n\t}\n\t\n\t/**\n\t * 删除 Ticket索引 \n\t * @param client 应用 id\n\t * @param loginId 账号id\n\t */\n\tpublic static void deleteTicketIndex(String client, Object loginId) {\n\t\tSaSsoServerProcessor.instance.ssoServerTemplate.deleteTicketIndex(client, loginId);\n\t}\n\n\t/**\n\t * 根据 Ticket码 获取账号id，如果Ticket码无效则返回null \n\t * @param ticket Ticket码\n\t * @return 账号id \n\t */\n\tpublic static Object getLoginId(String ticket) {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket);\n\t}\n\n\t/**\n\t * 根据 Ticket码 获取账号id，并转换为指定类型 \n\t * @param <T> 要转换的类型 \n\t * @param ticket Ticket码\n\t * @param cs 要转换的类型 \n\t * @return 账号id \n\t */\n\tpublic static <T> T getLoginId(String ticket, Class<T> cs) {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate.getLoginId(ticket, cs);\n\t}\n\n\t/**\n\t * 校验 Ticket，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\n\t * @param ticket Ticket码\n\t * @return 账号id \n\t */\n\tpublic static TicketModel checkTicket(String ticket) {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket);\n\t}\n\t\n\t/**\n\t * 校验ticket码，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除\n\t * @param ticket Ticket码\n\t * @param client client 标识 \n\t * @return 账号id \n\t */\n\tpublic static TicketModel checkTicket(String ticket, String client) {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate.checkTicketParamAndDelete(ticket, client);\n\t}\n\n\t/**\n\t * 校验重定向url合法性\n\t *\n\t * @param client 应用标识\n\t * @param url 下放ticket的url地址\n\t */\n\tpublic static void checkRedirectUrl(String client, String url) {\n\t\tSaSsoServerProcessor.instance.ssoServerTemplate.checkRedirectUrl(client, url);\n\t}\n\n\t\n\t// ------------------- SSO 模式三 ------------------- \n\n\t/**\n\t * 为指定账号id注册单点注销回调URL \n\t * @param loginId 账号id\n\t * @param client 指定客户端标识，可为null\n\t * @param sloCallbackUrl 单点注销时的回调URL \n\t */\n\tpublic static void registerSloCallbackUrl(Object loginId, String client, String sloCallbackUrl) {\n\t\tSaSsoServerProcessor.instance.ssoServerTemplate.registerSloCallbackUrl(loginId, client, sloCallbackUrl);\n\t}\n\n\t/**\n\t * 指定账号单点注销 (以Server方发起)\n\t * @param loginId 指定账号 \n\t */\n\tpublic static void ssoLogout(Object loginId) {\n\t\tSaSsoServerProcessor.instance.ssoServerTemplate.ssoLogout(loginId);\n\t}\n\n\t/**\n\t * 获取：查询数据\n\t * @param paramMap 查询参数\n\t * @return 查询结果\n\t */\n\tpublic static Object getData(Map<String, Object> paramMap) {\n\t\treturn SaSsoClientProcessor.instance.ssoClientTemplate.getData(paramMap);\n\t}\n\n\t/**\n\t * 根据自定义 path 查询数据 （此方法需要配置 sa-token.sso.server-url 地址）\n\t * @param path 自定义 path\n\t * @param paramMap 查询参数\n\t * @return 查询结果\n\t */\n\tpublic static Object getData(String path, Map<String, Object> paramMap) {\n\t\treturn SaSsoClientProcessor.instance.ssoClientTemplate.getData(path, paramMap);\n\t}\n\n\n\t// ---------------------- 构建URL ---------------------- \n\n\t/**\n\t * 构建URL：Server端 单点登录地址\n\t * @param clientLoginUrl Client端登录地址 \n\t * @param back 回调路径 \n\t * @return [SSO-Server端-认证地址 ]\n\t */\n\tpublic static String buildServerAuthUrl(String clientLoginUrl, String back) {\n\t\treturn SaSsoClientProcessor.instance.ssoClientTemplate.buildServerAuthUrl(clientLoginUrl, back);\n\t}\n\n\t/**\n\t * 构建 URL：sso-server 端向 sso-client 下放 ticket 的地址\n\t *\n\t * @param client 客户端标识\n\t * @param redirect sso-client 端的重定向地址\n\t * @param loginId 账号 id\n\t * @param tokenValue 会话 token\n\t * @return /\n\t */\n\tpublic static String buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue) {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate.buildRedirectUrl(client, redirect, loginId, tokenValue);\n\t}\n\n\t/**\n\t * 构建URL：Server端 getData 地址，带签名等参数\n\t * @param paramMap 查询参数\n\t * @return /\n\t */\n\tpublic static String buildGetDataUrl(Map<String, Object> paramMap) {\n\t\treturn SaSsoClientProcessor.instance.ssoClientTemplate.buildGetDataUrl(paramMap);\n\t}\n\n\t/**\n\t * 构建URL：Server 端自定义 path 地址，带签名等参数 （此方法需要配置 sa-token.sso.server-url 地址）\n\t * @param paramMap 请求参数\n\t * @return /\n\t */\n\tpublic static String buildCustomPathUrl(String path, Map<String, Object> paramMap) {\n\t\treturn SaSsoClientProcessor.instance.ssoClientTemplate.buildCustomPathUrl(path, paramMap);\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/util/SaSsoConsts.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.sso.util;\n\n/**\n * Sa-Token-SSO模块相关常量\n *\n * @author click33\n * @since 1.30.0\n */\npublic class SaSsoConsts {\n\n\t/** Client端单点注销回调URL的Set集合，存储在Session中使用的key */\n\t@Deprecated\n\tpublic static final String SLO_CALLBACK_SET_KEY = \"SLO_CALLBACK_SET_KEY_\";\n\n\t/** Client 端 Model 信息的 List 集合，存储在 SaSession 中使用的key */\n\tpublic static final String SSO_CLIENT_MODEL_LIST_KEY_ = \"SSO_CLIENT_MODEL_LIST_KEY_\";\n\n\t/** 表示OK的返回结果 */\n\tpublic static final String OK = \"ok\";\n\n\t/** 表示自己 */\n\tpublic static final String SELF = \"self\";\n\n\t/** 表示简单模式（SSO模式一） */\n\tpublic static final String MODE_SIMPLE = \"simple\";\n\n\t/** 表示ticket模式（SSO模式二和模式三） */\n\tpublic static final String MODE_TICKET = \"ticket\";\n\t\n\t/** 表示请求没有得到任何有效处理 {msg: \"not handle\"} */\n\tpublic static final String NOT_HANDLE = \"{\\\"msg\\\": \\\"not handle\\\"}\";\n\n\t/** client 身份，* 代表通配，可以解析出所有 client 的 ticket */\n\tpublic static final String CLIENT_WILDCARD = \"*\";\n\n\t/** client 身份，代表匿名 client  */\n\tpublic static final String CLIENT_ANON = \"anon\";\n\n\t/** SSO 模式1 */\n\tpublic static final int SSO_MODE_1 = 1;\n\t/** SSO 模式2 */\n\tpublic static final int SSO_MODE_2 = 2;\n\t/** SSO 模式3 */\n\tpublic static final int SSO_MODE_3 = 3;\n\n\t/** 消息类型：校验 ticket */\n\tpublic static final String MESSAGE_CHECK_TICKET = \"checkTicket\";\n\n\t/** 消息类型：单点注销 */\n\tpublic static final String MESSAGE_SIGNOUT = \"signout\";\n\n\t/** 消息类型：单点注销回调 */\n\tpublic static final String MESSAGE_LOGOUT_CALL = \"logoutCall\";\n\n\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-temp-jwt/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-temp-jwt</name>\n    <artifactId>sa-token-temp-jwt</artifactId>\n\t<description>sa-token-temp-jwt</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <!-- jwt -->\n        <dependency>\n            <groupId>io.jsonwebtoken</groupId>\n            <artifactId>jjwt</artifactId>\n        </dependency>\n        <!-- 不加这个报 java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <version>2.3.1</version>\n        </dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.plugin;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.temp.jwt.SaTempTemplateForJwt;\n\n/**\n * SaToken 插件安装：临时 token 生成器 - Jwt 版\n *\n * @author click33\n * @since 1.41.0\n */\npublic class SaTokenPluginForTempForJwt implements SaTokenPlugin {\n\n    @Override\n    public void install() {\n        SaManager.setSaTempTemplate(new SaTempTemplateForJwt());\n    }\n\n}"
  },
  {
    "path": "sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.temp.jwt;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.secure.SaSecureUtil;\nimport cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode;\nimport io.jsonwebtoken.Claims;\nimport io.jsonwebtoken.JwtBuilder;\nimport io.jsonwebtoken.Jwts;\nimport io.jsonwebtoken.security.Keys;\n\nimport javax.crypto.SecretKey;\n\n/**\n * jwt 相关操作工具类，封装一下\n *\n * @author click33\n * @since 1.20.0\n */\npublic class SaJwtUtil {\n\t\n\t/**\n\t * key: value 前缀 \n\t */\n\tpublic static final String KEY_VALUE = \"value_\"; \n\n\t/**\n\t * key: 有效期 (时间戳)\n\t */\n\tpublic static final String KEY_EFF = \"eff\"; \n\n\t/** 当有效期被设为此值时，代表永不过期 */ \n\tpublic static final long NEVER_EXPIRE = SaTokenDao.NEVER_EXPIRE;\n\t\n\t/**\n\t * 根据指定值创建 jwt-token\n\t *\n\t * @param value 要保存的值\n\t * @param timeout token有效期 (单位 秒)\n     * @param keyt 秘钥\n\t * @return jwt-token \n\t */\n    public static String createToken(Object value, long timeout, String keyt) {\n    \t// 计算eff有效期：\n\t\t// \t\t如果 timeout 指定为 -1，那么 eff 也为 -1，代表永不过期\n\t\t// \t\t如果 timeout 指定为一个具体的值，那么 eff 为 13 位时间戳，代表此数据到期的时间\n    \tlong eff = timeout;\n    \tif(timeout != NEVER_EXPIRE) {\n    \t\teff = timeout * 1000 + System.currentTimeMillis();\n    \t}\n\n    \t// 在这里你可以使用官方提供的claim方法构建载荷，也可以使用setPayload自定义载荷，但是两者不可一起使用\n\t\tSecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes());\n\t\tJwtBuilder builder = Jwts.builder()\n\t\t\t\t.header().add(\"typ\", \"JWT\").and()\n        \t\t.claim(KEY_VALUE, value)\n        \t\t.claim(KEY_EFF, eff)\n                .signWith(key);\n\n        // 生成jwt-token \n        return builder.compact();\n    }\n\n    /**\n     * 从一个 jwt-token 解析出载荷 \n     * @param jwtToken JwtToken值 \n     * @param keyt 秘钥\n     * @return Claims对象 \n     */\n    public static Claims parseToken(String jwtToken, String keyt) {\n    \t// 解析出载荷\n\t\tSecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes());\n        return Jwts.parser()\n\t\t\t\t.verifyWith(key)\n\t\t\t\t.build()\n\t\t\t\t.parseSignedClaims(jwtToken).getPayload();\n    }\n\n    /**\n     * 从一个 jwt-token 解析出载荷, 并取出数据\n     * @param jwtToken JwtToken值 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public static Object getValue(String jwtToken, String keyt) {\n    \t// 取出数据 \n    \tClaims claims = parseToken(jwtToken, keyt);\n    \t\n    \t// 验证是否超时 \n    \tLong eff = claims.get(KEY_EFF, Long.class);\n    \tif(eff == null || (eff < System.currentTimeMillis() && eff != NEVER_EXPIRE)) {\n    \t\tthrow new SaTokenException(\"token 已超时，无法解析：\" + jwtToken).setCode(SaTempJwtErrorCode.CODE_30303);\n    \t}\n    \t\n        // 获取数据 \n        return claims.get(KEY_VALUE);\n    }\n\n    /**\n     * 从一个 jwt-token 解析出载荷, 并取出其剩余有效期\n     * @param jwtToken JwtToken值 \n     * @param keyt 秘钥\n     * @return 值 \n     */\n    public static long getTimeout(String jwtToken, String keyt) {\n    \t// 取出数据 \n    \tClaims claims = parseToken(jwtToken, keyt);\n\n    \t// 验证是否超时 \n    \tLong eff = claims.get(KEY_EFF, Long.class);\n    \t\n    \t// 永不过期 \n    \tif(eff == NEVER_EXPIRE) {\n    \t\treturn NEVER_EXPIRE;\n    \t}\n    \t// 已经超时 \n    \tif(eff < System.currentTimeMillis()) {\n    \t\treturn SaTokenDao.NOT_VALUE_EXPIRE;\n    \t}\n    \t\n        // 计算timeout \n        return (eff - System.currentTimeMillis()) / 1000;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.temp.jwt;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.exception.ApiDisabledException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.temp.SaTempTemplate;\nimport cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport java.util.List;\n\n/**\n * Sa-Token 临时令牌验证模块接口 JWT实现类，提供以 JWT 为逻辑内核的临时 token 验证功能\n *\n * @author click33\n * @since 1.20.0\n */\npublic class SaTempTemplateForJwt extends SaTempTemplate {\n\t\n\t/**\n\t * 根据value创建一个token \n\t */\n\t@Override\n\tpublic String createToken(Object value, long timeout, boolean isRecordIndex) {\n\t\treturn SaJwtUtil.createToken(value, timeout, getJwtSecretKey());\n\t}\n\t\n\t/**\n\t *  解析token获取value \n\t */\n\t@Override\n\tpublic Object parseToken(String token) {\n\t\treturn SaJwtUtil.getValue(token, getJwtSecretKey());\n\t}\n\t\n\t/**\n\t * 返回指定token的剩余有效期，单位：秒 \n\t */\n\t@Override\n\tpublic long getTimeout(String token) {\n\t\treturn SaJwtUtil.getTimeout(token, getJwtSecretKey());\n\t}\n\n\t/**\n\t * 删除一个token\n\t */\n\t@Override\n\tpublic void deleteToken(String token) {\n\t\tthrow new ApiDisabledException(\"jwt cannot delete token\").setCode(SaTempJwtErrorCode.CODE_30302);\n\t}\n\n\t/**\n\t * 获取指定 value 的 temp-token 列表记录\n\t * @param value /\n\t * @return /\n\t */\n\tpublic List<String> getTempTokenList(Object value) {\n\t\tthrow new ApiDisabledException(\"jwt cannot get token list\").setCode(SaTempJwtErrorCode.CODE_30304);\n\t}\n\n\t/**\n\t * 获取jwt秘钥 \n\t * @return jwt秘钥 \n\t */\n\t@Override\n\tpublic String getJwtSecretKey() {\n\t\tString jwtSecretKey = SaManager.getConfig().getJwtSecretKey();\n\t\tif(SaFoxUtil.isEmpty(jwtSecretKey)) {\n\t\t\tthrow new SaTokenException(\"请配置：jwtSecretKey\").setCode(SaTempJwtErrorCode.CODE_30301);\n\t\t}\n\t\treturn jwtSecretKey;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.temp.jwt.error;\n\n/**\n * 定义 sa-token-temp-jwt 所有异常细分状态码 \n * \n * @author click33\n * @since 1.33.0\n */\npublic interface SaTempJwtErrorCode {\n\n\t/** jwt 模式没有提供秘钥 */\n\tint CODE_30301 = 30301;\n\n\t/** jwt 模式不可以删除 Token */\n\tint CODE_30302 = 30302;\n\n\t/** Token已超时 */\n\tint CODE_30303 = 30303;\n\n\t/** jwt 模式不可以查询旧 Token 列表 */\n\tint CODE_30304 = 30304;\n\n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-temp-jwt/src/main/resources/META-INF/satoken/cn.dev33.satoken.plugin.SaTokenPlugin",
    "content": "cn.dev33.satoken.plugin.SaTokenPluginForTempForJwt"
  },
  {
    "path": "sa-token-plugin/sa-token-thymeleaf/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-plugin</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-thymeleaf</name>\n    <artifactId>sa-token-thymeleaf</artifactId>\n\t<description>sa-token-thymeleaf</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-spring-boot-starter -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\t\t<!-- thymeleaf -->\n\t\t<dependency>\n\t\t\t<groupId>org.thymeleaf</groupId>\n\t\t\t<artifactId>thymeleaf</artifactId>\n\t\t\t<optional>true</optional>\n\t    </dependency>\n\t\t<!-- spring-boot-configuration -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<version>2.5.14</version>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/Sa-Token-Dialect.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!--\n  ~ =============================================================================\n  ~\n  ~   Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org)\n  ~\n  ~   Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~   you may not use this file except in compliance with the License.\n  ~   You may obtain a copy of the License at\n  ~\n  ~       http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~   Unless required by applicable law or agreed to in writing, software\n  ~   distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~   See the License for the specific language governing permissions and\n  ~   limitations under the License.\n  ~\n  ~ =============================================================================\n  -->\n\n<dialect\n        xmlns=\"http://www.thymeleaf.org/extras/dialect\"\n        prefix=\"sa\"\n        namespace-uri=\"http://www.thymeleaf.org/extras/sa-token\"\n        class=\"cn.dev33.satoken.thymeleaf.dialect.SaTokenDialect\">\n\n    <!-- 登录判断 -->\n    <attribute-processor name=\"login\">\n        <documentation> <![CDATA[ 登录之后才能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"notLogin\">\n        <documentation> <![CDATA[ 不登录才能显示元素 ]]> </documentation>\n    </attribute-processor>\n\n    <!-- 角色判断 -->\n    <attribute-processor name=\"hasRole\">\n        <documentation> <![CDATA[ 具有指定角色才能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"hasRoleAnd\">\n        <documentation> <![CDATA[ 同时具备多个角色才能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"hasRoleOr\">\n        <documentation> <![CDATA[ 只要具有其中一个角色就能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"notRole\">\n        <documentation> <![CDATA[ 不具有指定角色才能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"lackRole\">\n        <documentation> <![CDATA[ 不具有指定角色才能显示元素（未来版本可能废弃，建议更换为 notRole） ]]> </documentation>\n    </attribute-processor>\n\n    <!-- 权限判断 -->\n    <attribute-processor name=\"hasPermission\">\n        <documentation> <![CDATA[ 具有指定权限才能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"hasPermissionAnd\">\n        <documentation> <![CDATA[ 同时具备多个权限才能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"hasPermissionOr\">\n        <documentation> <![CDATA[ 只要具有其中一个权限就能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"notPermission\">\n        <documentation> <![CDATA[ 不具有指定权限才能显示元素 ]]> </documentation>\n    </attribute-processor>\n    <attribute-processor name=\"lackPermission\">\n        <documentation> <![CDATA[ 不具有指定权限才能显示元素（未来版本可能废弃，建议更换为 notPermission） ]]> </documentation>\n    </attribute-processor>\n\n</dialect>\n"
  },
  {
    "path": "sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenDialect.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.thymeleaf.dialect;\n\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport org.thymeleaf.dialect.AbstractProcessorDialect;\nimport org.thymeleaf.processor.IProcessor;\n\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.thymeleaf.standard.processor.StandardXmlNsTagProcessor;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n * Sa-Token 集成 Thymeleaf 标签方言 \n * \n * @author click33\n * @since 1.27.0\n */\npublic class SaTokenDialect extends AbstractProcessorDialect {\n\t\n\t/**\n\t * 底层使用的 StpLogic \n\t */\n\tpublic StpLogic stpLogic;\n\t\n\t/**\n\t * 使用默认参数注册方言 \n\t */\n    public SaTokenDialect() {\n    \tthis(\"sa\", 1000, StpUtil.stpLogic);\n    }\n    \n    /**\n     * 构造方言对象，使用自定义参数\n\t *\n     * @param name 方言名称\n     * @param precedence 优先级\n     * @param stpLogic 使用的 StpLogic 对象 \n     */\n    public SaTokenDialect(String name, int precedence, StpLogic stpLogic) {\n    \t// 名称、前缀、优先级 \n        super(name, name, precedence);\n        this.stpLogic = stpLogic;\n    }\n    \n    /**\n     * 返回所有方言处理器 \n     */\n    @Override\n    public Set<IProcessor> getProcessors(String prefix) {\n    \treturn new HashSet<>(Arrays.asList(\n\t\t\t\t// 登录判断\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"login\", value -> stpLogic.isLogin()),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"notLogin\", value -> ! stpLogic.isLogin()),\n\n\t\t\t\t// 角色判断\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"hasRole\", value -> stpLogic.hasRole(value)),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"hasRoleAnd\", value -> stpLogic.hasRoleAnd(toArray(value))),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"hasRoleOr\", value -> stpLogic.hasRoleOr(toArray(value))),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"notRole\", value -> ! stpLogic.hasRole(value)),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"lackRole\", value -> ! stpLogic.hasRole(value)),\n\n\t\t\t\t// 权限判断\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"hasPermission\", value -> stpLogic.hasPermission(value)),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"hasPermissionAnd\", value -> stpLogic.hasPermissionAnd(toArray(value))),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"hasPermissionOr\", value -> stpLogic.hasPermissionOr(toArray(value))),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"notPermission\", value -> ! stpLogic.hasPermission(value)),\n\t\t\t\tnew SaTokenTagProcessor(prefix, \"lackPermission\", value -> ! stpLogic.hasPermission(value)),\n\n\t\t\t\t// 移除<html>标签命名空间\n\t\t\t\tnew StandardXmlNsTagProcessor(TemplateMode.HTML,prefix)\n\n\t\t));\n    }\n\n    /**\n     * String 转 Array \n     * @param str 字符串 \n     * @return 数组 \n     */\n    public String[] toArray(String str) {\n    \tList<String> list = SaFoxUtil.convertStringToList(str);\n    \treturn list.toArray(new String[0]);\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-plugin/sa-token-thymeleaf/src/main/java/cn/dev33/satoken/thymeleaf/dialect/SaTokenTagProcessor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.thymeleaf.dialect;\n\nimport java.util.function.Function;\n\nimport org.thymeleaf.context.ITemplateContext;\nimport org.thymeleaf.engine.AttributeName;\nimport org.thymeleaf.model.IProcessableElementTag;\nimport org.thymeleaf.processor.element.AbstractAttributeTagProcessor;\nimport org.thymeleaf.processor.element.IElementTagStructureHandler;\nimport org.thymeleaf.templatemode.TemplateMode;\n\n/**\n * 封装 Sa-Token 标签方言处理器\n * \n * @author click33\n * @since 1.27.0\n */\npublic class SaTokenTagProcessor extends AbstractAttributeTagProcessor {\n\n    Function <String, Boolean> fun;\n\n    public SaTokenTagProcessor(final String dialectPrefix, String attrName, Function <String, Boolean> fun) {\n        super(\n            TemplateMode.HTML, // This processor will apply only to HTML mode\n            dialectPrefix,     // Prefix to be applied to name for matching\n            null,              // No tag name: match any tag name\n            false,             // No prefix to be applied to tag name\n            attrName,         // Name of the attribute that will be matched\n            true,              // Apply dialect prefix to attribute name\n            10000,        \t\t// Precedence (inside dialect's own precedence)\n            true);             // Remove the matched attribute afterwards\n        this.fun = fun;\n    }\n\n    @Override\n    protected void doProcess( \n            final ITemplateContext context, final IProcessableElementTag tag,\n            final AttributeName attributeName, final String attributeValue,\n            final IElementTagStructureHandler structureHandler) {\n    \t// 执行表达式返回值为false，则删除这个标签 \n    \tif( ! this.fun.apply(attributeValue)) {\n    \t\tstructureHandler.removeElement();\n    \t}\n    }\n    \n}"
  },
  {
    "path": "sa-token-special-dependencies/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-parent</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <packaging>pom</packaging>\n    <artifactId>sa-token-special-dependencies</artifactId>\n    <name>sa-token-special-dependencies</name>\n    <description>Sa-Token Special Dependencies</description>\n\n    <modules>\n        <module>sa-token-spring-boot2-dependencies</module>\n        <module>sa-token-spring-boot3-dependencies</module>\n        <module>sa-token-spring-boot4-dependencies</module>\n    </modules>\n\n</project>\n"
  },
  {
    "path": "sa-token-special-dependencies/sa-token-spring-boot2-dependencies/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <!-- 父仓库 -->\n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-special-dependencies</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>pom</packaging>\n\n    <artifactId>sa-token-spring-boot2-dependencies</artifactId>\n    <name>sa-token-spring-boot2-dependencies</name>\n    <description>Sa-Token SpringBoot2 Dependencies</description>\n\n    <properties>\n        <springboot2.version>2.7.18</springboot2.version>\n        <springboot2-spring.version>5.3.39</springboot2-spring.version>\n        <reactor-core.version>3.7.4</reactor-core.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n\n            <!-- spring-boot-starter -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n            <!-- spring-boot-starter-web -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-web</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n            <!-- spring-boot-configuration-processor -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-configuration-processor</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.springframework</groupId>\n                <artifactId>spring-web</artifactId>\n                <version>${springboot2-spring.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.springframework</groupId>\n                <artifactId>spring-webmvc</artifactId>\n                <version>${springboot2-spring.version}</version>\n            </dependency>\n\n            <!-- reactor-core -->\n            <dependency>\n                <groupId>io.projectreactor</groupId>\n                <artifactId>reactor-core</artifactId>\n                <version>${reactor-core.version}</version>\n            </dependency>\n\n\n            <!-- ****************** sa-token-plugin 相关依赖 ****************** -->\n\n            <!-- spring-boot-starter-data-redis -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-data-redis</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n            <!-- spring-boot-starter-thymeleaf -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-thymeleaf</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n            <!-- spring-boot-starter-aop -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-aop</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n            <!-- spring-boot-starter-actuator -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-actuator</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n            <!-- spring-boot-starter-test 测试 -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-test</artifactId>\n                <version>${springboot2.version}</version>\n            </dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\t\n</project>"
  },
  {
    "path": "sa-token-special-dependencies/sa-token-spring-boot3-dependencies/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <!-- 父仓库 -->\n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-special-dependencies</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>pom</packaging>\n\n    <artifactId>sa-token-spring-boot3-dependencies</artifactId>\n    <name>sa-token-spring-boot3-dependencies</name>\n    <description>Sa-Token SpringBoot3 Dependencies</description>\n\n    <properties>\n        <springboot3.version>3.5.11</springboot3.version>\n        <springboot3-spring.version>6.2.16</springboot3-spring.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n\n            <!-- spring-boot-starter -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter</artifactId>\n                <version>${springboot3.version}</version>\n            </dependency>\n\n            <!-- spring-boot-starter-web -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-web</artifactId>\n                <version>${springboot3.version}</version>\n            </dependency>\n\n            <!-- spring-web (for reactor/webflux) -->\n            <dependency>\n                <groupId>org.springframework</groupId>\n                <artifactId>spring-web</artifactId>\n                <version>${springboot3-spring.version}</version>\n            </dependency>\n\n            <!-- config (optional) -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-configuration-processor</artifactId>\n                <version>${springboot3.version}</version>\n            </dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\t\n</project>"
  },
  {
    "path": "sa-token-special-dependencies/sa-token-spring-boot4-dependencies/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <!-- 父仓库 -->\n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-special-dependencies</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>pom</packaging>\n\n    <artifactId>sa-token-spring-boot4-dependencies</artifactId>\n    <name>sa-token-spring-boot4-dependencies</name>\n    <description>Sa-Token SpringBoot4 Dependencies</description>\n\n    <properties>\n        <springboot4.version>4.0.3</springboot4.version>\n        <springboot4-spring.version>7.0.3</springboot4-spring.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n\n            <!-- spring-boot-starter -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter</artifactId>\n                <version>${springboot4.version}</version>\n            </dependency>\n            <!-- spring-boot-starter-webmvc -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-webmvc</artifactId>\n                <version>${springboot4.version}</version>\n            </dependency>\n            <!-- spring-web (for reactor/webflux) -->\n            <dependency>\n                <groupId>org.springframework</groupId>\n                <artifactId>spring-web</artifactId>\n                <version>${springboot4-spring.version}</version>\n            </dependency>\n            <!-- spring-boot-starter-webflux (for reactor) -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-webflux</artifactId>\n                <version>${springboot4.version}</version>\n            </dependency>\n            <!-- config (optional) -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-configuration-processor</artifactId>\n                <version>${springboot4.version}</version>\n            </dependency>\n\n            <!-- spring-boot-starter-data-redis (for sa-token-alone-redis-by-spring-boot4) -->\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-starter-data-redis</artifactId>\n                <version>${springboot4.version}</version>\n            </dependency>\n\n            <!-- redis pool -->\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-pool2</artifactId>\n                <version>2.12.1</version>\n                <optional>true</optional>\n            </dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\t\n</project>"
  },
  {
    "path": "sa-token-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n   \n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-parent</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>pom</packaging>\n    \n\t<name>sa-token-starter</name>\n    <artifactId>sa-token-starter</artifactId>\n\t<description>sa-token starters</description>\n\t\n\t<!-- 所有子模块 -->\n    <modules>\n        <module>sa-token-servlet</module>\n        <module>sa-token-jakarta-servlet</module>\n        <module>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</module>\n        <module>sa-token-spring-boot-reactor-v2v3v4-common</module>\n        <module>sa-token-spring-boot-starter</module>\n        <module>sa-token-spring-boot-webmvc-v3v4-common</module>\n        <module>sa-token-spring-boot3-starter</module>\n        <module>sa-token-spring-boot4-starter</module>\n        <module>sa-token-reactor-spring-boot-starter</module>\n        <module>sa-token-reactor-spring-boot3-starter</module>\n        <module>sa-token-reactor-spring-boot4-starter</module>\n        <module>sa-token-solon-plugin</module>\n        <module>sa-token-jboot-plugin</module>\n        <module>sa-token-jfinal-plugin</module>\n        <module>sa-token-loveqq-boot-starter</module>\n    </modules>\n\n</project>"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-jakarta-servlet</name>\n    <artifactId>sa-token-jakarta-servlet</artifactId>\n\t<description>sa-token authentication by Jakarta Servlet API</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        \n        <!-- Servlet API -->\n        <dependency>\n            <groupId>jakarta.servlet</groupId>\n            <artifactId>jakarta.servlet-api</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.error;\n\n/**\n * 定义 sa-token-servlet 所有异常细分状态码 \n * \n * @author click33\n * @since 1.34.0\n */\npublic interface SaServletErrorCode {\n\t\n\t/** 转发失败 */\n\tint CODE_20001 = 20001;\n\n\t/** 重定向失败 */\n\tint CODE_20002 = 20002;\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.model;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.servlet.error.SaServletErrorCode;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.http.Cookie;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\nimport java.io.IOException;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 对 SaRequest 包装类的实现（Jakarta-Servlet 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaRequestForServlet implements SaRequest {\n\n\t/**\n\t * 底层Request对象\n\t */\n\tprotected HttpServletRequest request;\n\t\n\t/**\n\t * 实例化\n\t * @param request request对象 \n\t */\n\tpublic SaRequestForServlet(HttpServletRequest request) {\n\t\tthis.request = request;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn request;\n\t}\n\n\t/**\n\t * 在 [请求体] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getParam(String name) {\n\t\treturn request.getParameter(name);\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数名称\n\t * @return 参数名称列表\n\t */\n\t@Override\n\tpublic Collection<String> getParamNames(){\n\t\treturn Collections.list(request.getParameterNames());\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数\n\t * @return 参数列表\n\t */\n\t@Override\n\tpublic Map<String, String> getParamMap(){\n\t\t// 获取所有参数\n\t\tMap<String, String[]> parameterMap = request.getParameterMap();\n\t\tMap<String, String> map = new LinkedHashMap<>(parameterMap.size());\n\t\tfor (String key : parameterMap.keySet()) {\n\t\t\tString[] values = parameterMap.get(key);\n\t\t\tmap.put(key, values[0]);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 在 [请求头] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getHeader(String name) {\n\t\treturn request.getHeader(name);\n\t}\n\n\t/**\n\t * 在 [Cookie作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getCookieValue(String name) {\n\t\treturn getCookieLastValue(name);\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n\t */\n\t@Override\n\tpublic String getCookieFirstValue(String name){\n\t\tCookie[] cookies = request.getCookies();\n\t\tif (cookies != null) {\n\t\t\tfor (Cookie cookie : cookies) {\n\t\t\t\tif (cookie != null && name.equals(cookie.getName())) {\n\t\t\t\t\treturn cookie.getValue();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\t@Override\n\tpublic String getCookieLastValue(String name){\n\t\tString value = null;\n\t\tCookie[] cookies = request.getCookies();\n\t\tif (cookies != null) {\n\t\t\tfor (Cookie cookie : cookies) {\n\t\t\t\tif (cookie != null && name.equals(cookie.getName())) {\n\t\t\t\t\tvalue = cookie.getValue();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 返回当前请求path (不包括上下文名称) \n\t */\n\t@Override\n\tpublic String getRequestPath() {\n\t\treturn ApplicationInfo.cutPathPrefix(request.getRequestURI());\n\t}\n\n\t/**\n\t * 返回当前请求的url，例：http://xxx.com/test\n\t * @return see note\n\t */\n\t@Override\n\tpublic String getUrl() {\n\t\tString currDomain = SaManager.getConfig().getCurrDomain();\n\t\tif( ! SaFoxUtil.isEmpty(currDomain)) {\n\t\t\treturn currDomain + this.getRequestPath();\n\t\t}\n\t\treturn request.getRequestURL().toString();\n\t}\n\t\n\t/**\n\t * 返回当前请求的类型 \n\t */\n\t@Override\n\tpublic String getMethod() {\n\t\treturn request.getMethod();\n\t}\n\n\t/**\n\t * 查询请求 host\n\t */\n\t@Override\n\tpublic String getHost() {\n\t\treturn request.getServerName();\n\t}\n\n\t/**\n\t * 转发请求 \n\t */\n\t@Override\n\tpublic Object forward(String path) {\n\t\ttry {\n\t\t\tHttpServletResponse response = (HttpServletResponse)SaManager.getSaTokenContext().getResponse().getSource();\n\t\t\trequest.getRequestDispatcher(path).forward(request, response);\n\t\t\treturn null;\n\t\t} catch (ServletException | IOException e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaServletErrorCode.CODE_20001);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.model;\n\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.servlet.error.SaServletErrorCode;\nimport jakarta.servlet.http.HttpServletResponse;\n\n/**\n * 对 SaResponse 包装类的实现（Jakarta-Servlet 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaResponseForServlet implements SaResponse {\n\n\t/**\n\t * 底层Request对象\n\t */\n\tprotected HttpServletResponse response;\n\t\n\t/**\n\t * 实例化\n\t * @param response response对象 \n\t */\n\tpublic SaResponseForServlet(HttpServletResponse response) {\n\t\tthis.response = response;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn response;\n\t}\n\n\t/**\n\t * 设置响应状态码 \n\t */\n\t@Override\n\tpublic SaResponse setStatus(int sc) {\n\t\tresponse.setStatus(sc);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 在响应头里写入一个值 \n\t */\n\t@Override\n\tpublic SaResponse setHeader(String name, String value) {\n\t\tresponse.setHeader(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在响应头里添加一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\t@Override\n\tpublic SaResponse addHeader(String name, String value) {\n\t\tresponse.addHeader(name, value);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 重定向 \n\t */\n\t@Override\n\tpublic Object redirect(String url) {\n\t\ttry {\n\t\t\tresponse.sendRedirect(url);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaServletErrorCode.CODE_20002);\n\t\t}\n\t\treturn null;\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.model;\n\nimport cn.dev33.satoken.context.model.SaStorage;\nimport jakarta.servlet.http.HttpServletRequest;\n\n/**\n * 对 SaStorage 包装类的实现（Jakarta-Servlet 版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaStorageForServlet implements SaStorage {\n\n\t/**\n\t * 底层Request对象\n\t */\n\tprotected HttpServletRequest request;\n\t\n\t/**\n\t * 实例化\n\t * @param request request对象 \n\t */\n\tpublic SaStorageForServlet(HttpServletRequest request) {\n\t\tthis.request = request;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn request;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里写入一个值 \n\t */\n\t@Override\n\tpublic SaStorageForServlet set(String key, Object value) {\n\t\trequest.setAttribute(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn request.getAttribute(key);\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里删除一个值 \n\t */\n\t@Override\n\tpublic SaStorageForServlet delete(String key) {\n\t\trequest.removeAttribute(key);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token对接 Jakarta-Servlet API 容器所需要的实现类接口包\n */\npackage cn.dev33.satoken.servlet;"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaJakartaServletOperateUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.util;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport jakarta.servlet.ServletResponse;\n\nimport java.io.IOException;\n\n/**\n * Jakarta Servlet 操作工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaJakartaServletOperateUtil {\n\n\t/**\n\t * 写入结果到输出流\n\t * @param response /\n\t * @param result /\n\t */\n\tpublic static void writeResult(ServletResponse response, String result) throws IOException {\n\t\t// 写入输出流\n\t\t// \t\t请注意此处默认 Content-Type 为 text/plain，如果需要返回 JSON 信息，需要在 return 前自行设置 Content-Type 为 application/json\n\t\t// \t\t例如：SaHolder.getResponse().setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n\t\tif(response.getContentType() == null) {\n\t\t\tresponse.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);\n\t\t}\n\t\tresponse.getWriter().print(result);\n\t\tresponse.getWriter().flush();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextJakartaServletUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.util;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.model.SaStorageForServlet;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\n\n\n/**\n * SaTokenContext 上下文读写工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextJakartaServletUtil {\n\n\t/**\n\t * 写入当前上下文\n\t * @param request /\n\t * @param response /\n\t */\n\tpublic static void setContext(HttpServletRequest request, HttpServletResponse response) {\n\t\tSaRequest req = new SaRequestForServlet(request);\n\t\tSaResponse res = new SaResponseForServlet(response);\n\t\tSaStorage stg = new SaStorageForServlet(request);\n\t\tSaManager.getSaTokenContext().setContext(req, res, stg);\n\t}\n\n\t/**\n\t * 写入上下文对象, 并在执行函数后将其清除\n\t * @param request /\n\t * @param response /\n\t * @param fun /\n\t */\n\tpublic static void setContext(HttpServletRequest request, HttpServletResponse response, SaFunction fun) {\n\t\ttry {\n\t\t\tsetContext(request, response);\n\t\t\tfun.run();\n\t\t} finally {\n\t\t\tclearContext();\n\t\t}\n\t}\n\n\t/**\n\t * 写入上下文对象, 并在执行函数后将其清除\n\t *\n\t * @param request /\n\t * @param response /\n\t * @param fun /\n\t * @return /\n\t * @param <T> /\n\t */\n\tpublic static <T> T setContext(HttpServletRequest request, HttpServletResponse response, SaRetGenericFunction<T> fun) {\n\t\ttry {\n\t\t\tsetContext(request, response);\n\t\t\treturn fun.run();\n\t\t} finally {\n\t\t\tclearContext();\n\t\t}\n\t}\n\n\t/**\n\t * 清除当前上下文\n\t */\n\tpublic static void clearContext() {\n\t\tSaManager.getSaTokenContext().clearContext();\n\t}\n\n\t/**\n\t * 获取当前 ModelBox\n\t * @return /\n\t */\n\tpublic static SaTokenContextModelBox getModelBox() {\n\t\treturn SaManager.getSaTokenContext().getModelBox();\n\t}\n\n\t/**\n\t * 获取当前 Request\n\t * @return /\n\t */\n\tpublic static HttpServletRequest getRequest() {\n\t\treturn (HttpServletRequest) getModelBox().getRequest().getSource();\n\t}\n\n\t/**\n\t * 获取当前 Response\n\t * @return /\n\t */\n\tpublic static HttpServletResponse getResponse() {\n\t\treturn (HttpServletResponse) getModelBox().getResponse().getSource();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-starter</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n    <packaging>jar</packaging>\n\n    <name>sa-token-jboot-plugin</name>\n    <artifactId>sa-token-jboot-plugin</artifactId>\n    <description>jboot integrate sa-token</description>\n\n    <properties>\n        <maven.compiler.source>8</maven.compiler.source>\n        <maven.compiler.target>8</maven.compiler.target>\n        <jedis.version>3.8.0</jedis.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>io.jboot</groupId>\n            <artifactId>jboot</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-servlet</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>redis.clients</groupId>\n            <artifactId>jedis</artifactId>\n            <version>${jedis.version}</version>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n        <plugin>\n            <groupId>org.apache.maven.plugins</groupId>\n            <artifactId>maven-compiler-plugin</artifactId>\n            <version>3.6.1</version>\n            <configuration>\n                <source>1.8</source>\n                <target>1.8</target>\n                <encoding>UTF-8</encoding>\n                <compilerArgument>-parameters</compilerArgument>\n            </configuration>\n        </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/PathAnalyzer.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class PathAnalyzer {\n\n    private static final Map<String, PathAnalyzer> cached = new LinkedHashMap<>();\n    private final Pattern pattern;\n\n    public static PathAnalyzer get(String expr) {\n        PathAnalyzer pa = cached.get(expr);\n        if (pa == null) {\n            synchronized(expr.intern()) {\n                pa = cached.get(expr);\n                if (pa == null) {\n                    pa = new PathAnalyzer(expr);\n                    cached.put(expr, pa);\n                }\n            }\n        }\n\n        return pa;\n    }\n\n    private PathAnalyzer(String expr) {\n        this.pattern = Pattern.compile(exprCompile(expr), Pattern.CASE_INSENSITIVE);\n    }\n\n    public Matcher matcher(String uri) {\n        return this.pattern.matcher(uri);\n    }\n\n    public boolean matches(String uri) {\n        return this.pattern.matcher(uri).find();\n    }\n\n    private static String exprCompile(String expr) {\n        String p = expr.replace(\".\", \"\\\\.\");\n        p = p.replace(\"$\", \"\\\\$\");\n        p = p.replace(\"**\", \".[]\");\n        p = p.replace(\"*\", \"[^/]*\");\n        if (p.contains(\"{\")) {\n            if (p.indexOf(\"_}\") > 0) {\n                p = p.replaceAll(\"\\\\{[^\\\\}]+?\\\\_\\\\}\", \"(.+?)\");\n            }\n\n            p = p.replaceAll(\"\\\\{[^\\\\}]+?\\\\}\", \"([^/]+?)\");\n        }\n\n        if (!p.startsWith(\"/\")) {\n            p = \"/\" + p;\n        }\n\n        p = p.replace(\".[]\", \".*\");\n        return \"^\" + p + \"$\";\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaAnnotationInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot;\n\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport com.jfinal.aop.Interceptor;\nimport com.jfinal.aop.Invocation;\n\n/**\n * 注解式鉴权 - 拦截器\n */\npublic class SaAnnotationInterceptor implements Interceptor {\n    @Override\n    public void intercept(Invocation invocation) {\n        SaAnnotationStrategy.instance.checkMethodAnnotation.accept((invocation.getMethod()));\n        invocation.invoke();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaJdkSerializer.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot;\n\nimport com.jfinal.log.Log;\nimport io.jboot.components.serializer.JbootSerializer;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\n\npublic class SaJdkSerializer implements JbootSerializer {\n\n    private static final Log LOG = Log.getLog(SaJdkSerializer.class);\n\n    @Override\n    public byte[] serialize(Object value) {\n        if (value == null) {\n            return null;\n        }\n        ObjectOutputStream objectOut = null;\n        try {\n            ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024);\n            objectOut = new ObjectOutputStream(bytesOut);\n            objectOut.writeObject(value);\n            objectOut.flush();\n            return bytesOut.toByteArray();\n        }\n        catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        finally {\n            if(objectOut != null)\n                try {objectOut.close();} catch (Exception e) {\n                    LOG.error(e.getMessage(), e);}\n        }\n    }\n\n    @Override\n    public Object deserialize(byte[] bytes) {\n        if (bytes == null || bytes.length == 0) {\n            return null;\n        }\n        ObjectInputStream objectInput = null;\n        try {\n            ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes);\n            objectInput = new ObjectInputStream(bytesInput);\n            return objectInput.readObject();\n        }\n        catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        finally {\n            if (objectInput != null)\n                try {objectInput.close();} catch (Exception e) {LOG.error(e.getMessage(), e);}\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaRedisCache.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot;\n\nimport com.jfinal.plugin.ehcache.IDataLoader;\nimport io.jboot.components.cache.JbootCache;\nimport io.jboot.components.cache.JbootCacheConfig;\nimport io.jboot.core.spi.JbootSpi;\nimport io.jboot.exception.JbootIllegalConfigException;\nimport io.jboot.support.redis.JbootRedisConfig;\nimport io.jboot.support.redis.RedisScanResult;\nimport io.jboot.utils.StrUtil;\nimport redis.clients.jedis.*;\nimport redis.clients.jedis.exceptions.JedisConnectionException;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * sa 缓存处理\n */\n@JbootSpi(\"sacache\")\n@SuppressWarnings({\"unchecked\", \"rawtypes\"})\npublic class SaRedisCache implements JbootCache {\n    protected JbootRedisConfig config;\n    protected JedisPool jedisPool;\n    private final ThreadLocal<String> CACHE_NAME_PREFIX_TL = new ThreadLocal<>();\n\n\tpublic SaRedisCache(JbootRedisConfig config) {\n        this.config = config;\n\n        String host = config.getHost();\n        Integer port = config.getPort();\n        Integer timeout = config.getTimeout();\n        String password = config.getPassword();\n        Integer database = config.getDatabase();\n        String clientName = config.getClientName();\n\n        if (host.contains(\":\")) {\n            port = Integer.valueOf(host.split(\":\")[1]);\n        }\n\n\n        JedisPoolConfig poolConfig = new JedisPoolConfig();\n\n        if (StrUtil.isNotBlank(config.getTestWhileIdle())) {\n            poolConfig.setTestWhileIdle(config.getTestWhileIdle());\n        }\n\n        if (StrUtil.isNotBlank(config.getTestOnBorrow())) {\n            poolConfig.setTestOnBorrow(config.getTestOnBorrow());\n        }\n\n        if (StrUtil.isNotBlank(config.getTestOnCreate())) {\n            poolConfig.setTestOnCreate(config.getTestOnCreate());\n        }\n\n        if (StrUtil.isNotBlank(config.getTestOnReturn())) {\n            poolConfig.setTestOnReturn(config.getTestOnReturn());\n        }\n\n        if (StrUtil.isNotBlank(config.getMinEvictableIdleTimeMillis())) {\n            poolConfig.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis());\n        }\n\n        if (StrUtil.isNotBlank(config.getTimeBetweenEvictionRunsMillis())) {\n            poolConfig.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis());\n        }\n\n        if (StrUtil.isNotBlank(config.getNumTestsPerEvictionRun())) {\n            poolConfig.setNumTestsPerEvictionRun(config.getNumTestsPerEvictionRun());\n        }\n\n        if (StrUtil.isNotBlank(config.getMaxTotal())) {\n            poolConfig.setMaxTotal(config.getMaxTotal());\n        }\n\n        if (StrUtil.isNotBlank(config.getMaxIdle())) {\n            poolConfig.setMaxIdle(config.getMaxIdle());\n        }\n\n        if (StrUtil.isNotBlank(config.getMinIdle())) {\n            poolConfig.setMinIdle(config.getMinIdle());\n        }\n\n        if (StrUtil.isNotBlank(config.getMaxWaitMillis())) {\n            poolConfig.setMaxWaitMillis(config.getMaxWaitMillis());\n        }\n\n        this.jedisPool = new JedisPool(poolConfig, host, port, timeout, timeout, password, database, clientName);\n    }\n\n    public SaRedisCache(JedisPool jedisPool) {\n        this.jedisPool = jedisPool;\n    }\n\n    @Override\n    public JbootCache setCurrentCacheNamePrefix(String cacheNamePrefix) {\n        if (StrUtil.isNotBlank(cacheNamePrefix)) {\n            CACHE_NAME_PREFIX_TL.set(cacheNamePrefix);\n        } else {\n            CACHE_NAME_PREFIX_TL.remove();\n        }\n        return this;\n    }\n\n    @Override\n    public void removeCurrentCacheNamePrefix() {\n        CACHE_NAME_PREFIX_TL.remove();\n    }\n\n    @Override\n    public JbootCacheConfig getConfig() {\n        return null;\n    }\n\n\t@Override\n    public <T> T get(String cacheName, Object key) {\n        Jedis jedis = getJedis();\n        try {\n            return (T) (jedis.get(key.toString()));\n        } finally {\n            returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void put(String cacheName, Object key, Object value) {\n        Jedis jedis = getJedis();\n        try {\n            jedis.set(key.toString(), value.toString());\n        } finally {\n            returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void put(String cacheName, Object key, Object value, int liveSeconds) {\n        Jedis jedis = getJedis();\n        try {\n            jedis.setex(key.toString(), Long.parseLong(liveSeconds + \"\"), value.toString());\n        } finally {\n            returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void remove(String cacheName, Object key) {\n        Jedis jedis = getJedis();\n        try {\n            jedis.del(key.toString());\n        } finally {\n            returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void removeAll(String cacheName) {\n\n    }\n\n    @Override\n    public <T> T get(String cacheName, Object key, IDataLoader dataLoader) {\n        return null;\n    }\n\n    @Override\n    public <T> T get(String cacheName, Object key, IDataLoader dataLoader, int liveSeconds) {\n        return null;\n    }\n\n    @Override\n    public Integer getTtl(String cacheName, Object key) {\n        Jedis jedis = getJedis();\n        try {\n            return jedis.ttl(key.toString()).intValue();\n        } finally {\n            returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void setTtl(String cacheName, Object key, int seconds) {\n        Jedis jedis = getJedis();\n        try {\n            jedis.expire(key.toString(), Long.parseLong(seconds + \"\"));\n        } finally {\n            returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void refresh(String cacheName, Object key) {\n\n    }\n\n    @Override\n    public void refresh(String cacheName) {\n\n    }\n\n    @Override\n    public List getNames() {\n        return null;\n    }\n\n    @Override\n    public List getKeys(String cacheName) {\n        List<String> keys = new ArrayList<>();\n        String cursor = \"0\";\n        int scanCount = 1000;\n        boolean continueState = true;\n        do {\n            RedisScanResult<String> redisScanResult = this.scan(\"*\", cursor, scanCount);\n            List<String> scanKeys = redisScanResult.getResults();\n            cursor = redisScanResult.getCursor();\n\n            if (scanKeys != null && scanKeys.size() > 0) {\n                for (String key : scanKeys) {\n                    keys.add(key.substring(3));\n                }\n            }\n\n            if (redisScanResult.isCompleteIteration()) {\n                continueState = false;\n            }\n        } while (continueState);\n\n        return keys;\n    }\n\n    public Jedis getJedis() {\n        try {\n            return jedisPool.getResource();\n        } catch (JedisConnectionException e) {\n            throw new JbootIllegalConfigException(\"can not connect to redis host  \" + config.getHost() + \":\" + config.getPort() + \" ,\" +\n                    \" cause : \" + e, e);\n        }\n    }\n\n\n    public void returnResource(Jedis jedis) {\n        if (jedis != null) {\n            jedis.close();\n        }\n    }\n\n    public RedisScanResult<String> scan(String pattern, String cursor, int scanCount) {\n        ScanParams params = new ScanParams();\n        params.match(pattern).count(scanCount);\n        try (Jedis jedis = getJedis()) {\n            ScanResult<String> scanResult = jedis.scan(cursor, params);\n            return new RedisScanResult<>(scanResult.getCursor(), scanResult.getResult());\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenCacheDao.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport io.jboot.Jboot;\nimport io.jboot.components.serializer.JbootSerializer;\nimport io.jboot.exception.JbootIllegalConfigException;\nimport io.jboot.support.redis.JbootRedisConfig;\nimport io.jboot.utils.ConfigUtil;\nimport redis.clients.jedis.Jedis;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * 使用Jboot的缓存方法存取Token数据\n */\n@SuppressWarnings({\"unchecked\", \"rawtypes\"})\npublic class SaTokenCacheDao implements SaTokenDaoBySessionFollowObject {\n\n    protected SaRedisCache saRedisCache;\n    protected JbootSerializer serializer;\n\n    private final Map<String, SaRedisCache> saRedisMap = new ConcurrentHashMap();\n\n    /**\n     * 使用默认redis配置\n     */\n    public SaTokenCacheDao() {\n        JbootRedisConfig config = Jboot.config(JbootRedisConfig.class);\n        this.saRedisCache = new SaRedisCache(config);\n        this.serializer = new SaJdkSerializer();\n    }\n\n    /**\n     * 调用的Cache名称\n     *\n     * @param cacheName 使用的缓存配置名，默认为 default\n     */\n    public SaTokenCacheDao(String cacheName) {\n        SaRedisCache saCache = this.saRedisMap.get(cacheName);\n        if (saCache == null) {\n            synchronized (this) {\n                saCache = this.saRedisMap.get(cacheName);\n                if (saCache == null) {\n                    Map<String, JbootRedisConfig> configModels = ConfigUtil.getConfigModels(JbootRedisConfig.class);\n                    if (!configModels.containsKey(cacheName)) {\n                        throw new JbootIllegalConfigException(\"Please config \\\"jboot.redis.\" + cacheName + \".host\\\" in your jboot.properties.\");\n                    }\n\n                    JbootRedisConfig jbootRedisConfig = configModels.get(cacheName);\n                    saCache = new SaRedisCache(jbootRedisConfig);\n                    this.saRedisMap.put(cacheName, saCache);\n                }\n            }\n        }\n        this.saRedisCache = saCache;\n        this.serializer = new SaJdkSerializer();\n    }\n\n\n    @Override\n    public String get(String key) {\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            return jedis.get(key);\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void set(String key, String value, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            if (timeout == SaTokenDao.NEVER_EXPIRE) {\n                jedis.set(key, value);\n            } else {\n                jedis.setex(key, timeout, value);\n            }\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void update(String key, String value) {\n        long expire = getTimeout(key);\n        // -2 = 无此键\n        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        this.set(key, value, expire);\n    }\n\n    @Override\n    public void delete(String key) {\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            jedis.del(key);\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public long getTimeout(String key) {\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            return jedis.ttl(key);\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void updateTimeout(String key, long timeout) {\n        //判断是否想要设置为永久\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            long expire = getTimeout(key);\n            if (expire == SaTokenDao.NEVER_EXPIRE) {\n                // 如果其已经被设置为永久，则不作任何处理\n            } else {\n                // 如果尚未被设置为永久，那么再次set一次\n                this.set(key, this.get(key), timeout);\n            }\n            return;\n        }\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            jedis.expire(key, timeout);\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public Object getObject(String key) {\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            return valueFromBytes(jedis.get(keyToBytes(key)));\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public <T> T getObject(String key, Class<T> classType) {\n        return (T) getObject(key);\n    }\n\n    @Override\n    public void setObject(String key, Object object, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            if (timeout == SaTokenDao.NEVER_EXPIRE) {\n                jedis.set(keyToBytes(key), valueToBytes(object));\n            } else {\n                jedis.setex(keyToBytes(key), timeout, valueToBytes(object));\n            }\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void updateObject(String key, Object object) {\n        long expire = getObjectTimeout(key);\n        // -2 = 无此键\n        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        this.setObject(key, object, expire);\n    }\n\n    @Override\n    public void deleteObject(String key) {\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            jedis.del(keyToBytes(key));\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public long getObjectTimeout(String key) {\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            return jedis.ttl(keyToBytes(key));\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public void updateObjectTimeout(String key, long timeout) {\n        //判断是否想要设置为永久\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            long expire = getObjectTimeout(key);\n            if (expire == SaTokenDao.NEVER_EXPIRE) {\n                // 如果其已经被设置为永久，则不作任何处理\n            } else {\n                // 如果尚未被设置为永久，那么再次set一次\n                this.setObject(key, this.getObject(key), timeout);\n            }\n            return;\n        }\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            jedis.expire(keyToBytes(key), timeout);\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n    @Override\n    public SaSession getSession(String sessionId) {\n        return SaTokenDaoBySessionFollowObject.super.getSession(sessionId);\n    }\n\n    @Override\n    public void setSession(SaSession session, long timeout) {\n        SaTokenDaoBySessionFollowObject.super.setSession(session, timeout);\n    }\n\n    @Override\n    public void updateSession(SaSession session) {\n        SaTokenDaoBySessionFollowObject.super.updateSession(session);\n    }\n\n    @Override\n    public void deleteSession(String sessionId) {\n        SaTokenDaoBySessionFollowObject.super.deleteSession(sessionId);\n    }\n\n    @Override\n    public long getSessionTimeout(String sessionId) {\n        return SaTokenDaoBySessionFollowObject.super.getSessionTimeout(sessionId);\n    }\n\n    @Override\n    public void updateSessionTimeout(String sessionId, long timeout) {\n        SaTokenDaoBySessionFollowObject.super.updateSessionTimeout(sessionId, timeout);\n    }\n\n    @Override\n    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n        Jedis jedis = saRedisCache.getJedis();\n        try {\n            Set<String> keys = jedis.keys(prefix + \"*\" + keyword + \"*\");\n            List<String> list = new ArrayList<>(keys);\n            return SaFoxUtil.searchList(list, start, size, sortType);\n        } finally {\n            saRedisCache.returnResource(jedis);\n        }\n    }\n\n\n    protected byte[] keyToBytes(Object key) {\n        return key.toString().getBytes();\n    }\n\n    protected byte[] valueToBytes(Object value) {\n        return serializer.serialize(value);\n    }\n\n    protected Object valueFromBytes(byte[] bytes) {\n        if (bytes == null || bytes.length == 0) {\n            return null;\n        }\n        return serializer.deserialize(bytes);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenContextForJboot.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot;\n\nimport cn.dev33.satoken.context.SaTokenContextForReadOnly;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.model.SaStorageForServlet;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport io.jboot.web.controller.JbootControllerContext;\n\n/**\n * Sa-Token 上线文处理器 [Jboot 版本实现]\n */\npublic class SaTokenContextForJboot implements SaTokenContextForReadOnly {\n\n    public SaTokenContextForJboot() {\n        // 重写路由匹配算法\n        SaStrategy.instance.routeMatcher = (pattern, path) -> {\n            return PathAnalyzer.get(pattern).matches(path);\n        };\n    }\n\n    /**\n     * 获取当前请求的Request对象\n     */\n    @Override\n    public SaRequest getRequest() {\n        return new SaRequestForServlet(JbootControllerContext.get().getRequest());\n    }\n\n    /**\n     * 获取当前请求的Response对象\n     */\n    @Override\n    public SaResponse getResponse() {\n        return new SaResponseForServlet(JbootControllerContext.get().getResponse());\n    }\n\n    /**\n     * 获取当前请求的 [存储器] 对象\n     */\n    @Override\n    public SaStorage getStorage() {\n        return new SaStorageForServlet(JbootControllerContext.get().getRequest());\n    }\n\n    @Override\n    public boolean isValid() {\n        return JbootControllerContext.get() != null;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/main/java/cn/dev33/satoken/jboot/SaTokenPathFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.filter.SaFilterAuthStrategy;\nimport cn.dev33.satoken.filter.SaFilterErrorStrategy;\nimport cn.dev33.satoken.filter.SaFilter;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class SaTokenPathFilter implements SaFilter {\n\n    // ------------------------ 设置此过滤器 拦截 & 放行 的路由\n\n    /**\n     * 拦截路由\n     */\n    public List<String> includeList = new ArrayList<>();\n\n    /**\n     * 放行路由\n     */\n    public List<String> excludeList = new ArrayList<>();\n\n    @Override\n    public SaTokenPathFilter addInclude(String... paths) {\n        includeList.addAll(Arrays.asList(paths));\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter addExclude(String... paths) {\n        excludeList.addAll(Arrays.asList(paths));\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setIncludeList(List<String> pathList) {\n        includeList = pathList;\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setExcludeList(List<String> pathList) {\n        excludeList = pathList;\n        return this;\n    }\n\n\n    // ------------------------ 钩子函数\n\n    /**\n     * 认证函数：每次请求执行\n     */\n    public SaFilterAuthStrategy auth = r -> {};\n\n    /**\n     * 异常处理函数：每次[认证函数]发生异常时执行此函数\n     */\n    public SaFilterErrorStrategy error = e -> {\n        throw new SaTokenException(e);\n    };\n\n    /**\n     * 前置函数：在每次[认证函数]之前执行\n     *      <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n     */\n    public SaFilterAuthStrategy beforeAuth = r -> {};\n\n    @Override\n    public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) {\n        this.auth = auth;\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setError(SaFilterErrorStrategy error) {\n        this.error = error;\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n        this.beforeAuth = beforeAuth;\n        return this;\n    }\n\n\n    /*@Override\n    public void doFilter(Controller ctx, FilterChain chain) throws Throwable {\n        try {\n            // 执行全局过滤器\n            beforeAuth.run(null);\n            SaRouter.match(includeList).notMatch(excludeList).check(r -> {\n                auth.run(null);\n            });\n\n        } catch (StopMatchException e) {\n\n        } catch (Throwable e) {\n            // 1. 获取异常处理策略结果\n            String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));\n            // 2. 写入输出流\n            ctx.renderText(result);\n            return;\n        }\n\n        // 执行\n        chain.doFilter(ctx);\n    }*/\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AppRun.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot.test;\n\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.stp.StpUtil;\nimport io.jboot.Jboot;\nimport io.jboot.app.JbootApplication;\nimport io.jboot.web.controller.JbootController;\nimport io.jboot.web.controller.annotation.RequestMapping;\n\n@RequestMapping(\"/\")\npublic class AppRun extends JbootController {\n    public static void main(String[] args) {\n        JbootApplication.run(args);\n    }\n\n    public void index() {\n        renderText(\"index\");\n    }\n\n    public void doLogin() {\n        StpUtil.login(10001);\n        //赋值角色\n        renderText(\"登录成功\");\n    }\n\n    public void getLoginInfo() {\n        System.out.println(\"是否登录：\" + StpUtil.isLogin());\n        System.out.println(\"登录信息\" + StpUtil.getTokenInfo());\n        renderJson(StpUtil.getTokenInfo());\n    }\n\n    @SaCheckRole(\"super-admin\")\n    public void add() {\n        renderText(\"超级管理员方法！\");\n    }\n\n    @SuppressWarnings(\"unused\")\n    public void token(String token) {\n\t\tObject t = Jboot.getRedis().get(\"xxxxx\"); //默认redis库\n        SaSession saSession = StpUtil.getSessionByLoginId(StpUtil.getLoginIdByToken(token), false); //satoken redis库\n        renderJson(saSession);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/AtteStartListener.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot.test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaCookieConfig;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.context.SaTokenContext;\nimport cn.dev33.satoken.jboot.SaAnnotationInterceptor;\nimport cn.dev33.satoken.jboot.SaTokenCacheDao;\nimport cn.dev33.satoken.jboot.SaTokenContextForJboot;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.jfinal.config.Constants;\nimport com.jfinal.config.Interceptors;\nimport com.jfinal.config.Routes;\nimport com.jfinal.template.Engine;\nimport io.jboot.aop.jfinal.JfinalHandlers;\nimport io.jboot.aop.jfinal.JfinalPlugins;\nimport io.jboot.core.listener.JbootAppListener;\n\npublic class AtteStartListener implements JbootAppListener {\n    public void onInit() {\n        SaTokenContext saTokenContext = new SaTokenContextForJboot();\n        SaManager.setSaTokenContext(saTokenContext);\n        SaManager.setStpInterface(new StpInterfaceImpl());\n        SaTokenConfig saTokenConfig = new SaTokenConfig();\n        saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID);\n        saTokenConfig.setTimeout(60*60*4);  //登录有效时间4小时\n        saTokenConfig.setActiveTimeout(30*60); //半小时无操作就冻结 token \n        saTokenConfig.setIsShare(false);\n        saTokenConfig.setTokenName(\"token\");    //更换satoken的名称\n        saTokenConfig.setCookie(new SaCookieConfig().setHttpOnly(true));    //开启cookies的httponly属性\n        SaManager.setConfig(saTokenConfig);\n    }\n\n    @Override\n    public void onConstantConfig(Constants constants) {\n\n    }\n\n    @Override\n    public void onRouteConfig(Routes routes) {\n\n    }\n\n    @Override\n    public void onEngineConfig(Engine engine) {\n\n    }\n\n    @Override\n    public void onPluginConfig(JfinalPlugins plugins) {\n\n    }\n\n    @Override\n    public void onInterceptorConfig(Interceptors interceptors) {\n        //开启注解方式权限验证\n        interceptors.add(new SaAnnotationInterceptor());\n    }\n\n    @Override\n    public void onHandlerConfig(JfinalHandlers handlers) {\n\n    }\n\n    @Override\n    public void onStartBefore() {\n\n    }\n\n    @Override\n    public void onStart() {\n        SaManager.setSaTokenDao(new SaTokenCacheDao(\"sa\"));\n    }\n\n    @Override\n    public void onStartFinish() {\n\n    }\n\n    @Override\n    public void onStop() {\n\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/test/java/cn/dev33/satoken/jboot/test/StpInterfaceImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jboot.test;\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport io.jboot.aop.annotation.Bean;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Bean\npublic class StpInterfaceImpl implements StpInterface {\n    @Override\n    public List<String> getPermissionList(Object o, String s) {\n        return null;\n    }\n\n    @Override\n    public List<String> getRoleList(Object o, String s) {\n        List<String> list = new ArrayList<String>();\n        list.add(\"admin\");\n        list.add(\"super-admin\");\n        return list;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jboot-plugin/src/test/resources/jboot.properties",
    "content": "undertow.devMode=true\nundertow.port=9980\nundertow.host=0.0.0.0\n#other redis config\njboot.cache.type=redis\njboot.redis.host=127.0.0.1\njboot.redis.port=6379\njboot.redis.password=123456\njboot.redis.database=3\n#satoken redis config\njboot.cache.sa.type=sacache\njboot.redis.sa.host=127.0.0.1\njboot.redis.sa.port=6379\njboot.redis.sa.password=123456\njboot.redis.sa.database=1"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>sa-token-starter</artifactId>\n        <groupId>cn.dev33</groupId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n    <packaging>jar</packaging>\n\n    <name>sa-token-jfinal-plugin</name>\n    <artifactId>sa-token-jfinal-plugin</artifactId>\n    <description>jfinal integrate sa-token</description>\n\n    <properties>\n        <maven.compiler.source>8</maven.compiler.source>\n        <maven.compiler.target>8</maven.compiler.target>\n    </properties>\n    <dependencies>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <version>1.7.24</version>\n        </dependency>\n        <dependency>\n            <groupId>com.jfinal</groupId>\n            <artifactId>jfinal-undertow</artifactId>\n            <version>2.8</version>\n        </dependency>\n        <dependency>\n            <groupId>com.jfinal</groupId>\n            <artifactId>jfinal</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-servlet</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-pool2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>redis.clients</groupId>\n            <artifactId>jedis</artifactId>\n            <version>3.7.0</version>\n            <exclusions>\n                <exclusion>\n                    <artifactId>slf4j-api</artifactId>\n                    <groupId>org.slf4j</groupId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>de.ruedigermoeller</groupId>\n            <artifactId>fst</artifactId>\n            <version>2.29</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.6.1</version>\n                <configuration>\n                    <source>1.8</source>\n                    <target>1.8</target>\n                    <encoding>UTF-8</encoding>\n                    <compilerArgument>-parameters</compilerArgument>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/PathAnalyzer.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class PathAnalyzer {\n\n    private static final Map<String, PathAnalyzer> cached = new LinkedHashMap<>();\n    private final Pattern pattern;\n\n    public static PathAnalyzer get(String expr) {\n        PathAnalyzer pa = cached.get(expr);\n        if (pa == null) {\n            synchronized(expr.intern()) {\n                pa = cached.get(expr);\n                if (pa == null) {\n                    pa = new PathAnalyzer(expr);\n                    cached.put(expr, pa);\n                }\n            }\n        }\n\n        return pa;\n    }\n\n    private PathAnalyzer(String expr) {\n        this.pattern = Pattern.compile(exprCompile(expr), Pattern.CASE_INSENSITIVE);\n    }\n\n    public Matcher matcher(String uri) {\n        return this.pattern.matcher(uri);\n    }\n\n    public boolean matches(String uri) {\n        return this.pattern.matcher(uri).find();\n    }\n\n    private static String exprCompile(String expr) {\n        String p = expr.replace(\".\", \"\\\\.\");\n        p = p.replace(\"$\", \"\\\\$\");\n        p = p.replace(\"**\", \".[]\");\n        p = p.replace(\"*\", \"[^/]*\");\n        if (p.contains(\"{\")) {\n            if (p.indexOf(\"_}\") > 0) {\n                p = p.replaceAll(\"\\\\{[^\\\\}]+?\\\\_\\\\}\", \"(.+?)\");\n            }\n\n            p = p.replaceAll(\"\\\\{[^\\\\}]+?\\\\}\", \"([^/]+?)\");\n        }\n\n        if (!p.startsWith(\"/\")) {\n            p = \"/\" + p;\n        }\n\n        p = p.replace(\".[]\", \".*\");\n        return \"^\" + p + \"$\";\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaAnnotationInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport com.jfinal.aop.Interceptor;\nimport com.jfinal.aop.Invocation;\n\n/**\n * 注解式鉴权 - 拦截器\n */\npublic class SaAnnotationInterceptor implements Interceptor {\n    @Override\n    public void intercept(Invocation invocation) {\n        SaAnnotationStrategy.instance.checkMethodAnnotation.accept((invocation.getMethod()));\n        invocation.invoke();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaControllerContext.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport com.jfinal.core.Controller;\n\npublic class SaControllerContext {\n    private static ThreadLocal<Controller> controllers = new ThreadLocal<>();\n\n\n    public static void hold(Controller controller) {\n        controllers.set(controller);\n    }\n\n    public static Controller get() {\n        return controllers.get();\n    }\n\n    public static void release() {\n        controllers.remove();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaJdkSerializer.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport com.jfinal.kit.LogKit;\nimport com.jfinal.plugin.redis.serializer.ISerializer;\nimport com.jfinal.plugin.redis.serializer.JdkSerializer;\nimport redis.clients.jedis.util.SafeEncoder;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\n\npublic class SaJdkSerializer implements ISerializer {\n\n    public static final ISerializer me = new JdkSerializer();\n\n    public byte[] keyToBytes(String key) {\n        return SafeEncoder.encode(key);\n    }\n\n    public String keyFromBytes(byte[] bytes) {\n        return SafeEncoder.encode(bytes);\n    }\n\n    public byte[] fieldToBytes(Object field) {\n        return valueToBytes(field);\n    }\n\n    public Object fieldFromBytes(byte[] bytes) {\n        return valueFromBytes(bytes);\n    }\n\n    public byte[] valueToBytes(Object value) {\n        ObjectOutputStream objectOut = null;\n        try {\n            ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(1024);\n            objectOut = new ObjectOutputStream(bytesOut);\n            objectOut.writeObject(value);\n            objectOut.flush();\n            return bytesOut.toByteArray();\n        }\n        catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        finally {\n            if(objectOut != null)\n                try {objectOut.close();} catch (Exception e) {\n                    LogKit.error(e.getMessage(), e);}\n        }\n    }\n\n    public Object valueFromBytes(byte[] bytes) {\n        if(bytes == null || bytes.length == 0)\n            return null;\n\n        ObjectInputStream objectInput = null;\n        try {\n            ByteArrayInputStream bytesInput = new ByteArrayInputStream(bytes);\n            objectInput = new ObjectInputStream(bytesInput);\n            return objectInput.readObject();\n        }\n        catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        finally {\n            if (objectInput != null)\n                try {objectInput.close();} catch (Exception e) {LogKit.error(e.getMessage(), e);}\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenActionHandler.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport com.jfinal.aop.Invocation;\nimport com.jfinal.config.Constants;\nimport com.jfinal.core.*;\nimport com.jfinal.kit.ReflectKit;\nimport com.jfinal.log.Log;\nimport com.jfinal.render.Render;\nimport com.jfinal.render.RenderException;\nimport com.jfinal.render.RenderManager;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n\npublic class SaTokenActionHandler extends ActionHandler {\n    protected boolean devMode;\n    protected ActionMapping actionMapping;\n    protected ControllerFactory controllerFactory;\n    protected ActionReporter actionReporter;\n    protected static final RenderManager renderManager = RenderManager.me();\n    private static final Log log = Log.getLog(ActionHandler.class);\n\n    protected void init(ActionMapping actionMapping, Constants constants) {\n        this.actionMapping = actionMapping;\n        this.devMode = constants.getDevMode();\n        this.controllerFactory = constants.getControllerFactory();\n        this.actionReporter = constants.getActionReporter();\n    }\n\n    /**\n     * 子类覆盖 getAction 方法可以定制路由功能\n     */\n    protected Action getAction(String target, String[] urlPara) {\n        return actionMapping.getAction(target, urlPara);\n    }\n\n    @Override\n    public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {\n        if (target.indexOf('.') != -1) {\n            return ;\n        }\n\n        isHandled[0] = true;\n        String[] urlPara = {null};\n        Action action = getAction(target, urlPara);\n\n        if (action == null) {\n            if (log.isWarnEnabled()) {\n                String qs = request.getQueryString();\n                log.warn(\"404 Action Not Found: \" + (qs == null ? target : target + \"?\" + qs));\n            }\n            renderManager.getRenderFactory().getErrorRender(404).setContext(request, response).render();\n            return ;\n        }\n\n        Controller controller = null;\n        try {\n            // Controller controller = action.getControllerClass().newInstance();\n            controller = controllerFactory.getController(action.getControllerClass());\n            CPI._init_(controller, action, request, response, urlPara[0]);\n//            if (resolveJson && controller.isJsonRequest()) {\n//                // 注入 JsonRequest 包装对象接管 request\n//                controller.setHttpServletRequest(jsonRequestFactory.apply(controller.getRawData(), controller.getRequest()));\n//            }\n             //加入SaToken上下文处理\n            SaControllerContext.hold(controller);\n            if (devMode) {\n                if (actionReporter.isReportAfterInvocation(request)) {\n                    new Invocation(action, controller).invoke();\n                    actionReporter.report(target, controller, action);\n                } else {\n                    actionReporter.report(target, controller, action);\n                    new Invocation(action, controller).invoke();\n                }\n            }\n            else {\n                new Invocation(action, controller).invoke();\n            }\n\n            Render render = controller.getRender();\n            if (render instanceof ForwardActionRender) {\n                String actionUrl = ((ForwardActionRender)render).getActionUrl();\n                if (target.equals(actionUrl)) {\n                    throw new RuntimeException(\"The forward action url is the same as before.\");\n                } else {\n                    handle(actionUrl, request, response, isHandled);\n                }\n                return ;\n            }\n\n            if (render == null) {\n                render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName());\n            }\n            render.setContext(request, response, action.getViewPath()).render();\n        }\n        catch (RenderException e) {\n            if (log.isErrorEnabled()) {\n                String qs = request.getQueryString();\n                log.error(qs == null ? target : target + \"?\" + qs, e);\n            }\n        }\n        catch (ActionException e) {\n            handleActionException(target, request, response, action, e);\n        }\n        catch (Exception e) {\n            if (log.isErrorEnabled()) {\n                String qs = request.getQueryString();\n                String targetInfo = (qs == null ? target : target + \"?\" + qs);\n                String sign = ReflectKit.getMethodSignature(action.getMethod());\n                log.error(sign + \" : \" + targetInfo, e);\n            }\n            renderManager.getRenderFactory().getErrorRender(500).setContext(request, response, action.getViewPath()).render();\n        } finally {\n            SaControllerContext.release();\n            controllerFactory.recycle(controller);\n        }\n    }\n\n    /**\n     * 抽取出该方法是为了缩短 handle 方法中的代码量，确保获得 JIT 优化，\n     * 方法长度超过 8000 个字节码时，将不会被 JIT 编译成二进制码\n     * <p>\n     * 通过开启 java 的 -XX:+PrintCompilation 启动参数得知，handle(...)\n     * 方法(73 行代码)已被 JIT 优化，优化后的字节码长度为 593 个字节，相当于\n     * 每行代码产生 8.123 个字节\n     */\n    private void handleActionException(String target, HttpServletRequest request, HttpServletResponse response, Action action, ActionException e) {\n        int errorCode = e.getErrorCode();\n        String msg = null;\n        if (errorCode == 404) {\n            msg = \"404 Not Found: \";\n        } else if (errorCode == 400) {\n            msg = \"400 Bad Request: \";\n        } else if (errorCode == 401) {\n            msg = \"401 Unauthorized: \";\n        } else if (errorCode == 403) {\n            msg = \"403 Forbidden: \";\n        }\n\n        if (msg != null) {\n            if (log.isWarnEnabled()) {\n                String qs = request.getQueryString();\n                msg = msg + (qs == null ? target : target + \"?\" + qs);\n                if (e.getMessage() != null) {\n                    msg = msg + \"\\n\" + e.getMessage();\n                }\n                log.warn(msg);\n            }\n        } else {\n            if (log.isErrorEnabled()) {\n                String qs = request.getQueryString();\n                log.error(errorCode + \" Error: \" + (qs == null ? target : target + \"?\" + qs), e);\n            }\n        }\n\n        e.getErrorRender().setContext(request, response, action.getViewPath()).render();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenContextForJfinal.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport cn.dev33.satoken.context.SaTokenContextForReadOnly;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.model.SaStorageForServlet;\nimport cn.dev33.satoken.strategy.SaStrategy;\n\n/**\n * Sa-Token 上线文处理器 [Jfinal 版本实现]\n */\npublic class SaTokenContextForJfinal implements SaTokenContextForReadOnly {\n\n    public SaTokenContextForJfinal() {\n        // 重写路由匹配算法\n        SaStrategy.instance.routeMatcher = (pattern, path) -> {\n            return PathAnalyzer.get(pattern).matches(path);\n        };\n    }\n\n    /**\n     * 获取当前请求的Request对象\n     */\n    @Override\n    public SaRequest getRequest() {\n        return new SaRequestForServlet(SaControllerContext.get().getRequest());\n    }\n\n    /**\n     * 获取当前请求的Response对象\n     */\n    @Override\n    public SaResponse getResponse() {\n        return new SaResponseForServlet(SaControllerContext.get().getResponse());\n    }\n\n    /**\n     * 获取当前请求的 [存储器] 对象\n     */\n    @Override\n    public SaStorage getStorage() {\n        return new SaStorageForServlet(SaControllerContext.get().getRequest());\n    }\n\n    @Override\n    public boolean isValid() {\n        return SaControllerContext.get() != null;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenDaoRedis.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.auto.SaTokenDaoBySessionFollowObject;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.jfinal.plugin.redis.Cache;\nimport com.jfinal.plugin.redis.Redis;\nimport com.jfinal.plugin.redis.serializer.ISerializer;\nimport redis.clients.jedis.Jedis;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\npublic class SaTokenDaoRedis implements SaTokenDaoBySessionFollowObject {\n\n    protected Cache redis;\n    protected ISerializer serializer;\n    /**\n     * 标记：是否已初始化成功\n     */\n    public boolean isInit;\n\n    public SaTokenDaoRedis(String confName) {\n        redis = Redis.use(confName);\n        serializer = new SaJdkSerializer();\n    }\n\n    /**\n     * 获取Value，如无返空\n     */\n    @Override\n    public String get(String key) {\n        Jedis jedis = getJedis();\n        try {\n            return jedis.get(key);\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 写入Value，并设定存活时间 (单位: 秒)\n     */\n    @Override\n    public void set(String key, String value, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        Jedis jedis = getJedis();\n        try {\n            if (timeout == SaTokenDao.NEVER_EXPIRE) {\n                jedis.set(key, value);\n            } else {\n                jedis.setex(key, timeout, value);\n            }\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 修改指定key-value键值对 (过期时间不变)\n     */\n    @Override\n    public void update(String key, String value) {\n        long expire = getTimeout(key);\n        // -2 = 无此键\n        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        this.set(key, value, expire);\n    }\n\n    /**\n     * 删除Value\n     */\n    @Override\n    public void delete(String key) {\n        Jedis jedis = getJedis();\n        try {\n            jedis.del(key);\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 获取Value的剩余存活时间 (单位: 秒)\n     */\n    @Override\n    public long getTimeout(String key) {\n        Jedis jedis = getJedis();\n        try {\n            return jedis.ttl(key);\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 修改Value的剩余存活时间 (单位: 秒)\n     */\n    @Override\n    public void updateTimeout(String key, long timeout) {\n        //判断是否想要设置为永久\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            long expire = getTimeout(key);\n            if (expire == SaTokenDao.NEVER_EXPIRE) {\n                // 如果其已经被设置为永久，则不作任何处理\n            } else {\n                // 如果尚未被设置为永久，那么再次set一次\n                this.set(key, this.get(key), timeout);\n            }\n            return;\n        }\n        Jedis jedis = getJedis();\n        try {\n            jedis.expire(key, timeout);\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 获取Object，如无返空\n     */\n    @Override\n    public Object getObject(String key) {\n        Jedis jedis = getJedis();\n        try {\n            return valueFromBytes(jedis.get(keyToBytes(key)));\n        } finally {\n            close(jedis);\n        }\n    }\n\n    @Override\n    public <T> T getObject(String key, Class<T> classType) {\n        return (T) getObject(key);\n    }\n\n    /**\n     * 写入Object，并设定存活时间 (单位: 秒)\n     */\n    @Override\n    public void setObject(String key, Object object, long timeout) {\n        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        Jedis jedis = getJedis();\n        try {\n            if (timeout == SaTokenDao.NEVER_EXPIRE) {\n                jedis.set(keyToBytes(key), valueToBytes(object));\n            } else {\n                jedis.setex(keyToBytes(key), timeout, valueToBytes(object));\n            }\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 更新Object (过期时间不变)\n     */\n    @Override\n    public void updateObject(String key, Object object) {\n        long expire = getObjectTimeout(key);\n        // -2 = 无此键\n        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {\n            return;\n        }\n        this.setObject(key, object, expire);\n    }\n\n    /**\n     * 删除Object\n     */\n    @Override\n    public void deleteObject(String key) {\n        Jedis jedis = getJedis();\n        try {\n            jedis.del(keyToBytes(key));\n        } finally {\n            close(jedis);\n        }\n    }\n\n    @Override\n    public long getObjectTimeout(String key) {\n        Jedis jedis = getJedis();\n        try {\n            return jedis.ttl(keyToBytes(key));\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 修改Object的剩余存活时间 (单位: 秒)\n     */\n    @Override\n    public void updateObjectTimeout(String key, long timeout) {\n        //判断是否想要设置为永久\n        if (timeout == SaTokenDao.NEVER_EXPIRE) {\n            long expire = getObjectTimeout(key);\n            if (expire == SaTokenDao.NEVER_EXPIRE) {\n                // 如果其已经被设置为永久，则不作任何处理\n            } else {\n                // 如果尚未被设置为永久，那么再次set一次\n                this.setObject(key, this.getObject(key), timeout);\n            }\n            return;\n        }\n        Jedis jedis = getJedis();\n        try {\n            jedis.expire(keyToBytes(key), timeout);\n        } finally {\n            close(jedis);\n        }\n    }\n\n    /**\n     * 搜索数据\n     */\n    @Override\n    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {\n        Set<String> keys = redis.keys(prefix + \"*\" + keyword + \"*\");\n        List<String> list = new ArrayList<>(keys);\n        return SaFoxUtil.searchList(list, start, size, sortType);\n    }\n\n    public Jedis getJedis() {\n        return redis.getJedis();\n    }\n\n    public void close(Jedis jedis) {\n        if (jedis != null)\n            jedis.close();\n    }\n\n    protected byte[] keyToBytes(Object key) {\n        return key.toString().getBytes();\n    }\n\n    protected byte[] valueToBytes(Object value) {\n        return serializer.valueToBytes(value);\n    }\n\n    protected Object valueFromBytes(byte[] bytes) {\n        return serializer.valueFromBytes(bytes);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/main/java/cn/dev33/satoken/jfinal/SaTokenPathFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.filter.SaFilter;\nimport cn.dev33.satoken.filter.SaFilterAuthStrategy;\nimport cn.dev33.satoken.filter.SaFilterErrorStrategy;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class SaTokenPathFilter implements SaFilter {\n\n    // ------------------------ 设置此过滤器 拦截 & 放行 的路由\n\n    /**\n     * 拦截路由\n     */\n    public List<String> includeList = new ArrayList<>();\n\n    /**\n     * 放行路由\n     */\n    public List<String> excludeList = new ArrayList<>();\n\n    @Override\n    public SaTokenPathFilter addInclude(String... paths) {\n        includeList.addAll(Arrays.asList(paths));\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter addExclude(String... paths) {\n        excludeList.addAll(Arrays.asList(paths));\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setIncludeList(List<String> pathList) {\n        includeList = pathList;\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setExcludeList(List<String> pathList) {\n        excludeList = pathList;\n        return this;\n    }\n\n\n    // ------------------------ 钩子函数\n\n    /**\n     * 认证函数：每次请求执行\n     */\n    public SaFilterAuthStrategy auth = r -> {};\n\n    /**\n     * 异常处理函数：每次[认证函数]发生异常时执行此函数\n     */\n    public SaFilterErrorStrategy error = e -> {\n        throw new SaTokenException(e);\n    };\n\n    /**\n     * 前置函数：在每次[认证函数]之前执行\n     *      <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n     */\n    public SaFilterAuthStrategy beforeAuth = r -> {};\n\n    @Override\n    public SaTokenPathFilter setAuth(SaFilterAuthStrategy auth) {\n        this.auth = auth;\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setError(SaFilterErrorStrategy error) {\n        this.error = error;\n        return this;\n    }\n\n    @Override\n    public SaTokenPathFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n        this.beforeAuth = beforeAuth;\n        return this;\n    }\n\n\n    /*@Override\n    public void doFilter(Controller ctx, FilterChain chain) throws Throwable {\n        try {\n            // 执行全局过滤器\n            beforeAuth.run(null);\n            SaRouter.match(includeList).notMatch(excludeList).check(r -> {\n                auth.run(null);\n            });\n\n        } catch (StopMatchException e) {\n\n        } catch (Throwable e) {\n            // 1. 获取异常处理策略结果\n            String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));\n            // 2. 写入输出流\n            ctx.renderText(result);\n            return;\n        }\n\n        // 执行\n        chain.doFilter(ctx);\n    }*/\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/AppRun.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal.test;\n\nimport cn.dev33.satoken.annotation.SaCheckRole;\nimport cn.dev33.satoken.stp.StpUtil;\nimport com.jfinal.core.Controller;\nimport com.jfinal.core.Path;\nimport com.jfinal.server.undertow.UndertowServer;\n\n@Path(\"/\")\npublic class AppRun extends Controller {\n    public static void main(String[] args) {\n        UndertowServer.create(Config.class)\n                .addHotSwapClassPrefix(\"cn.dev33.satoken.jfinal.\")\n                .start();\n    }\n\n    public void index(){\n        renderText(\"index\");\n    }\n\n    public void doLogin(){\n        StpUtil.logout();\n        StpUtil.login(10002);\n        //赋值角色\n        renderText(\"登录成功\");\n    }\n\n    public void getLoginInfo(){\n        System.out.println(\"是否登录：\"+StpUtil.isLogin());\n        System.out.println(\"登录信息\"+StpUtil.getTokenInfo());\n        renderJson(StpUtil.getTokenInfo());\n    }\n\n    @SaCheckRole(\"super-admin\")\n    public void add(){\n        renderText(\"超级管理员方法！\");\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/Config.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal.test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaCookieConfig;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.context.SaTokenContext;\nimport cn.dev33.satoken.jfinal.*;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.jfinal.config.*;\nimport com.jfinal.plugin.redis.RedisPlugin;\nimport com.jfinal.plugin.redis.serializer.ISerializer;\nimport com.jfinal.template.Engine;\n\npublic class Config extends JFinalConfig {\n\n    public Config(){\n        //注册权限验证功能，由saToken处理请求上下文\n        SaTokenContext saTokenContext = new SaTokenContextForJfinal();\n        SaManager.setSaTokenContext(saTokenContext);\n        //加载权限角色设置数据接口\n        SaManager.setStpInterface(new StpInterfaceImpl());\n        //设置token生成类型\n        SaTokenConfig saTokenConfig = new SaTokenConfig();\n        saTokenConfig.setTokenStyle(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID);\n        saTokenConfig.setTimeout(60*60*4);  //登录有效时间4小时\n        saTokenConfig.setActiveTimeout(30*60); //半小时无操作就冻结 token \n        saTokenConfig.setIsShare(false);\n        saTokenConfig.setTokenName(\"token\");    //更改satoken的cookies名称\n        SaCookieConfig saCookieConfig = new SaCookieConfig();\n        saCookieConfig.setHttpOnly(true);   //开启cookies 的httponly属性\n        saTokenConfig.setCookie(saCookieConfig);\n        SaManager.setConfig(saTokenConfig);\n    }\n\n    @Override\n    public void configConstant(Constants constants) {\n\n    }\n\n    @Override\n    public void configRoute(Routes routes) {\n        //路由扫描\n        routes.scan(\"cn.dev33.satoken.jfinal\");\n    }\n\n    @Override\n    public void configEngine(Engine engine) {\n\n    }\n\n    @Override\n    public void configPlugin(Plugins plugins) {\n        //添加redis扩展\n        plugins.add(createRedisPlugin(\"satoken\",1, SaJdkSerializer.me));\n    }\n\n    @Override\n    public void configInterceptor(Interceptors interceptors) {\n        //开启注解方式权限验证\n        interceptors.add(new SaAnnotationInterceptor());\n    }\n\n    @Override\n    public void configHandler(Handlers handlers) {\n        //将上下文交给satoken处理\n        handlers.setActionHandler(new SaTokenActionHandler());\n    }\n\n    /**\n     * 创建Redis插件\n     * @param name 名称\n     * @param dbIndex 使用的库ID\n     * @param serializer 自定义序列化方法\n     * @return\n     */\n    private RedisPlugin createRedisPlugin(String name, Integer dbIndex, ISerializer serializer) {\n        RedisPlugin redisPlugin = new RedisPlugin(name, \"redis-host\", 6379, 3000,\"pwd\",dbIndex);\n        redisPlugin.setSerializer(serializer);\n        return redisPlugin;\n    }\n    @Override\n    public void onStart(){\n        //增加redis缓存,需要先配置redis地址\n        SaManager.setSaTokenDao(new SaTokenDaoRedis(\"satoken\"));\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-jfinal-plugin/src/test/java/cn/dev33/satoken/jfinal/test/StpInterfaceImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.jfinal.test;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class StpInterfaceImpl implements StpInterface {\n    @Override\n    public List<String> getPermissionList(Object o, String s) {\n        return null;\n    }\n\n    @Override\n    public List<String> getRoleList(Object o, String s) {\n        List<String> list = new ArrayList<String>();\n        list.add(\"admin\");\n        list.add(\"super-admin\");\n        return list;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <name>sa-token-loveqq-boot-starter</name>\n    <artifactId>sa-token-loveqq-boot-starter</artifactId>\n    <packaging>jar</packaging>\n    <description>loveqq-framework integrate sa-token</description>\n\n    <properties>\n        <jdk.version>1.8</jdk.version>\n    </properties>\n\n    <dependencies>\n        <!-- loveqq-core -->\n        <dependency>\n            <groupId>com.kfyty</groupId>\n            <artifactId>loveqq-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- loveqq-mvc-core -->\n        <dependency>\n            <groupId>com.kfyty</groupId>\n            <artifactId>loveqq-mvc-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- loveqq redisson starter，默认提供，不配置链接信息不生效 -->\n        <dependency>\n            <groupId>com.kfyty</groupId>\n            <artifactId>loveqq-boot-starter-redisson</artifactId>\n        </dependency>\n\n        <!-- jackson 序列化、redis集成、SSO、OAuth2 等模块要用到，比较重要所以内置集成 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-jackson</artifactId>\n            <exclusions>\n                <!-- loveqq-core 已有，这里排除以统一版本 -->\n                <exclusion>\n                    <groupId>com.fasterxml.jackson.core</groupId>\n                    <artifactId>jackson-databind</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>com.fasterxml.jackson.datatype</groupId>\n                    <artifactId>jackson-datatype-jsr310</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <!-- redisson，默认提供，不配置不生效 -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-redisson</artifactId>\n            <exclusions>\n                <!-- 使用 loveqq redisson starter 提供的 -->\n                <exclusion>\n                    <groupId>org.redisson</groupId>\n                    <artifactId>redisson</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <!-- Servlet API -->\n        <dependency>\n            <groupId>jakarta.servlet</groupId>\n            <artifactId>jakarta.servlet-api</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- SSO (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- OAuth2.0 (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-oauth2</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- API Key (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-apikey</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- API Sign (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sign</artifactId>\n            <optional>true</optional>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.context.SaTokenContext;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.http.SaHttpTemplate;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate;\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil;\nimport cn.dev33.satoken.json.SaJsonTemplate;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.listener.SaTokenListener;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.loveqq.boot.support.SaPathMatcherHolder;\nimport cn.dev33.satoken.plugin.SaTokenPlugin;\nimport cn.dev33.satoken.plugin.SaTokenPluginHolder;\nimport cn.dev33.satoken.same.SaSameTemplate;\nimport cn.dev33.satoken.secure.totp.SaTotpTemplate;\nimport cn.dev33.satoken.serializer.SaSerializerTemplate;\nimport cn.dev33.satoken.stp.StpInterface;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook;\nimport cn.dev33.satoken.temp.SaTempTemplate;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.support.PatternMatcher;\n\nimport java.util.List;\n\n/**\n * 注入 Sa-Token 所需要的 Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@Component\npublic class SaBeanInject {\n\n    /**\n     * 组件注入\n     * <p> 为确保 Log 组件正常打印，必须将 SaLog 和 SaTokenConfig 率先初始化 </p>\n     *\n     * @param log           log 对象\n     * @param saTokenConfig 配置对象\n     */\n    public SaBeanInject(@Autowired(required = false) SaLog log,\n                        @Autowired(required = false) SaTokenConfig saTokenConfig,\n                        @Autowired(required = false) SaTokenPluginHolder pluginHolder) {\n        if (log != null) {\n            SaManager.setLog(log);\n        }\n\n        if (saTokenConfig != null) {\n            SaManager.setConfig(saTokenConfig);\n        }\n\n        // 初始化 Sa-Token SPI 插件\n        if (pluginHolder == null) {\n            pluginHolder = SaTokenPluginHolder.instance;\n        }\n\n        pluginHolder.init();\n\n        SaTokenPluginHolder.instance = pluginHolder;\n    }\n\n    /**\n     * 注入持久化Bean\n     *\n     * @param saTokenDao SaTokenDao对象\n     */\n    @Autowired(required = false)\n    public void setSaTokenDao(SaTokenDao saTokenDao) {\n        SaManager.setSaTokenDao(saTokenDao);\n    }\n\n    /**\n     * 注入权限认证Bean\n     *\n     * @param stpInterface StpInterface对象\n     */\n    @Autowired(required = false)\n    public void setStpInterface(StpInterface stpInterface) {\n        SaManager.setStpInterface(stpInterface);\n    }\n\n    /**\n     * 注入上下文Bean\n     *\n     * @param saTokenContext SaTokenContext对象\n     */\n    @Autowired(required = false)\n    public void setSaTokenContext(SaTokenContext saTokenContext) {\n        SaManager.setSaTokenContext(saTokenContext);\n    }\n\n    /**\n     * 注入侦听器Bean\n     *\n     * @param listenerList 侦听器集合\n     */\n    @Autowired(required = false)\n    public void setSaTokenListener(List<SaTokenListener> listenerList) {\n        SaTokenEventCenter.registerListenerList(listenerList);\n    }\n\n    /**\n     * 注入自定义注解处理器\n     *\n     * @param handlerList 自定义注解处理器集合\n     */\n    @Autowired(required = false)\n    public void setSaAnnotationHandler(List<SaAnnotationHandlerInterface<?>> handlerList) {\n        for (SaAnnotationHandlerInterface<?> handler : handlerList) {\n            SaAnnotationStrategy.instance.registerAnnotationHandler(handler);\n        }\n    }\n\n    /**\n     * 注入临时令牌验证模块 Bean\n     *\n     * @param saTempTemplate /\n     */\n    @Autowired(required = false)\n    public void setSaTempTemplate(SaTempTemplate saTempTemplate) {\n        SaManager.setSaTempTemplate(saTempTemplate);\n    }\n\n    /**\n     * 注入 Same-Token 模块 Bean\n     *\n     * @param saSameTemplate saSameTemplate对象\n     */\n    @Autowired(required = false)\n    public void setSaIdTemplate(SaSameTemplate saSameTemplate) {\n        SaManager.setSaSameTemplate(saSameTemplate);\n    }\n\n    /**\n     * 注入 Sa-Token Http Basic 认证模块\n     *\n     * @param saBasicTemplate saBasicTemplate对象\n     */\n    @Autowired(required = false)\n    public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) {\n        SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate;\n    }\n\n    /**\n     * 注入 Sa-Token Http Digest 认证模块\n     *\n     * @param saHttpDigestTemplate saHttpDigestTemplate 对象\n     */\n    @Autowired(required = false)\n    public void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) {\n        SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate;\n    }\n\n    /**\n     * 注入自定义的 JSON 转换器 Bean\n     *\n     * @param saJsonTemplate JSON 转换器\n     */\n    @Autowired(required = false)\n    public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) {\n        SaManager.setSaJsonTemplate(saJsonTemplate);\n    }\n\n    /**\n     * 注入自定义的 Http 转换器 Bean\n     *\n     * @param saHttpTemplate /\n     */\n    @Autowired(required = false)\n    public void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) {\n        SaManager.setSaHttpTemplate(saHttpTemplate);\n    }\n\n    /**\n     * 注入自定义的序列化器 Bean\n     *\n     * @param saSerializerTemplate 序列化器\n     */\n    @Autowired(required = false)\n    public void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) {\n        SaManager.setSaSerializerTemplate(saSerializerTemplate);\n    }\n\n    /**\n     * 注入自定义的 TOTP 算法 Bean\n     *\n     * @param totpTemplate TOTP 算法类\n     */\n    @Autowired(required = false)\n    public void setSaTotpTemplate(SaTotpTemplate totpTemplate) {\n        SaManager.setSaTotpTemplate(totpTemplate);\n    }\n\n    /**\n     * 注入自定义的 StpLogic\n     *\n     * @param stpLogic /\n     */\n    @Autowired(required = false)\n    public void setStpLogic(StpLogic stpLogic) {\n        StpUtil.setStpLogic(stpLogic);\n    }\n\n    /**\n     * 利用自动注入特性，获取Spring框架内部使用的路由匹配器\n     *\n     * @param pathMatcher 要设置的 pathMatcher\n     */\n    @Autowired(required = false)\n    public void setPathMatcher(PatternMatcher pathMatcher) {\n        SaPathMatcherHolder.setPathMatcher(pathMatcher);\n    }\n\n    /**\n     * 注入自定义防火墙校验 hook 集合\n     *\n     * @param hooks /\n     */\n    @Autowired(required = false)\n    public void setSaFirewallCheckHooks(List<SaFirewallCheckHook> hooks) {\n        for (SaFirewallCheckHook hook : hooks) {\n            SaFirewallStrategy.instance.registerHook(hook);\n        }\n    }\n\n    /**\n     * 注入CORS 策略处理函数\n     *\n     * @param corsHandle /\n     */\n    @Autowired(required = false)\n    public void setCorsHandle(SaCorsHandleFunction corsHandle) {\n        SaStrategy.instance.corsHandle = corsHandle;\n    }\n\n    /**\n     * 注入自定义插件集合\n     *\n     * @param plugins /\n     */\n    @Autowired(required = false)\n    public void setSaTokenPluginList(List<SaTokenPlugin> plugins) {\n        for (SaTokenPlugin plugin : plugins) {\n            SaTokenPluginHolder.instance.installPlugin(plugin);\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/SaBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot;\n\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoForRedisson;\nimport cn.dev33.satoken.loveqq.boot.context.path.ApplicationContextPathLoading;\nimport cn.dev33.satoken.loveqq.boot.filter.SaFirewallCheckFilter;\nimport cn.dev33.satoken.loveqq.boot.filter.SaTokenContextFilter;\nimport cn.dev33.satoken.loveqq.boot.filter.SaTokenCorsFilter;\nimport cn.dev33.satoken.loveqq.boot.support.SaPathMatcherHolder;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Import;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnBean;\nimport org.redisson.api.RedissonClient;\n\n/**\n * 注册Sa-Token所需要的Bean\n * <p> Bean 的注册与注入应该分开在两个文件中，否则在某些场景下会造成循环依赖\n *\n * @author click33\n */\n@Component\n@Import(config = {\n        SaFirewallCheckFilter.class,\n        SaTokenContextFilter.class,\n        SaTokenCorsFilter.class\n})\npublic class SaBeanRegister {\n\n    public SaBeanRegister() {\n        // 重写路由匹配算法\n        SaStrategy.instance.routeMatcher = SaPathMatcherHolder::match;\n    }\n\n    /**\n     * 获取配置Bean\n     *\n     * @return 配置对象\n     */\n    @Bean\n    @ConfigurationProperties(\"sa-token\")\n    public SaTokenConfig getSaTokenConfig() {\n        return new SaTokenConfig();\n    }\n\n    /**\n     * redis dao 集成\n     *\n     * @return {@link SaTokenDao}\n     */\n    @Bean\n    @ConditionalOnBean(RedissonClient.class)\n    public SaTokenDao saTokenDao(RedissonClient redisson) {\n        return new SaTokenDaoForRedisson(redisson);\n    }\n\n    /**\n     * 应用上下文路径加载器\n     *\n     * @return /\n     */\n    @Bean\n    public ApplicationContextPathLoading getApplicationContextPathLoading() {\n        return new ApplicationContextPathLoading();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.apiKey;\n\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.config.SaApiKeyConfig;\nimport cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader;\nimport cn.dev33.satoken.apikey.template.SaApiKeyTemplate;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\n\n/**\n * 注入 Sa-Token API Key 所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.apikey.SaApiKeyManager\")\npublic class SaApiKeyBeanInject {\n    /**\n     * 注入 API Key 配置对象\n     *\n     * @param saApiKeyConfig 配置对象\n     */\n    @Autowired(required = false)\n    public void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) {\n        SaApiKeyManager.setConfig(saApiKeyConfig);\n    }\n\n    /**\n     * 注入自定义的 API Key 模版方法 Bean\n     *\n     * @param apiKeyTemplate /\n     */\n    @Autowired(required = false)\n    public void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) {\n        SaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate);\n    }\n\n    /**\n     * 注入自定义的 API Key 数据加载器 Bean\n     *\n     * @param apiKeyDataLoader /\n     */\n    @Autowired(required = false)\n    public void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) {\n        SaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/apiKey/SaApiKeyBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.apiKey;\n\nimport cn.dev33.satoken.apikey.config.SaApiKeyConfig;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\n\n/**\n * 注册 Sa-Token API Key 所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.apikey.SaApiKeyManager\")\npublic class SaApiKeyBeanRegister {\n    /**\n     * 获取 API Key 配置对象\n     *\n     * @return 配置对象\n     */\n    @Bean\n    @ConfigurationProperties(\"sa-token.api-key\")\n    public SaApiKeyConfig getSaApiKeyConfig() {\n        return new SaApiKeyConfig();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/SaReactorHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.context;\n\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\nimport reactor.core.publisher.Mono;\n\n/**\n * Reactor 上下文操作（异步），持有当前请求的 ServerWebExchange 全局引用\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaReactorHolder {\n    public static final String REQUEST_CONTEXT_ATTRIBUTE = \"com.kfyty.loveqq.framework.web.mvc.reactor.request.support.RequestContextHolder.REQUEST_CONTEXT_ATTRIBUTE\";\n    public static final String RESPONSE_CONTEXT_ATTRIBUTE = \"com.kfyty.loveqq.framework.web.mvc.reactor.request.support.ResponseContextHolder.REQUEST_CONTEXT_ATTRIBUTE\";\n\n    /**\n     * 获取 Mono < ServerRequest >\n     *\n     * @return /\n     */\n    public static Mono<ServerRequest> getRequest() {\n        return Mono.deferContextual(Mono::just).map(e -> e.get(REQUEST_CONTEXT_ATTRIBUTE));\n    }\n\n    /**\n     * 获取 Mono < ServerResponse >\n     *\n     * @return /\n     */\n    public static Mono<ServerResponse> getResponse() {\n        return Mono.deferContextual(Mono::just).map(e -> e.get(RESPONSE_CONTEXT_ATTRIBUTE));\n    }\n\n    /**\n     * 将 ServerRequest/ServerResponse 写入到同步上下文中，并执行一段代码，执行完毕清除上下文\n     *\n     * @return /\n     */\n    public static <R> Mono<R> sync(SaRetGenericFunction<R> fun) {\n        return Mono.deferContextual(ctx -> {\n            SaTokenContextModelBox prev = SaTokenContextUtil.setContext(ctx.get(REQUEST_CONTEXT_ATTRIBUTE), ctx.get(RESPONSE_CONTEXT_ATTRIBUTE));\n            try {\n                return Mono.just(fun.run());\n            } finally {\n\t\t\t\tSaTokenContextUtil.clearContext(prev);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/context/path/ApplicationContextPathLoading.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.context.path;\n\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.kfyty.loveqq.framework.core.autoconfig.CommandLineRunner;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Value;\n\n/**\n * 应用上下文路径加载器\n *\n * @author click33\n * @since 1.37.0\n */\npublic class ApplicationContextPathLoading implements CommandLineRunner {\n    @Value(\"${k.mvc.tomcat.contextPath:}\")\n    private String contextPath;\n\n    @Override\n    public void run(String... args) throws Exception {\n\n        String routePrefix = \"\";\n\n        if (SaFoxUtil.isNotEmpty(contextPath)) {\n            if (!contextPath.startsWith(\"/\")) {\n                contextPath = \"/\" + contextPath;\n            }\n            if (contextPath.endsWith(\"/\")) {\n                contextPath = contextPath.substring(0, contextPath.length() - 1);\n            }\n            routePrefix += contextPath;\n        }\n\n        if (SaFoxUtil.isNotEmpty(routePrefix) && !routePrefix.equals(\"/\")) {\n            ApplicationInfo.routePrefix = routePrefix;\n        }\n    }\n}"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaFirewallCheckFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.filter;\n\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.FirewallCheckException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.loveqq.boot.model.LoveqqSaRequest;\nimport cn.dev33.satoken.loveqq.boot.model.LoveqqSaResponse;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Order;\nimport com.kfyty.loveqq.framework.web.core.filter.Filter;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\n/**\n * 防火墙校验过滤器 (基于 loveqq-framework 统一 Filter，可以统一 servlet 和 reactor 配置)\n *\n * @author click33\n * @since 1.37.0\n */\n@Component\n@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER)\npublic class SaFirewallCheckFilter implements Filter {\n\n    @Override\n    public Continue doFilter(ServerRequest request, ServerResponse response) {\n        LoveqqSaRequest saRequest = new LoveqqSaRequest(request);\n        LoveqqSaResponse saResponse = new LoveqqSaResponse(response);\n        SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response);\n        try {\n            SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null);\n        } catch (StopMatchException ignored) {\n            // ignored\n        } catch (BackResultException e) {\n            SaTokenOperateUtil.writeResult(response, e.getMessage());\n            return Continue.FALSE;\n        } catch (FirewallCheckException e) {\n            if (SaFirewallStrategy.instance.checkFailHandle == null) {\n                SaTokenOperateUtil.writeResult(response, e.getMessage());\n            } else {\n                SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null);\n            }\n            return Continue.FALSE;\n        } finally {\n            SaTokenContextUtil.clearContext(prev);\n        }\n\n        // 更多异常则不处理，交由 Web 框架处理\n\n        // 向内执行\n        return Continue.TRUE;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaRequestFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.filter;\n\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.filter.SaFilter;\nimport cn.dev33.satoken.filter.SaFilterAuthStrategy;\nimport cn.dev33.satoken.filter.SaFilterErrorStrategy;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Order;\nimport com.kfyty.loveqq.framework.web.core.filter.Filter;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 全局鉴权过滤器 (基于 loveqq-framework 统一 Filter，可以统一 servlet 和 reactor 配置)\n * <p>\n * 默认优先级为 -100，尽量保证在其它过滤器之前执行\n * </p>\n *\n * @author click33\n * @since 1.19.0\n */\n@Order(SaTokenConsts.ASSEMBLY_ORDER)\npublic class SaRequestFilter implements SaFilter, Filter {\n\n    // ------------------------ 设置此过滤器 拦截 & 放行 的路由\n\n    /**\n     * 拦截路由\n     */\n    public List<String> includeList = new ArrayList<>();\n\n    /**\n     * 放行路由\n     */\n    public List<String> excludeList = new ArrayList<>();\n\n    @Override\n    public SaRequestFilter addInclude(String... paths) {\n        includeList.addAll(Arrays.asList(paths));\n        return this;\n    }\n\n    @Override\n    public SaRequestFilter addExclude(String... paths) {\n        excludeList.addAll(Arrays.asList(paths));\n        return this;\n    }\n\n    @Override\n    public SaRequestFilter setIncludeList(List<String> pathList) {\n        includeList = pathList;\n        return this;\n    }\n\n    @Override\n    public SaRequestFilter setExcludeList(List<String> pathList) {\n        excludeList = pathList;\n        return this;\n    }\n\n\n    // ------------------------ 钩子函数\n\n    /**\n     * 认证函数：每次请求执行\n     */\n    public SaFilterAuthStrategy auth = r -> {\n    };\n\n    /**\n     * 异常处理函数：每次[认证函数]发生异常时执行此函数\n     */\n    public SaFilterErrorStrategy error = e -> {\n        throw new SaTokenException(e);\n    };\n\n    /**\n     * 前置函数：在每次[认证函数]之前执行\n     * <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n     */\n    public SaFilterAuthStrategy beforeAuth = r -> {\n    };\n\n    @Override\n    public SaRequestFilter setAuth(SaFilterAuthStrategy auth) {\n        this.auth = auth;\n        return this;\n    }\n\n    @Override\n    public SaRequestFilter setError(SaFilterErrorStrategy error) {\n        this.error = error;\n        return this;\n    }\n\n    @Override\n    public SaRequestFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n        this.beforeAuth = beforeAuth;\n        return this;\n    }\n\n\n    // ------------------------ doFilter\n\n    @Override\n    public Continue doFilter(ServerRequest request, ServerResponse response) {\n        SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response);\n        try {\n            beforeAuth.run(null);\n            SaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null));\n        } catch (StopMatchException ignored) {\n            // ignored\n        } catch (BackResultException e) {\n            SaTokenOperateUtil.writeResult(response, e.getMessage());\n            return Continue.FALSE;\n        } catch (Throwable e) {\n            SaTokenOperateUtil.writeResult(response, String.valueOf(error.run(e)));\n            return Continue.FALSE;\n        } finally {\n            SaTokenContextUtil.clearContext(prev);\n        }\n\n        // 执行\n        return Continue.TRUE;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenContextFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.filter;\n\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Order;\nimport com.kfyty.loveqq.framework.web.core.filter.Filter;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\n/**\n * SaTokenContext 上下文初始化过滤器 (基于 loveqq-framework 统一 Filter，可以统一 servlet 和 reactor 配置)\n *\n * @author click33\n * @since 1.42.0\n */\n@Component\n@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)\npublic class SaTokenContextFilter implements Filter {\n\n\t@Override\n\tpublic Continue doFilter(ServerRequest request, ServerResponse response) {\n\t\tSaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response);\n\t\treturn Continue.ofTrue(() -> SaTokenContextUtil.clearContext(prev));\n\t}\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/filter/SaTokenCorsFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.filter;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Order;\nimport com.kfyty.loveqq.framework.web.core.filter.Filter;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\n/**\n * CORS 跨域策略过滤器 (基于 loveqq-framework 统一 Filter，可以统一 servlet 和 reactor 配置)\n * loveqq-framework 也有跨域过滤器，切勿同时配置\n *\n * @author click33\n * @see com.kfyty.loveqq.framework.web.core.cors.CorsFilter\n * @since 1.42.0\n */\n@Component\n@Order(SaTokenConsts.CORS_FILTER_ORDER)\npublic class SaTokenCorsFilter implements Filter {\n\n    @Override\n    public Continue doFilter(ServerRequest request, ServerResponse response) {\n        SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response);\n        try {\n            SaTokenContextModelBox box = SaHolder.getContext().getModelBox();\n            SaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage());\n        } catch (StopMatchException ignored) {\n            // ignored\n        } catch (BackResultException e) {\n            SaTokenOperateUtil.writeResult(response, e.getMessage());\n            return Continue.FALSE;\n        } finally {\n            SaTokenContextUtil.clearContext(prev);\n        }\n        return Continue.TRUE;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/interceptor/SaInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.interceptor;\n\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenContextUtil;\nimport cn.dev33.satoken.loveqq.boot.utils.SaTokenOperateUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\nimport com.kfyty.loveqq.framework.web.core.interceptor.HandlerInterceptor;\nimport com.kfyty.loveqq.framework.web.core.route.HandlerMethodRoute;\nimport com.kfyty.loveqq.framework.web.core.route.Route;\n\nimport java.lang.reflect.Method;\n\n/**\n * Sa-Token 综合拦截器，提供注解鉴权和路由拦截鉴权能力\n *\n * @author click33\n * @since 1.31.0\n */\npublic class SaInterceptor implements HandlerInterceptor {\n    /**\n     * 是否打开注解鉴权，配置为 true 时注解鉴权才会生效，配置为 false 时，即使写了注解也不会进行鉴权\n     */\n    public boolean isAnnotation = true;\n\n    /**\n     * 认证前置函数：在注解鉴权之前执行\n     * <p> 参数：路由处理函数指针\n     */\n    public SaParamFunction<Object> beforeAuth = handler -> {\n    };\n\n    /**\n     * 认证函数：每次请求执行\n     * <p> 参数：路由处理函数指针\n     */\n    public SaParamFunction<Object> auth = handler -> {\n    };\n\n    /**\n     * 创建一个 Sa-Token 综合拦截器，默认带有注解鉴权能力\n     */\n    public SaInterceptor() {\n    }\n\n    /**\n     * 创建一个 Sa-Token 综合拦截器，默认带有注解鉴权能力\n     *\n     * @param auth 认证函数，每次请求执行\n     */\n    public SaInterceptor(SaParamFunction<Object> auth) {\n        this.auth = auth;\n    }\n\n    /**\n     * 设置是否打开注解鉴权：配置为 true 时注解鉴权才会生效，配置为 false 时，即使写了注解也不会进行鉴权\n     *\n     * @param isAnnotation /\n     * @return 对象自身\n     */\n    public SaInterceptor isAnnotation(boolean isAnnotation) {\n        this.isAnnotation = isAnnotation;\n        return this;\n    }\n\n    /**\n     * 写入 [ 认证前置函数 ]: 在注解鉴权之前执行\n     *\n     * @param beforeAuth /\n     * @return 对象自身\n     */\n    public SaInterceptor setBeforeAuth(SaParamFunction<Object> beforeAuth) {\n        this.beforeAuth = beforeAuth;\n        return this;\n    }\n\n    /**\n     * 写入 [ 认证函数 ]: 每次请求执行\n     *\n     * @param auth /\n     * @return 对象自身\n     */\n    public SaInterceptor setAuth(SaParamFunction<Object> auth) {\n        this.auth = auth;\n        return this;\n    }\n\n\n    // ----------------- 验证方法 -----------------\n\n    /**\n     * 每次请求之前触发的方法\n     */\n    @Override\n    public boolean preHandle(ServerRequest request, ServerResponse response, Route handler) {\n        SaTokenContextModelBox prev = SaTokenContextUtil.setContext(request, response);\n        try {\n            // 前置函数：在注解鉴权之前执行\n            beforeAuth.run(handler);\n\n            // 这里必须确保 handler 是 HandlerMethod 类型时，才能进行注解鉴权\n            if (isAnnotation && handler instanceof HandlerMethodRoute) {\n                Method method = ((HandlerMethodRoute) handler).getMappedMethod();\n                SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method);\n            }\n\n            // Auth 路由拦截鉴权校验\n            auth.run(handler);\n        } catch (StopMatchException e) {\n            // StopMatchException 异常代表：停止匹配，进入Controller\n        } catch (BackResultException e) {\n            SaTokenOperateUtil.writeResult(response, e.getMessage());\n            return false;\n        } finally {\n            SaTokenContextUtil.clearContext(prev);\n        }\n\n        // 通过验证\n        return true;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaRequest.java",
    "content": "package cn.dev33.satoken.loveqq.boot.model;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\nimport java.net.HttpCookie;\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * 对 SaRequest 包装类的实现\n *\n * @author kfyty725\n */\npublic class LoveqqSaRequest implements SaRequest {\n    /**\n     * loveqq-framework 包装请求\n     */\n    private final ServerRequest request;\n\n    public LoveqqSaRequest(ServerRequest request) {\n        this.request = request;\n    }\n\n    @Override\n    public Object getSource() {\n        return request;\n    }\n\n    @Override\n    public String getParam(String name) {\n        return request.getParameter(name);\n    }\n\n    @Override\n    public Collection<String> getParamNames() {\n        return request.getParameterNames();\n    }\n\n    @Override\n    public Map<String, String> getParamMap() {\n        return request.getParameterMap();\n    }\n\n    @Override\n    public String getHeader(String name) {\n        return request.getHeader(name);\n    }\n\n    @Override\n    public String getCookieValue(String name) {\n        HttpCookie cookie = request.getCookie(name);\n        return cookie == null ? null : cookie.getValue();\n    }\n\n    @Override\n    public String getCookieFirstValue(String name) {\n        HttpCookie[] cookies = request.getCookies();\n        if (cookies != null) {\n            for (HttpCookie cookie : cookies) {\n                if (cookie != null && name.equals(cookie.getName())) {\n                    return cookie.getValue();\n                }\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public String getCookieLastValue(String name) {\n        String value = null;\n        HttpCookie[] cookies = request.getCookies();\n        if (cookies != null) {\n            for (HttpCookie cookie : cookies) {\n                if (cookie != null && name.equals(cookie.getName())) {\n                    value = cookie.getValue();\n                }\n            }\n        }\n        return value;\n    }\n\n    @Override\n    public String getRequestPath() {\n        return ApplicationInfo.cutPathPrefix(request.getRequestURI());\n    }\n\n    @Override\n    public String getUrl() {\n        String currDomain = SaManager.getConfig().getCurrDomain();\n        if (!SaFoxUtil.isEmpty(currDomain)) {\n            return currDomain + this.getRequestPath();\n        }\n        return request.getRequestURL();\n    }\n\n    @Override\n    public String getMethod() {\n        return request.getMethod();\n    }\n\n    @Override\n    public String getHost() {\n        return request.getHost();\n    }\n\n    @Override\n    public Object forward(String path) {\n        ServerResponse response = (ServerResponse) SaManager.getSaTokenContext().getResponse().getSource();\n        return response.sendRedirect(path);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaResponse.java",
    "content": "package cn.dev33.satoken.loveqq.boot.model;\n\nimport cn.dev33.satoken.context.model.SaResponse;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\n/**\n * 对 SaResponse 包装类的实现\n *\n * @author kfyty725\n */\npublic class LoveqqSaResponse implements SaResponse {\n    /**\n     * loveqq-framework 包装响应\n     */\n    private final ServerResponse response;\n\n    public LoveqqSaResponse(ServerResponse response) {\n        this.response = response;\n    }\n\n    @Override\n    public Object getSource() {\n        return response;\n    }\n\n    @Override\n    public SaResponse setStatus(int sc) {\n        response.setStatus(sc);\n        return this;\n    }\n\n    @Override\n    public SaResponse setHeader(String name, String value) {\n        response.setHeader(name, value);\n        return this;\n    }\n\n    @Override\n    public SaResponse addHeader(String name, String value) {\n        response.addHeader(name, value);\n        return this;\n    }\n\n    @Override\n    public Object redirect(String url) {\n        return response.sendRedirect(url);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/model/LoveqqSaStorage.java",
    "content": "package cn.dev33.satoken.loveqq.boot.model;\n\nimport cn.dev33.satoken.context.model.SaStorage;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\n\n/**\n * 对 SaStorage 包装类的实现\n *\n * @author kfyty725\n */\npublic class LoveqqSaStorage implements SaStorage {\n    /**\n     * loveqq-framework 包装请求\n     */\n    private final ServerRequest request;\n\n    public LoveqqSaStorage(ServerRequest request) {\n        this.request = request;\n    }\n\n    @Override\n    public Object getSource() {\n        return request;\n    }\n\n    @Override\n    public Object get(String key) {\n        return request.getAttribute(key);\n    }\n\n    @Override\n    public SaStorage set(String key, Object value) {\n        request.setAttribute(key, value);\n        return this;\n    }\n\n    @Override\n    public SaStorage delete(String key) {\n        request.removeAttribute(key);\n        return this;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.oauth2;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.dao.SaOAuth2Dao;\nimport cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter;\nimport cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;\nimport cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;\nimport cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver;\nimport cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface;\nimport cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;\nimport cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Template;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\n\nimport java.util.List;\n\n\n// 小提示：如果你在 idea 中运行源码时出现异常：java: 程序包cn.dev33.satoken.oauth2不存在。\n// 在项目根目录进入 cmd，执行 mvn package 即可解决\n\n\n/**\n * 注入 Sa-Token-OAuth2 所需要的组件\n *\n * @author click33\n * @since 1.34.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.oauth2.SaOAuth2Manager\")\npublic class SaOAuth2BeanInject {\n    /**\n     * 注入 OAuth2 配置对象\n     *\n     * @param saOAuth2Config 配置对象\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) {\n        SaOAuth2Manager.setServerConfig(saOAuth2Config);\n    }\n\n    /**\n     * 注入 OAuth2 模板代码类\n     *\n     * @param saOAuth2Template 模板代码类\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) {\n        SaOAuth2Manager.setTemplate(saOAuth2Template);\n    }\n\n    /**\n     * 注入 OAuth2 请求处理器\n     *\n     * @param serverProcessor 请求处理器\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) {\n        SaOAuth2ServerProcessor.instance = serverProcessor;\n    }\n\n    /**\n     * 注入 OAuth2 数据加载器\n     *\n     * @param dataLoader /\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) {\n        SaOAuth2Manager.setDataLoader(dataLoader);\n    }\n\n    /**\n     * 注入 OAuth2 数据解析器 Bean\n     *\n     * @param dataResolver /\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) {\n        SaOAuth2Manager.setDataResolver(dataResolver);\n    }\n\n    /**\n     * 注入 OAuth2 数据格式转换器 Bean\n     *\n     * @param dataConverter /\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) {\n        SaOAuth2Manager.setDataConverter(dataConverter);\n    }\n\n    /**\n     * 注入 OAuth2 数据构建器 Bean\n     *\n     * @param dataGenerate /\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) {\n        SaOAuth2Manager.setDataGenerate(dataGenerate);\n    }\n\n    /**\n     * 注入 OAuth2 数据持久 Bean\n     *\n     * @param dao /\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2Dao(SaOAuth2Dao dao) {\n        SaOAuth2Manager.setDao(dao);\n    }\n\n    /**\n     * 注入自定义 scope 处理器\n     *\n     * @param handlerList 自定义 scope 处理器集合\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2ScopeHandler(List<SaOAuth2ScopeHandlerInterface> handlerList) {\n        for (SaOAuth2ScopeHandlerInterface handler : handlerList) {\n            SaOAuth2Strategy.instance.registerScopeHandler(handler);\n        }\n    }\n\n    /**\n     * 注入自定义 grant_type 处理器\n     *\n     * @param handlerList 自定义 grant_type 处理器集合\n     */\n    @Autowired(required = false)\n    public void setSaOAuth2GrantTypeHandlerInterface(List<SaOAuth2GrantTypeHandlerInterface> handlerList) {\n        for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) {\n            SaOAuth2Strategy.instance.registerGrantTypeHandler(handler);\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/oauth2/SaOAuth2BeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.oauth2;\n\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\n\n/**\n * 注册 Sa-Token-OAuth2 所需要的Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.oauth2.SaOAuth2Manager\")\npublic class SaOAuth2BeanRegister {\n    /**\n     * 获取 OAuth2 配置 Bean\n     *\n     * @return 配置对象\n     */\n    @Bean\n    @ConfigurationProperties(\"sa-token.oauth2-server\")\n    public SaOAuth2ServerConfig getSaOAuth2Config() {\n        return new SaOAuth2ServerConfig();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token 集成 loveqq-framework 的各个组件\n */\npackage cn.dev33.satoken.loveqq.boot;"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.sign;\n\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.config.SaSignManyConfigWrapper;\nimport cn.dev33.satoken.sign.template.SaSignTemplate;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\n\n/**\n * 注入 Sa-Token API 参数签名 所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.sign.SaSignManager\")\npublic class SaSignBeanInject {\n    /**\n     * 注入 API 参数签名配置对象\n     *\n     * @param saSignConfig 配置对象\n     */\n    @Autowired(required = false)\n    public void setSignConfig(SaSignConfig saSignConfig) {\n        SaSignManager.setConfig(saSignConfig);\n    }\n\n    /**\n     * 注入 API 参数签名配置对象\n     *\n     * @param signManyConfigWrapper 配置对象\n     */\n    @Autowired(required = false)\n    public void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) {\n        SaSignManager.setSignMany(signManyConfigWrapper.getSignMany());\n    }\n\n    /**\n     * 注入自定义的 参数签名 模版方法 Bean\n     *\n     * @param saSignTemplate 参数签名 Bean\n     */\n    @Autowired(required = false)\n    public void setSaSignTemplate(SaSignTemplate saSignTemplate) {\n        SaSignManager.setSaSignTemplate(saSignTemplate);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sign/SaSignBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.sign;\n\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.config.SaSignManyConfigWrapper;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\n\n/**\n * 注册 Sa-Token API 参数签名所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.sign.SaSignManager\")\npublic class SaSignBeanRegister {\n    /**\n     * 获取 API 参数签名配置对象\n     *\n     * @return 配置对象\n     */\n    @Bean\n    @ConfigurationProperties(\"sa-token.sign\")\n    public SaSignConfig getSaSignConfig() {\n        return new SaSignConfig();\n    }\n\n    /**\n     * 获取 API 参数签名 Many 配置对象\n     *\n     * @return 配置对象\n     */\n    @Bean\n    @ConfigurationProperties(\"sa-token\")\n    public SaSignManyConfigWrapper getSaSignManyConfigWrapper() {\n        return new SaSignManyConfigWrapper();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.sso;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Autowired;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\n\n/**\n * 注入 Sa-Token SSO 所需要的 Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.sso.SaSsoManager\")\npublic class SaSsoBeanInject {\n    /**\n     * 注入 Sa-Token SSO Server 端 配置类\n     *\n     * @param serverConfig 配置对象\n     */\n    @Autowired(required = false)\n    public void setSaSsoServerConfig(SaSsoServerConfig serverConfig) {\n        SaSsoManager.setServerConfig(serverConfig);\n    }\n\n    /**\n     * 注入 Sa-Token SSO Client 端 配置类\n     *\n     * @param clientConfig 配置对象\n     */\n    @Autowired(required = false)\n    public void setSaSsoClientConfig(SaSsoClientConfig clientConfig) {\n        SaSsoManager.setClientConfig(clientConfig);\n    }\n\n    /**\n     * 注入 SSO 模板代码类 (Server 端)\n     *\n     * @param ssoServerTemplate /\n     */\n    @Autowired(required = false)\n    public void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) {\n        SaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate;\n    }\n\n    /**\n     * 注入 SSO 模板代码类 (Client 端)\n     *\n     * @param ssoClientTemplate /\n     */\n    @Autowired(required = false)\n    public void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) {\n        SaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/sso/SaSsoBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.sso;\n\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Bean;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.Component;\nimport com.kfyty.loveqq.framework.core.autoconfig.annotation.ConfigurationProperties;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnClass;\nimport com.kfyty.loveqq.framework.core.autoconfig.condition.annotation.ConditionalOnMissingBean;\n\n/**\n * 注册 Sa-Token SSO 所需要的 Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@Component\n@ConditionalOnClass(\"cn.dev33.satoken.sso.SaSsoManager\")\npublic class SaSsoBeanRegister {\n    /**\n     * 获取 SSO Server 端 配置对象\n     *\n     * @return 配置对象\n     */\n    @Bean\n    @ConfigurationProperties(\"sa-token.sso-server\")\n    public SaSsoServerConfig getSaSsoServerConfig() {\n        return new SaSsoServerConfig();\n    }\n\n    /**\n     * 获取 SSO Client 端 配置对象\n     *\n     * @return 配置对象\n     */\n    @Bean\n    @ConfigurationProperties(\"sa-token.sso-client\")\n    public SaSsoClientConfig getSaSsoClientConfig() {\n        return new SaSsoClientConfig();\n    }\n\n    /**\n     * 获取 SSO Server 端 SaSsoServerTemplate\n     *\n     * @return /\n     */\n    @Bean\n    @ConditionalOnMissingBean(SaSsoServerTemplate.class)\n    public SaSsoServerTemplate getSaSsoServerTemplate() {\n        return SaSsoServerProcessor.instance.ssoServerTemplate;\n    }\n\n    /**\n     * 获取 SSO Client 端 SaSsoClientTemplate\n     *\n     * @return /\n     */\n    @Bean\n    @ConditionalOnMissingBean(SaSsoClientTemplate.class)\n    public SaSsoClientTemplate getSaSsoClientTemplate() {\n        return SaSsoClientProcessor.instance.ssoClientTemplate;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/support/SaPathMatcherHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.support;\n\nimport com.kfyty.loveqq.framework.core.support.AntPathMatcher;\nimport com.kfyty.loveqq.framework.core.support.PatternMatcher;\n\n/**\n * 路由匹配工具类：持有 PathMatcher 全局引用，方便快捷的调用 PathMatcher 相关方法\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaPathMatcherHolder {\n\n    private SaPathMatcherHolder() {\n    }\n\n    /**\n     * 路由匹配器\n     */\n    public static PatternMatcher pathMatcher;\n\n    /**\n     * 获取路由匹配器\n     *\n     * @return 路由匹配器\n     */\n    public static PatternMatcher getPathMatcher() {\n        if (pathMatcher == null) {\n            pathMatcher = new AntPathMatcher();\n        }\n        return pathMatcher;\n    }\n\n    /**\n     * 写入路由匹配器\n     *\n     * @param pathMatcher 路由匹配器\n     */\n    public static void setPathMatcher(PatternMatcher pathMatcher) {\n        SaPathMatcherHolder.pathMatcher = pathMatcher;\n    }\n\n    /**\n     * 判断：指定路由匹配符是否可以匹配成功指定路径\n     *\n     * @param pattern 路由匹配符\n     * @param path    要匹配的路径\n     * @return 是否匹配成功\n     */\n    public static boolean match(String pattern, String path) {\n        return getPathMatcher().matches(pattern, path);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenContextUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.utils;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\nimport cn.dev33.satoken.loveqq.boot.model.LoveqqSaRequest;\nimport cn.dev33.satoken.loveqq.boot.model.LoveqqSaResponse;\nimport cn.dev33.satoken.loveqq.boot.model.LoveqqSaStorage;\nimport com.kfyty.loveqq.framework.web.core.http.ServerRequest;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\n/**\n * SaTokenContext 上下文读写工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextUtil {\n    /**\n     * 写入当前上下文\n     * 并返回当前的上下文，以支持 loveqq-framework 的 servlet/reactor 的统一配置\n     *\n     * @param request  /\n     * @param response /\n     */\n    public static SaTokenContextModelBox setContext(ServerRequest request, ServerResponse response) {\n        SaTokenContextModelBox prev = SaTokenContextForThreadLocalStaff.getModelBoxOrNull();\n        SaRequest req = new LoveqqSaRequest(request);\n        SaResponse res = new LoveqqSaResponse(response);\n        SaStorage stg = new LoveqqSaStorage(request);\n        SaManager.getSaTokenContext().setContext(req, res, stg);\n        return prev;\n    }\n\n    /**\n     * 写入上下文对象, 并在执行函数后将其清除\n     *\n     * @param request  /\n     * @param response /\n     * @param fun      /\n     */\n    public static void setContext(ServerRequest request, ServerResponse response, SaFunction fun) {\n        SaTokenContextModelBox prev = setContext(request, response);\n        try {\n            fun.run();\n        } finally {\n            clearContext(prev);\n        }\n    }\n\n    /**\n     * 写入上下文对象, 并在执行函数后将其清除\n     *\n     * @param request  /\n     * @param response /\n     * @param fun      /\n     * @param <T>      /\n     * @return /\n     */\n    public static <T> T setContext(ServerRequest request, ServerResponse response, SaRetGenericFunction<T> fun) {\n        SaTokenContextModelBox prev = setContext(request, response);\n        try {\n            return fun.run();\n        } finally {\n            clearContext(prev);\n        }\n    }\n\n    /**\n     * 清除当前上下文\n     * 并恢复之前的上下文，以支持 loveqq-framework 的 servlet/reactor 的统一配置\n     */\n    public static void clearContext(SaTokenContextModelBox prev) {\n        if (prev == null) {\n            SaManager.getSaTokenContext().clearContext();\n        } else {\n            SaManager.getSaTokenContext().setContext(prev.getRequest(), prev.getResponse(), prev.getStorage());\n        }\n    }\n\n    /**\n     * 获取当前 ModelBox\n     *\n     * @return /\n     */\n    public static SaTokenContextModelBox getModelBox() {\n        return SaManager.getSaTokenContext().getModelBox();\n    }\n\n    /**\n     * 获取当前 Request\n     *\n     * @return /\n     */\n    public static ServerRequest getRequest() {\n        return (ServerRequest) getModelBox().getRequest().getSource();\n    }\n\n    /**\n     * 获取当前 Response\n     *\n     * @return /\n     */\n    public static ServerResponse getResponse() {\n        return (ServerResponse) getModelBox().getResponse().getSource();\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/java/cn/dev33/satoken/loveqq/boot/utils/SaTokenOperateUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.loveqq.boot.utils;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport com.kfyty.loveqq.framework.core.exception.ResolvableException;\nimport com.kfyty.loveqq.framework.web.core.http.ServerResponse;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * {@link ServerResponse} 操作工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenOperateUtil {\n    /**\n     * 写入结果到输出流\n     *\n     * @param response /\n     * @param result   /\n     */\n    public static void writeResult(ServerResponse response, String result) {\n        // 写入输出流\n        // 请注意此处默认 Content-Type 为 text/plain，如果需要返回 JSON 信息，需要在 return 前自行设置 Content-Type 为 application/json\n        // 例如：SaHolder.getResponse().setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n        if (response.getContentType() == null) {\n            response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);\n        }\n        try (OutputStream out = response.getOutputStream()) {\n            out.write(result.getBytes(StandardCharsets.UTF_8));\n            out.flush();\n        } catch (IOException e) {\n            throw new ResolvableException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-loveqq-boot-starter/src/main/resources/META-INF/k.factories",
    "content": "com.kfyty.loveqq.framework.core.autoconfig.annotation.EnableAutoConfiguration=\\\n    cn.dev33.satoken.loveqq.boot.SaBeanRegister,\\\n    cn.dev33.satoken.loveqq.boot.SaBeanInject,\\\n    cn.dev33.satoken.loveqq.boot.apiKey.SaApiKeyBeanRegister,\\\n    cn.dev33.satoken.loveqq.boot.apiKey.SaApiKeyBeanInject,\\\n    cn.dev33.satoken.loveqq.boot.oauth2.SaOAuth2BeanRegister,\\\n    cn.dev33.satoken.loveqq.boot.oauth2.SaOAuth2BeanInject,\\\n    cn.dev33.satoken.loveqq.boot.sign.SaSignBeanRegister,\\\n    cn.dev33.satoken.loveqq.boot.sign.SaSignBeanInject,\\\n    cn.dev33.satoken.loveqq.boot.sso.SaSsoBeanRegister,\\\n    cn.dev33.satoken.loveqq.boot.sso.SaSsoBeanInject\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-reactor-spring-boot-starter</name>\n    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>\n\t<description>springboot reactor integrate sa-token</description>\n\t\n\t<dependencies>\n\t\t<!-- spring-boot-starter (optional) -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter</artifactId>\n\t\t\t<optional>true</optional>\n        </dependency>\n\n\t\t<!-- sa-token-spring-boot-reactor-v2v3v4-common -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-reactor-v2v3v4-common</artifactId>\n        </dependency>\n\n\t\t<!-- sa-token-jackson: JSON serialization -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n\n\t<dependencyManagement>\n        <dependencies>\n\n\t\t\t<!-- sa-token springboot2 相关依赖版本定义 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot2-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Sa-Token 集成 Reactor 响应式编程 (SpringBoot 2.x)\n */\npackage cn.dev33.satoken.reactor;\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SpringBootVersionCompatibilityChecker.java",
    "content": "package cn.dev33.satoken.reactor.spring;\r\n\r\nimport cn.dev33.satoken.exception.SaTokenException;\r\nimport cn.dev33.satoken.util.SaFoxUtil;\r\nimport org.springframework.boot.SpringBootVersion;\r\n\r\n/**\r\n * SpringBoot 版本与 Sa-Token 版本兼容检查器，当开发者错误的在 SpringBoot3/4.x 项目中引入当前集成包时，将在控制台做出提醒并阻断项目启动\r\n *\r\n * @author Uncarbon\r\n * @since 1.38.0\r\n */\r\npublic class SpringBootVersionCompatibilityChecker {\r\n\r\n    public SpringBootVersionCompatibilityChecker() {\r\n        String version = SpringBootVersion.getVersion();\r\n        if (SaFoxUtil.isEmpty(version) || version.startsWith(\"1.\") || version.startsWith(\"2.\")) {\r\n            return;\r\n        }\r\n        String str = \"当前 SpringBoot 版本（\" + version + \"）与 Sa-Token 依赖不兼容，\" +\r\n                \"请将依赖 sa-token-reactor-spring-boot-starter 修改为：sa-token-reactor-spring-boot3/4-starter\";\r\n        System.err.println(str);\r\n        throw new SaTokenException(str);\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.reactor.spring.SpringBootVersionCompatibilityChecker\r\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncn.dev33.satoken.reactor.spring.SaTokenContextRegister"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot3-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-reactor-spring-boot3-starter</name>\n    <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>\n\t<description>springboot3 reactor integrate sa-token</description>\n\t\n\t<dependencies>\n\n\t\t<!-- sa-token-spring-boot-reactor-v2v3v4-common -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-reactor-v2v3v4-common</artifactId>\n        </dependency>\n\n\t\t<!-- sa-token-jackson: JSON serialization -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n\t<dependencyManagement>\n        <dependencies>\n\n\t\t\t<!-- sa-token springboot3 相关依赖版本定义 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot3-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.dev33.satoken.reactor;\n\n\n/**\n * 占位符\n *\n * @author click33\n * @since 1.45.0\n */\npublic class Placeholder {\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.reactor.spring.SaTokenContextRegister"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot4-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-reactor-spring-boot4-starter</name>\n    <artifactId>sa-token-reactor-spring-boot4-starter</artifactId>\n\t<description>springboot4 reactor integrate sa-token</description>\n\t\n\t<dependencies>\n\n\t\t<!-- sa-token-spring-boot-reactor-v2v3v4-common -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-reactor-v2v3v4-common</artifactId>\n        </dependency>\n\n\t\t<!-- sa-token-jackson3: JSON serialization for Spring Boot 4 (Jackson 3) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson3</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n\t<dependencyManagement>\n        <dependencies>\n\n\t\t\t<!-- sa-token springboot4 相关依赖版本定义 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot4-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n        </dependencies>\n    </dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/java/cn/dev33/satoken/reactor/Placeholder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.dev33.satoken.reactor;\n\n\n/**\n * 占位符\n *\n * @author click33\n * @since 1.45.0\n */\npublic class Placeholder {\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-reactor-spring-boot4-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.reactor.spring.SaTokenContextRegister\n"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-servlet</name>\n    <artifactId>sa-token-servlet</artifactId>\n\t<description>sa-token authentication by Servlet API</description>\n\n\t<dependencies>\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n        \n        <!-- Servlet API (optional) -->\n        <dependency>\n\t\t\t<groupId>javax.servlet</groupId>\n\t\t\t<artifactId>javax.servlet-api</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/error/SaServletErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.error;\n\n/**\n * 定义 sa-token-servlet 所有异常细分状态码 \n * \n * @author click33\n * @since 1.33.0\n */\npublic interface SaServletErrorCode {\n\t\n\t/** 转发失败 */\n\tint CODE_20001 = 20001;\n\n\t/** 重定向失败 */\n\tint CODE_20002 = 20002;\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.model;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.servlet.error.SaServletErrorCode;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.Cookie;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * 对 SaRequest 包装类的实现（Servlet 版）\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaRequestForServlet implements SaRequest {\n\n\t/**\n\t * 底层Request对象\n\t */\n\tprotected HttpServletRequest request;\n\n\t/**\n\t * 实例化\n\t * @param request request对象\n\t */\n\tpublic SaRequestForServlet(HttpServletRequest request) {\n\t\tthis.request = request;\n\t}\n\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn request;\n\t}\n\n\t/**\n\t * 在 [请求体] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getParam(String name) {\n\t\treturn request.getParameter(name);\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数名称\n\t * @return 参数名称列表\n\t */\n\t@Override\n\tpublic Collection<String> getParamNames(){\n\t\treturn Collections.list(request.getParameterNames());\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数\n\t * @return 参数列表\n\t */\n\t@Override\n\tpublic Map<String, String> getParamMap(){\n\t\t// 获取所有参数\n\t\tMap<String, String[]> parameterMap = request.getParameterMap();\n\t\tMap<String, String> map = new LinkedHashMap<>(parameterMap.size());\n\t\tfor (String key : parameterMap.keySet()) {\n\t\t\tString[] values = parameterMap.get(key);\n\t\t\tmap.put(key, values[0]);\n\t\t}\n\t\treturn map;\n\t}\n\n\t/**\n\t * 在 [请求头] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getHeader(String name) {\n\t\treturn request.getHeader(name);\n\t}\n\n\t/**\n\t * 在 [Cookie作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getCookieValue(String name) {\n\t\treturn getCookieLastValue(name);\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n\t */\n\t@Override\n\tpublic String getCookieFirstValue(String name){\n\t\tCookie[] cookies = request.getCookies();\n\t\tif (cookies != null) {\n\t\t\tfor (Cookie cookie : cookies) {\n\t\t\t\tif (cookie != null && name.equals(cookie.getName())) {\n\t\t\t\t\treturn cookie.getValue();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\t@Override\n\tpublic String getCookieLastValue(String name){\n\t\tString value = null;\n\t\tCookie[] cookies = request.getCookies();\n\t\tif (cookies != null) {\n\t\t\tfor (Cookie cookie : cookies) {\n\t\t\t\tif (cookie != null && name.equals(cookie.getName())) {\n\t\t\t\t\tvalue = cookie.getValue();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * 返回当前请求path (不包括上下文名称) \n\t */\n\t@Override\n\tpublic String getRequestPath() {\n\t\treturn ApplicationInfo.cutPathPrefix(request.getRequestURI());\n\t}\n\n\t/**\n\t * 返回当前请求的url，例：http://xxx.com/test\n\t * @return see note\n\t */\n\tpublic String getUrl() {\n\t\tString currDomain = SaManager.getConfig().getCurrDomain();\n\t\tif( ! SaFoxUtil.isEmpty(currDomain)) {\n\t\t\treturn currDomain + this.getRequestPath();\n\t\t}\n\t\treturn request.getRequestURL().toString();\n\t}\n\t\n\t/**\n\t * 返回当前请求的类型 \n\t */\n\t@Override\n\tpublic String getMethod() {\n\t\treturn request.getMethod();\n\t}\n\n\t/**\n\t * 查询请求 host\n\t */\n\t@Override\n\tpublic String getHost() {\n\t\treturn request.getServerName();\n\t}\n\n\t/**\n\t * 转发请求 \n\t */\n\t@Override\n\tpublic Object forward(String path) {\n\t\ttry {\n\t\t\tHttpServletResponse response = (HttpServletResponse)SaManager.getSaTokenContext().getResponse().getSource();\n\t\t\trequest.getRequestDispatcher(path).forward(request, response);\n\t\t\treturn null;\n\t\t} catch (ServletException | IOException e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaServletErrorCode.CODE_20001);\n\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.model;\n\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.servlet.error.SaServletErrorCode;\n\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * 对 SaResponse 包装类的实现（Servlet 版）\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaResponseForServlet implements SaResponse {\n\n\t/**\n\t * 底层Request对象\n\t */\n\tprotected HttpServletResponse response;\n\t\n\t/**\n\t * 实例化\n\t * @param response response对象 \n\t */\n\tpublic SaResponseForServlet(HttpServletResponse response) {\n\t\tthis.response = response;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn response;\n\t}\n\n\t/**\n\t * 设置响应状态码 \n\t */\n\t@Override\n\tpublic SaResponse setStatus(int sc) {\n\t\tresponse.setStatus(sc);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 在响应头里写入一个值 \n\t */\n\t@Override\n\tpublic SaResponse setHeader(String name, String value) {\n\t\tresponse.setHeader(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在响应头里添加一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tpublic SaResponse addHeader(String name, String value) {\n\t\tresponse.addHeader(name, value);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 重定向 \n\t */\n\t@Override\n\tpublic Object redirect(String url) {\n\t\ttry {\n\t\t\tresponse.sendRedirect(url);\n\t\t} catch (Exception e) {\n\t\t\tthrow new SaTokenException(e).setCode(SaServletErrorCode.CODE_20002);\n\t\t}\n\t\treturn null;\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.model;\n\nimport cn.dev33.satoken.context.model.SaStorage;\n\nimport javax.servlet.http.HttpServletRequest;\n\n/**\n * 对 SaStorage 包装类的实现（Servlet 版）\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaStorageForServlet implements SaStorage {\n\n\t/**\n\t * 底层Request对象\n\t */\n\tprotected HttpServletRequest request;\n\t\n\t/**\n\t * 实例化\n\t * @param request request对象 \n\t */\n\tpublic SaStorageForServlet(HttpServletRequest request) {\n\t\tthis.request = request;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn request;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里写入一个值 \n\t */\n\t@Override\n\tpublic SaStorageForServlet set(String key, Object value) {\n\t\trequest.setAttribute(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn request.getAttribute(key);\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里删除一个值 \n\t */\n\t@Override\n\tpublic SaStorageForServlet delete(String key) {\n\t\trequest.removeAttribute(key);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token对接 Servlet API 容器所需要的实现类接口包\n */\npackage cn.dev33.satoken.servlet;"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaServletOperateUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.util;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\n\nimport javax.servlet.ServletResponse;\nimport java.io.IOException;\n\n/**\n * Servlet 操作工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaServletOperateUtil {\n\n\t/**\n\t * 写入结果到输出流\n\t * @param response /\n\t * @param result /\n\t */\n\tpublic static void writeResult(ServletResponse response, String result) throws IOException {\n\t\t// 写入输出流\n\t\t// \t\t请注意此处默认 Content-Type 为 text/plain，如果需要返回 JSON 信息，需要在 return 前自行设置 Content-Type 为 application/json\n\t\t// \t\t例如：SaHolder.getResponse().setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n\t\tif(response.getContentType() == null) {\n\t\t\tresponse.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);\n\t\t}\n\t\tresponse.getWriter().print(result);\n\t\tresponse.getWriter().flush();\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/util/SaTokenContextServletUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.servlet.util;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.model.SaStorageForServlet;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * SaTokenContext 上下文读写工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextServletUtil {\n\n\t/**\n\t * 写入当前上下文\n\t * @param request /\n\t * @param response /\n\t */\n\tpublic static void setContext(HttpServletRequest request, HttpServletResponse response) {\n\t\tSaRequest req = new SaRequestForServlet(request);\n\t\tSaResponse res = new SaResponseForServlet(response);\n\t\tSaStorage stg = new SaStorageForServlet(request);\n\t\tSaManager.getSaTokenContext().setContext(req, res, stg);\n\t}\n\n\t/**\n\t * 写入上下文对象, 并在执行函数后将其清除\n\t * @param request /\n\t * @param response /\n\t * @param fun /\n\t */\n\tpublic static void setContext(HttpServletRequest request, HttpServletResponse response, SaFunction fun) {\n\t\ttry {\n\t\t\tsetContext(request, response);\n\t\t\tfun.run();\n\t\t} finally {\n\t\t\tclearContext();\n\t\t}\n\t}\n\n\t/**\n\t * 写入上下文对象, 并在执行函数后将其清除\n\t *\n\t * @param request /\n\t * @param response /\n\t * @param fun /\n\t * @return /\n\t * @param <T> /\n\t */\n\tpublic static <T> T setContext(HttpServletRequest request, HttpServletResponse response, SaRetGenericFunction<T> fun) {\n\t\ttry {\n\t\t\tsetContext(request, response);\n\t\t\treturn fun.run();\n\t\t} finally {\n\t\t\tclearContext();\n\t\t}\n\t}\n\n\t/**\n\t * 清除当前上下文\n\t */\n\tpublic static void clearContext() {\n\t\tSaManager.getSaTokenContext().clearContext();\n\t}\n\n\t/**\n\t * 获取当前 ModelBox\n\t * @return /\n\t */\n\tpublic static SaTokenContextModelBox getModelBox() {\n\t\treturn SaManager.getSaTokenContext().getModelBox();\n\t}\n\n\t/**\n\t * 获取当前 Request\n\t * @return /\n\t */\n\tpublic static HttpServletRequest getRequest() {\n\t\treturn (HttpServletRequest) getModelBox().getRequest().getSource();\n\t}\n\n\t/**\n\t * 获取当前 Response\n\t * @return /\n\t */\n\tpublic static HttpServletResponse getResponse() {\n\t\treturn (HttpServletResponse) getModelBox().getResponse().getSource();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n    <name>sa-token-solon-plugin</name>\n    <artifactId>sa-token-solon-plugin</artifactId>\n    <description>solon integrate sa-token</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.noear</groupId>\n            <artifactId>solon</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-core</artifactId>\n        </dependency>\n\n        <!-- OAuth2.0 (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-oauth2</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- SSO (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sso</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- API Key (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-apikey</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- API Sign (optional) -->\n        <dependency>\n            <groupId>cn.dev33</groupId>\n            <artifactId>sa-token-sign</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <!-- redisx + snack3 -->\n        <dependency>\n            <groupId>org.noear</groupId>\n            <artifactId>redisx</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.noear</groupId>\n            <artifactId>snack3</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- redisson + jackson  -->\n        <dependency>\n            <groupId>org.redisson</groupId>\n            <artifactId>redisson</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- jackson-databind -->\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <!-- jackson-datatype-jsr310 -->\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n    </dependencies>\n</project>"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.context.SaTokenContext;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.http.SaHttpTemplate;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate;\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil;\nimport cn.dev33.satoken.json.SaJsonTemplate;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.listener.SaTokenListener;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.plugin.SaTokenPlugin;\nimport cn.dev33.satoken.plugin.SaTokenPluginHolder;\nimport cn.dev33.satoken.same.SaSameTemplate;\nimport cn.dev33.satoken.secure.totp.SaTotpTemplate;\nimport cn.dev33.satoken.serializer.SaSerializerTemplate;\nimport cn.dev33.satoken.stp.StpInterface;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook;\nimport cn.dev33.satoken.temp.SaTempTemplate;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\nimport java.util.List;\n\n/**\n * 注入 Sa-Token 所需要的 Bean\n * \n * @author click33\n * @since 1.34.0\n */\n@Configuration\npublic class SaBeanInject {\n\n\t/**\n\t * 组件注入\n\t * <p> 为确保 Log 组件正常打印，必须将 SaLog 和 SaTokenConfig 率先初始化 </p>\n\t *\n\t * @param log           log 对象\n\t * @param saTokenConfig 配置对象\n\t */\n\tpublic SaBeanInject(\n\t\t\t@Inject(required = false) SaLog log,\n\t\t\t@Inject(required = false) SaTokenConfig saTokenConfig,\n\t\t\t@Inject(required = false) SaTokenPluginHolder pluginHolder\n\t) {\n\t\tif (log != null) {\n\t\t\tSaManager.setLog(log);\n\t\t}\n\n\t\tif (saTokenConfig != null) {\n\t\t\tSaManager.setConfig(saTokenConfig);\n\t\t}\n\n\t\t// 初始化 Sa-Token SPI 插件\n\t\tif (pluginHolder == null) {\n\t\t\tpluginHolder = SaTokenPluginHolder.instance;\n\t\t}\n\t\tpluginHolder.init();\n\t\tSaTokenPluginHolder.instance = pluginHolder;\n\t}\n\n\t/**\n\t * 注入持久化Bean\n\t *\n\t * @param saTokenDao SaTokenDao对象\n\t */\n\t@Condition(onBean = SaTokenDao.class)\n\t@Bean\n\tpublic void setSaTokenDao(SaTokenDao saTokenDao) {\n\t\tSaManager.setSaTokenDao(saTokenDao);\n\t}\n\n\t/**\n\t * 注入权限认证Bean\n\t *\n\t * @param stpInterface StpInterface对象\n\t */\n\t@Condition(onBean = StpInterface.class)\n\t@Bean\n\tpublic void setStpInterface(StpInterface stpInterface) {\n\t\tSaManager.setStpInterface(stpInterface);\n\t}\n\n\t/**\n\t * 注入上下文Bean\n\t *\n\t * @param saTokenContext SaTokenContext对象\n\t */\n\t@Condition(onBean = SaTokenContext.class)\n\t@Bean\n\tpublic void setSaTokenContext(SaTokenContext saTokenContext) {\n\t\tSaManager.setSaTokenContext(saTokenContext);\n\t}\n\n\t/**\n\t * 注入侦听器Bean\n\t *\n\t * @param listenerList 侦听器集合\n\t */\n\t@Bean\n\tpublic void setSaTokenListener(List<SaTokenListener> listenerList) {\n\t\tSaTokenEventCenter.registerListenerList(listenerList);\n\t}\n\n\t/**\n\t * 注入自定义注解处理器\n\t *\n\t * @param handlerList 自定义注解处理器集合\n\t */\n\t@Bean\n\tpublic void setSaAnnotationHandler(List<SaAnnotationHandlerInterface<?>> handlerList) {\n\t\tfor (SaAnnotationHandlerInterface<?> handler : handlerList) {\n\t\t\tSaAnnotationStrategy.instance.registerAnnotationHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * 注入临时令牌验证模块 Bean\n\t *\n\t * @param saTempTemplate /\n\t */\n\t@Condition(onBean = SaTempTemplate.class)\n\t@Bean\n\tpublic void setSaTempTemplate(SaTempTemplate saTempTemplate) {\n\t\tSaManager.setSaTempTemplate(saTempTemplate);\n\t}\n\n\t/**\n\t * 注入 Same-Token 模块 Bean\n\t *\n\t * @param saSameTemplate saSameTemplate对象\n\t */\n\t@Condition(onBean = SaSameTemplate.class)\n\t@Bean\n\tpublic void setSaIdTemplate(SaSameTemplate saSameTemplate) {\n\t\tSaManager.setSaSameTemplate(saSameTemplate);\n\t}\n\n\t/**\n\t * 注入 Sa-Token Http Basic 认证模块\n\t *\n\t * @param saBasicTemplate saBasicTemplate对象\n\t */\n\t@Condition(onBean = SaHttpBasicTemplate.class)\n\t@Bean\n\tpublic void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) {\n\t\tSaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate;\n\t}\n\n\t/**\n\t * 注入 Sa-Token Http Digest 认证模块\n\t *\n\t * @param saHttpDigestTemplate saHttpDigestTemplate 对象\n\t */\n\t@Condition(onBean = SaHttpDigestTemplate.class)\n\t@Bean\n\tpublic void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) {\n\t\tSaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate;\n\t}\n\n\t/**\n\t * 注入自定义的 JSON 转换器 Bean\n\t *\n\t * @param saJsonTemplate JSON 转换器\n\t */\n\t@Condition(onBean = SaJsonTemplate.class)\n\t@Bean\n\tpublic void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) {\n\t\tSaManager.setSaJsonTemplate(saJsonTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 Http 转换器 Bean\n\t *\n\t * @param saHttpTemplate Http 转换器\n\t */\n\t@Condition(onBean = SaHttpTemplate.class)\n\t@Bean\n\tpublic void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) {\n\t\tSaManager.setSaHttpTemplate(saHttpTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的序列化器 Bean\n\t *\n\t * @param saSerializerTemplate 序列化器\n\t */\n\t@Condition(onBean = SaSerializerTemplate.class)\n\t@Bean\n\tpublic void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) {\n\t\tSaManager.setSaSerializerTemplate(saSerializerTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 TOTP 算法 Bean\n\t *\n\t * @param totpTemplate TOTP 算法类\n\t */\n\t@Condition(onBean = SaTotpTemplate.class)\n\t@Bean\n\tpublic void setSaTotpTemplate(SaTotpTemplate totpTemplate) {\n\t\tSaManager.setSaTotpTemplate(totpTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 StpLogic\n\t *\n\t * @param stpLogic /\n\t */\n\t@Condition(onBean = StpLogic.class)\n\t@Bean\n\tpublic void setStpLogic(StpLogic stpLogic) {\n\t\tStpUtil.setStpLogic(stpLogic);\n\t}\n\n\t/**\n\t * 注入自定义防火墙校验 hook 集合\n\t *\n\t * @param hooks /\n\t */\n\t@Bean\n\tpublic void setSaFirewallCheckHooks(List<SaFirewallCheckHook> hooks) {\n\t\tfor (SaFirewallCheckHook hook : hooks) {\n\t\t\tSaFirewallStrategy.instance.registerHook(hook);\n\t\t}\n\t}\n\n\t/**\n\t * 注入CORS 策略处理函数\n\t *\n\t * @param corsHandle /\n\t */\n\t@Condition(onBean = SaCorsHandleFunction.class)\n\t@Bean\n\tpublic void setCorsHandle(SaCorsHandleFunction corsHandle) {\n\t\tSaStrategy.instance.corsHandle = corsHandle;\n\t}\n\n\t/**\n\t * 注入自定义插件集合\n\t *\n\t * @param plugins /\n\t */\n\t@Bean\n\tpublic void setSaTokenPluginList(List<SaTokenPlugin> plugins) {\n\t\tfor (SaTokenPlugin plugin : plugins) {\n\t\t\tSaTokenPluginHolder.instance.installPlugin(plugin);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon;\n\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.solon.integration.SaFirewallCheckFilterForSolon;\nimport cn.dev33.satoken.solon.integration.SaTokenContextFilterForSolon;\nimport cn.dev33.satoken.solon.integration.SaTokenCorsFilterForSolon;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.util.PathAnalyzer;\n\n/**\n * 注册Sa-Token所需要的Bean \n * <p> Bean 的注册与注入应该分开在两个文件中，否则在某些场景下会造成循环依赖 \n * @author click33\n *\n */\n@Configuration\npublic class SaBeanRegister {\n\n\tpublic SaBeanRegister() {\n\t\t// 重写路由匹配算法\n\t\tSaStrategy.instance.routeMatcher = (pattern, path) -> {\n\t\t\treturn PathAnalyzer.get(pattern).matches(path);\n\t\t};\n\t}\n\n\t/**\n\t * 获取配置Bean\n\t *\n\t * @return 配置对象\n\t */\n\t@Bean\n\tpublic SaTokenConfig getSaTokenConfig(@Inject(value = \"${sa-token}\", required = false) SaTokenConfig config) {\n\t\tif (config == null) {\n\t\t\treturn new SaTokenConfig();\n\t\t} else {\n\t\t\treturn config;\n\t\t}\n\t}\n\n\t/**\n\t * 上下文过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean(index = SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)\n\tpublic Filter saTokenContextFilterForSolon() {\n\t\treturn new SaTokenContextFilterForSolon();\n\t}\n\n\t/**\n\t * CORS 跨域策略过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean(index = SaTokenConsts.CORS_FILTER_ORDER)\n\tpublic Filter saTokenCorsFilterForSolon() {\n\t\treturn new SaTokenCorsFilterForSolon();\n\t}\n\n\t/**\n\t * 防火墙过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean(index = SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER)\n\tpublic Filter saFirewallCheckFilterForSolon() {\n\t\treturn new SaFirewallCheckFilterForSolon();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaSolonPlugin.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon;\n\nimport cn.dev33.satoken.solon.apikey.SaApiKeyBeanInject;\nimport cn.dev33.satoken.solon.apikey.SaApiKeyBeanRegister;\nimport cn.dev33.satoken.solon.oauth2.SaOAuth2BeanInject;\nimport cn.dev33.satoken.solon.oauth2.SaOAuth2BeanRegister;\nimport cn.dev33.satoken.solon.sign.SaSignBeanInject;\nimport cn.dev33.satoken.solon.sign.SaSignBeanRegister;\nimport cn.dev33.satoken.solon.sso.SaSsoBeanInject;\nimport cn.dev33.satoken.solon.sso.SaSsoBeanRegister;\nimport org.noear.solon.core.AppContext;\nimport org.noear.solon.core.Plugin;\n\n/**\n * @author noear\n * @since 1.4\n */\npublic class SaSolonPlugin implements Plugin {\n\n    @Override\n    public void start(AppContext context) {\n        // sa-token\n        context.beanMake(SaBeanRegister.class);\n        context.beanMake(SaBeanInject.class);\n\n        // sa-sso\n        context.beanMake(SaSsoBeanRegister.class);\n        context.beanMake(SaSsoBeanInject.class);\n\n        // sa-oauth2\n        context.beanMake(SaOAuth2BeanRegister.class);\n        context.beanMake(SaOAuth2BeanInject.class);\n\n        // sa-apikey\n        context.beanMake(SaApiKeyBeanRegister.class);\n        context.beanMake(SaApiKeyBeanInject.class);\n\n        // sa-sign\n        context.beanMake(SaSignBeanRegister.class);\n        context.beanMake(SaSignBeanInject.class);\n    }\n}"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.apikey;\n\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.config.SaApiKeyConfig;\nimport cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader;\nimport cn.dev33.satoken.apikey.template.SaApiKeyTemplate;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\n\n/**\n * 注入 Sa-Token API Key 所需要的 Bean\n * \n * @author click33\n * @since 1.43.0\n */\n@Condition(onClass=SaApiKeyManager.class)\n@Configuration\npublic class SaApiKeyBeanInject {\n\n\t/**\n\t * 注入 API Key 配置对象\n\t *\n\t * @param saApiKeyConfig 配置对象\n\t */\n\t@Bean\n\t@Condition(onBean = SaApiKeyConfig.class)\n\tpublic void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) {\n\t\tSaApiKeyManager.setConfig(saApiKeyConfig);\n\t}\n\n\t/**\n\t * 注入自定义的 API Key 模版方法 Bean\n\t *\n\t * @param apiKeyTemplate /\n\t */\n\t@Bean\n\t@Condition(onBean = SaApiKeyTemplate.class)\n\tpublic void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) {\n\t\tSaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 API Key 数据加载器 Bean\n\t *\n\t * @param apiKeyDataLoader /\n\t */\n\t@Bean\n\t@Condition(onBean = SaApiKeyDataLoader.class)\n\tpublic void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) {\n\t\tSaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/apikey/SaApiKeyBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.apikey;\n\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.config.SaApiKeyConfig;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * 注册 Sa-Token API Key 所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@Configuration\n@Condition(onClass= SaApiKeyManager.class)\npublic class SaApiKeyBeanRegister {\n\n\t/**\n\t * 获取 API Key 配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\tpublic SaApiKeyConfig getSaApiKeyConfig(@Inject(value = \"${sa-token.api-key}\", required = false) SaApiKeyConfig saApiKeyConfig) {\n\t\tif (saApiKeyConfig == null) {\n\t\t\treturn new SaApiKeyConfig();\n\t\t} else {\n\t\t\treturn saApiKeyConfig;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/error/SaSolonErrorCode.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.error;\n\n/**\n * 定义 sa-token-solon-plugin 所有异常细分状态码 \n * \n * @author click33\n * @since 2022-10-30\n */\npublic interface SaSolonErrorCode {\n\n\t/** 默认的拦截器异常处理函数 */\n\tint CODE_20301 = 20301;\n\n\t/** 默认的 Filter 异常处理函数 */\n\tint CODE_20302 = 20302;\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaFirewallCheckFilterForSolon.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.integration;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.FirewallCheckException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.solon.model.SaRequestForSolon;\nimport cn.dev33.satoken.solon.model.SaResponseForSolon;\nimport cn.dev33.satoken.solon.util.SaSolonOperateUtil;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n/**\n * 防火墙校验过滤器 (基于 Solon)\n *\n * @author noear\n * @since 1.41.0\n */\npublic class SaFirewallCheckFilterForSolon implements Filter {\n\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\n\t\tSaRequestForSolon saRequest = new SaRequestForSolon();\n\t\tSaResponseForSolon saResponse = new SaResponseForSolon();\n\n\t\ttry {\n\t\t\tSaFirewallStrategy.instance.check.execute(saRequest, saResponse, null);\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaSolonOperateUtil.writeResult(ctx, e.getMessage());\n\t\t\treturn;\n\t\t}\n\t\tcatch (FirewallCheckException e) {\n\t\t\tif(SaFirewallStrategy.instance.checkFailHandle == null) {\n\t\t\t\tSaSolonOperateUtil.writeResult(ctx, e.getMessage());\n\t\t\t} else {\n\t\t\t\tSaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// 更多异常则不处理，交由 Web 框架处理\n\n\t\tchain.doFilter(ctx);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenContextFilterForSolon.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.integration;\n\nimport cn.dev33.satoken.solon.util.SaTokenContextSolonUtil;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n/**\n * 上下文初始化过滤器  (基于 Solon)\n *\n * @author noear\n * @since 1.42.0\n */\npublic class SaTokenContextFilterForSolon implements Filter {\n\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\ttry {\n\t\t\tSaTokenContextSolonUtil.setContext(ctx);\n\t\t\tchain.doFilter(ctx);\n\t\t} finally {\n\t\t\tSaTokenContextSolonUtil.clearContext();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenCorsFilterForSolon.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.integration;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.solon.util.SaSolonOperateUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport org.noear.solon.core.handle.Context;\nimport org.noear.solon.core.handle.Filter;\nimport org.noear.solon.core.handle.FilterChain;\n\n/**\n * CORS 跨域策略过滤器 (基于 Solon)\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenCorsFilterForSolon implements Filter {\n\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\t\n\t\ttry {\n\t\t\tSaTokenContextModelBox box = SaHolder.getContext().getModelBox();\n\t\t\tSaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage());\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaSolonOperateUtil.writeResult(ctx, e.getMessage());\n\t\t\treturn;\n\t\t}\n\n\t\tchain.doFilter(ctx);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.integration;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.filter.SaFilter;\nimport cn.dev33.satoken.filter.SaFilterAuthStrategy;\nimport cn.dev33.satoken.filter.SaFilterErrorStrategy;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.solon.util.SaSolonOperateUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport org.noear.solon.Solon;\nimport org.noear.solon.core.handle.*;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * sa-token 基于路由的过滤式鉴权（增加了注解的处理）；使用优先级要低些\n * <p>\n * 对静态文件有处理效果\n * <p>\n * order: -100 (SaTokenInterceptor 和 SaTokenFilter 二选一；不要同时用)\n *\n * @author noear\n * @since 1.10\n */\npublic class SaTokenFilter implements SaFilter, Filter { //之所以改名，为了跟 SaTokenInterceptor 形成一对\n\n\t/**\n\t * 是否打开注解鉴权\n\t */\n\tpublic boolean isAnnotation = true;\n\n\t// ------------------------ 设置此过滤器 拦截 & 放行 的路由\n\n\t/**\n\t * 拦截路由\n\t */\n\tpublic List<String> includeList = new ArrayList<>();\n\n\t/**\n\t * 放行路由\n\t */\n\tpublic List<String> excludeList = new ArrayList<>();\n\n\t@Override\n\tpublic SaTokenFilter addInclude(String... paths) {\n\t\tincludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaTokenFilter addExclude(String... paths) {\n\t\texcludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaTokenFilter setIncludeList(List<String> pathList) {\n\t\tincludeList = pathList;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaTokenFilter setExcludeList(List<String> pathList) {\n\t\texcludeList = pathList;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 [拦截路由] 集合\n\t *\n\t * @return see note\n\t */\n\tpublic List<String> getIncludeList() {\n\t\treturn includeList;\n\t}\n\n\t/**\n\t * 获取 [放行路由] 集合\n\t *\n\t * @return see note\n\t */\n\tpublic List<String> getExcludeList() {\n\t\treturn excludeList;\n\t}\n\n\t// ------------------------ 钩子函数\n\n\t/**\n\t * 认证函数：每次请求执行\n\t */\n\tpublic SaFilterAuthStrategy auth = r -> {\n\t};\n\n\t/**\n\t * 异常处理函数：每次[认证函数]发生异常时执行此函数\n\t */\n\tpublic SaFilterErrorStrategy error = e -> {\n\t\tif (e instanceof SaTokenException) {\n\t\t\tthrow (SaTokenException) e;\n\t\t} else {\n\t\t\tthrow new SaTokenException(e);\n\t\t}\n\t};\n\n\t/**\n\t * 前置函数：在每次[认证函数]之前执行\n\t *      <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n\t */\n\tpublic SaFilterAuthStrategy beforeAuth = r -> {\n\t};\n\n\t@Override\n\tpublic SaTokenFilter setAuth(SaFilterAuthStrategy auth) {\n\t\tthis.auth = auth;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaTokenFilter setError(SaFilterErrorStrategy error) {\n\t\tthis.error = error;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaTokenFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n\t\tthis.beforeAuth = beforeAuth;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic void doFilter(Context ctx, FilterChain chain) throws Throwable {\n\t\ttry {\n\t\t\t//查找当前主处理（在网关内用时，可直接获取缓存）\n\t\t\tHandler mainHandler = ctx.mainHandler();\n\t\t\tif (mainHandler == null) {\n\t\t\t\tmainHandler = Solon.app().router().matchMain(ctx);\n\t\t\t}\n\n\t\t\tif (mainHandler instanceof Gateway) {\n\t\t\t\t//支持网关处理\n\t\t\t\tGateway gateway = (Gateway) mainHandler;\n\t\t\t\tmainHandler = gateway.find(ctx);\n\t\t\t}\n\n\t\t\tAction action = (mainHandler instanceof Action ? (Action) mainHandler : null);\n\n\t\t\t//1.执行前置处理（主要是一些跨域之类的）\n\t\t\tif(beforeAuth != null) {\n\t\t\t\tbeforeAuth.run(mainHandler);\n\t\t\t}\n\n\t\t\t//先路径过滤下（包括了静态文件）\n\t\t\tHandler finalMainHandler = mainHandler;\n\t\t\tSaRouter.match(includeList).notMatch(excludeList).check(r -> {\n\t\t\t\t//2.执行注解处理\n\t\t\t\tif(authAnno(action)) {\n\t\t\t\t\t//3.执行规则处理（如果没有被 @SaIgnore 忽略）\n\t\t\t\t\tauth.run(finalMainHandler);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaSolonOperateUtil.writeResult(ctx, e.getMessage());\n\t\t\treturn;\n\t\t}\n\t\tcatch (SaTokenException e) {\n\t\t\tSaSolonOperateUtil.writeResult(ctx, error.run(e));\n\t\t\treturn;\n\t\t}\n\n\t\tchain.doFilter(ctx);\n\t}\n\n\tprivate boolean authAnno(Action action) {\n\t\t//2.验证注解处理\n\t\tif (isAnnotation && action != null) {\n\t\t\t// 注解校验\n\t\t\ttry{\n\t\t\t\tMethod method = action.method().getMethod();\n\t\t\t\tSaAnnotationStrategy.instance.checkMethodAnnotation.accept(method);\n\t\t\t} catch (StopMatchException ignored) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/integration/SaTokenInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.integration;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.filter.SaFilter;\nimport cn.dev33.satoken.filter.SaFilterAuthStrategy;\nimport cn.dev33.satoken.filter.SaFilterErrorStrategy;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.solon.util.SaSolonOperateUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport org.noear.solon.core.handle.*;\nimport org.noear.solon.core.route.RouterInterceptor;\nimport org.noear.solon.core.route.RouterInterceptorChain;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * sa-token 基于路由的过滤式鉴权（增加了注解的处理）；使用优先级要低些\n * <p>\n * 对静态文件无处理效果\n * <p>\n * order: -100 (SaTokenInterceptor 和 SaTokenFilter 二选一；不要同时用)\n *\n * @author noear\n * @since 1.12\n */\npublic class SaTokenInterceptor implements SaFilter, RouterInterceptor {\n\t/**\n\t * 是否打开注解鉴权\n\t */\n\tpublic boolean isAnnotation = true;\n\n\t// ------------------------ 设置此过滤器 拦截 & 放行 的路由\n\n\t/**\n\t * 拦截路由\n\t */\n\tprotected List<String> includeList = new ArrayList<>();\n\n\t/**\n\t * 放行路由\n\t */\n\tprotected List<String> excludeList = new ArrayList<>();\n\n\t/**\n\t * 添加 [拦截路由]\n\t *\n\t * @param paths 路由\n\t * @return 对象自身\n\t */\n\t@Override\n    public SaTokenInterceptor addInclude(String... paths) {\n\t\tincludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 添加 [放行路由]\n\t *\n\t * @param paths 路由\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaTokenInterceptor addExclude(String... paths) {\n\t\texcludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入 [拦截路由] 集合\n\t *\n\t * @param pathList 路由集合\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaTokenInterceptor setIncludeList(List<String> pathList) {\n\t\tincludeList = pathList;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入 [放行路由] 集合\n\t *\n\t * @param pathList 路由集合\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaTokenInterceptor setExcludeList(List<String> pathList) {\n\t\texcludeList = pathList;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 获取 [拦截路由] 集合\n\t *\n\t * @return see note\n\t */\n\tpublic List<String> getIncludeList() {\n\t\treturn includeList;\n\t}\n\n\t/**\n\t * 获取 [放行路由] 集合\n\t *\n\t * @return see note\n\t */\n\tpublic List<String> getExcludeList() {\n\t\treturn excludeList;\n\t}\n\n\n\t// ------------------------ 钩子函数\n\n\t/**\n\t * 认证函数：每次请求执行\n\t */\n\tprotected SaFilterAuthStrategy auth = r -> {\n\t};\n\n\t/**\n\t * 异常处理函数：每次[认证函数]发生异常时执行此函数\n\t */\n\tprotected SaFilterErrorStrategy error = e -> {\n\t\tif (e instanceof SaTokenException) {\n\t\t\tthrow (SaTokenException) e;\n\t\t} else {\n\t\t\tthrow new SaTokenException(e);\n\t\t}\n\t};\n\n\t/**\n\t * 前置函数：在每次[认证函数]之前执行\n\t */\n\tprotected SaFilterAuthStrategy beforeAuth = r -> {\n\t};\n\n\t/**\n\t * 写入[认证函数]: 每次请求执行\n\t *\n\t * @param auth see note\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaTokenInterceptor setAuth(SaFilterAuthStrategy auth) {\n\t\tthis.auth = auth;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入[异常处理函数]：每次[认证函数]发生异常时执行此函数\n\t *\n\t * @param error see note\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaTokenInterceptor setError(SaFilterErrorStrategy error) {\n\t\tthis.error = error;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入[前置函数]：在每次[认证函数]之前执行\n\t *\n\t * @param beforeAuth see note\n\t * @return 对象自身\n\t */\n\t@Override\n\tpublic SaTokenInterceptor setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n\t\tthis.beforeAuth = beforeAuth;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic void doIntercept(Context ctx, Handler mainHandler, RouterInterceptorChain chain) throws Throwable {\n\t\ttry {\n\t\t\tif (mainHandler instanceof Gateway) {\n\t\t\t\t//支持网关处理\n\t\t\t\tGateway gateway = (Gateway) mainHandler;\n\t\t\t\tmainHandler = gateway.find(ctx);\n\t\t\t}\n\n\t\t\tAction action = (mainHandler instanceof Action ? (Action) mainHandler : null);\n\n\t\t\t//1.执行前置处理（主要是一些跨域之类的）\n\t\t\tif(beforeAuth != null) {\n\t\t\t\tbeforeAuth.run(mainHandler);\n\t\t\t}\n\n\t\t\t//先路径过滤下（不包括静态文件）\n\t\t\tHandler finalMainHandler = mainHandler;\n\t\t\tSaRouter.match(includeList).notMatch(excludeList).check(r -> {\n\t\t\t\t//2.执行注解处理\n\t\t\t\tif(authAnno(action)) {\n\t\t\t\t\t//3.执行规则处理（如果没有被 @SaIgnore 忽略）\n\t\t\t\t\tauth.run(finalMainHandler);\n\t\t\t\t}\n\t\t\t});\n\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaSolonOperateUtil.writeResult(ctx, e.getMessage());\n\t\t\treturn;\n\t\t}\n\t\tcatch (SaTokenException e) {\n\t\t\tSaSolonOperateUtil.writeResult(ctx, error.run(e));\n\t\t\treturn;\n\t\t}\n\n\t\tchain.doIntercept(ctx, mainHandler);\n\t}\n\n\tprivate boolean authAnno(Action action) {\n\t\t//2.验证注解处理\n\t\tif (isAnnotation && action != null) {\n\t\t\t// 注解校验\n\t\t\ttry{\n\t\t\t\tMethod method = action.method().getMethod();\n\t\t\t\tSaAnnotationStrategy.instance.checkMethodAnnotation.accept(method);\n\t\t\t} catch (StopMatchException ignored) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaContextForSolon.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.model;\n\nimport cn.dev33.satoken.context.SaTokenContextForReadOnly;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport org.noear.solon.core.handle.Context;\n\n/**\n * <h2> 此为低版本(<1.42.0) 的上下文处理方案，基于 Solon 内部封装 Context.current() 读写上下文，仅做留档，如无必要请勿使用 </h2>\n *\n * @author noear\n * @since 1.4\n */\npublic class SaContextForSolon implements SaTokenContextForReadOnly {\n\n    /**\n     * 获取当前请求的Request对象\n     */\n    @Override\n    public SaRequest getRequest() {\n        return new SaRequestForSolon();\n    }\n\n    /**\n     * 获取当前请求的Response对象\n     */\n    @Override\n    public SaResponse getResponse() {\n        return new SaResponseForSolon();\n    }\n\n    /**\n     * 获取当前请求的 [存储器] 对象\n     */\n    @Override\n    public SaStorage getStorage() {\n        return new SaStorageForSolon();\n    }\n\n    /**\n     * 此上下文是否有效\n     * @return /\n     */\n    public boolean isValid() {\n        return Context.current() != null;\n    }\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaRequestForSolon.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.model;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.noear.solon.core.handle.Context;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * @author noear\n * @since 1.4\n */\npublic class SaRequestForSolon implements SaRequest {\n\n    protected Context ctx;\n\n    public SaRequestForSolon() {\n        this(Context.current());\n    }\n\n    public SaRequestForSolon(Context ctx) {\n        this.ctx = ctx;\n    }\n\n    @Override\n    public Object getSource() {\n        return ctx;\n    }\n\n    @Override\n    public String getParam(String s) {\n        return ctx.param(s);\n    }\n\n    @Override\n    public Collection<String> getParamNames() {\n        return ctx.paramNames();\n    }\n\n    /**\n     * 获取 [请求体] 里提交的所有参数\n     *\n     * @return 参数列表\n     */\n    @Override\n    public Map<String, String> getParamMap() {\n        return ctx.paramMap().toValueMap();\n    }\n\n    @Override\n    public String getHeader(String s) {\n        return ctx.header(s);\n    }\n\n    @Override\n    public String getCookieValue(String name) {\n        return getCookieLastValue(name);\n    }\n\n    /**\n     * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n     */\n    @Override\n    public String getCookieFirstValue(String name) {\n        return ctx.cookie(name);\n    }\n\n    /**\n     * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n     *\n     * @param name 键\n     * @return 值\n     */\n    @Override\n    public String getCookieLastValue(String name) {\n        return ctx.cookieMap().holder(name).getLastValue();\n    }\n\n    @Override\n    public String getRequestPath() {\n        return ctx.pathNew();\n    }\n\n    @Override\n    public String getUrl() {\n        String currDomain = SaManager.getConfig().getCurrDomain();\n        if (!SaFoxUtil.isEmpty(currDomain)) {\n            return currDomain + this.getRequestPath();\n        }\n        return ctx.url();\n    }\n\n    @Override\n    public String getMethod() {\n        return ctx.method();\n    }\n\n    @Override\n    public String getHost() {\n        return ctx.uri().getHost();\n    }\n\n    @Override\n    public Object forward(String path) {\n        ctx.forward(path);\n        return null;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaResponseForSolon.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.model;\n\nimport cn.dev33.satoken.context.model.SaResponse;\nimport org.noear.solon.core.handle.Context;\n\n/**\n * @author noear\n * @since 1.4\n */\npublic class SaResponseForSolon implements SaResponse {\n\n\tprotected Context ctx;\n\n\tpublic SaResponseForSolon() {\n\t\tthis(Context.current());\n\t}\n\n\tpublic SaResponseForSolon(Context ctx) {\n\t\tthis.ctx = ctx;\n\t}\n\n\t@Override\n\tpublic Object getSource() {\n\t\treturn ctx;\n\t}\n\n\t@Override\n\tpublic SaResponse setStatus(int sc) {\n\t\tctx.status(sc);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaResponse setHeader(String name, String value) {\n\t\tctx.headerSet(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在响应头里添加一个值\n\t *\n\t * @param name  名字\n\t * @param value 值\n\t * @return 对象自身\n\t */\n\tpublic SaResponse addHeader(String name, String value) {\n\t\tctx.headerAdd(name, value);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Object redirect(String url) {\n\t\tctx.redirect(url);\n\t\treturn null;\n\t}\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/model/SaStorageForSolon.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.model;\n\nimport cn.dev33.satoken.context.model.SaStorage;\nimport org.noear.solon.core.handle.Context;\n\n/**\n * @author noear\n * @since 1.4\n */\npublic class SaStorageForSolon implements SaStorage {\n\n    protected Context ctx;\n\n    public SaStorageForSolon() {\n        this(Context.current());\n    }\n\n    public SaStorageForSolon(Context ctx) {\n        this.ctx = ctx;\n    }\n\n    @Override\n    public Object getSource() {\n        return ctx;\n    }\n\n    @Override\n    public SaStorageForSolon set(String key, Object value) {\n        ctx.attrSet(key, value);\n        return this;\n    }\n\n    @Override\n    public Object get(String key) {\n        return ctx.attr(key);\n    }\n\n    @Override\n    public SaStorageForSolon delete(String key) {\n        ctx.attrMap().remove(key);\n        return this;\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.oauth2;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.dao.SaOAuth2Dao;\nimport cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter;\nimport cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;\nimport cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;\nimport cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver;\nimport cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface;\nimport cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;\nimport cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Template;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\n\nimport java.util.List;\n\n\n// 小提示：如果你在 idea 中运行源码时出现异常：java: 程序包cn.dev33.satoken.oauth2不存在。\n// 在项目根目录进入 cmd，执行 mvn package 即可解决\n\n\n/**\n * 注入 Sa-Token-OAuth2 所需要的组件\n * \n * @author click33\n * @since 1.34.0\n */\n@Condition(onClass=SaOAuth2Manager.class)\n@Configuration\npublic class SaOAuth2BeanInject {\n\n\t/**\n\t * 注入 OAuth2 配置对象\n\t *\n\t * @param saOAuth2Config 配置对象\n\t */\n\t@Condition(onBean = SaOAuth2ServerConfig.class)\n\t@Bean\n\tpublic void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) {\n\t\tSaOAuth2Manager.setServerConfig(saOAuth2Config);\n\t}\n\n\t/**\n\t * 注入 OAuth2 模板代码类\n\t *\n\t * @param saOAuth2Template 模板代码类\n\t */\n\t@Condition(onBean = SaOAuth2Template.class)\n\t@Bean\n\tpublic void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) {\n\t\tSaOAuth2Manager.setTemplate(saOAuth2Template);\n\t}\n\n\t/**\n\t * 注入 OAuth2 请求处理器\n\t *\n\t * @param serverProcessor 请求处理器\n\t */\n\t@Condition(onBean = SaOAuth2ServerProcessor.class)\n\t@Bean\n\tpublic void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) {\n\t\tSaOAuth2ServerProcessor.instance = serverProcessor;\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据加载器\n\t *\n\t * @param dataLoader /\n\t */\n\t@Condition(onBean = SaOAuth2DataLoader.class)\n\t@Bean\n\tpublic void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) {\n\t\tSaOAuth2Manager.setDataLoader(dataLoader);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据解析器 Bean\n\t *\n\t * @param dataResolver /\n\t */\n\t@Condition(onBean = SaOAuth2DataResolver.class)\n\t@Bean\n\tpublic void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) {\n\t\tSaOAuth2Manager.setDataResolver(dataResolver);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据格式转换器 Bean\n\t *\n\t * @param dataConverter /\n\t */\n\t@Condition(onBean = SaOAuth2DataConverter.class)\n\t@Bean\n\tpublic void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) {\n\t\tSaOAuth2Manager.setDataConverter(dataConverter);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据构建器 Bean\n\t *\n\t * @param dataGenerate /\n\t */\n\t@Condition(onBean = SaOAuth2DataGenerate.class)\n\t@Bean\n\tpublic void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) {\n\t\tSaOAuth2Manager.setDataGenerate(dataGenerate);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据持久 Bean\n\t *\n\t * @param dao /\n\t */\n\t@Condition(onBean = SaOAuth2Dao.class)\n\t@Bean\n\tpublic void setSaOAuth2Dao(SaOAuth2Dao dao) {\n\t\tSaOAuth2Manager.setDao(dao);\n\t}\n\n\t/**\n\t * 注入自定义 scope 处理器\n\t *\n\t * @param handlerList 自定义 scope 处理器集合\n\t */\n\t@Bean\n\tpublic void setSaOAuth2ScopeHandler(List<SaOAuth2ScopeHandlerInterface> handlerList) {\n\t\tfor (SaOAuth2ScopeHandlerInterface handler : handlerList) {\n\t\t\tSaOAuth2Strategy.instance.registerScopeHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * 注入自定义 grant_type 处理器\n\t *\n\t * @param handlerList 自定义 grant_type 处理器集合\n\t */\n\t@Bean\n\tpublic void setSaOAuth2GrantTypeHandlerInterface(List<SaOAuth2GrantTypeHandlerInterface> handlerList) {\n\t\tfor (SaOAuth2GrantTypeHandlerInterface handler : handlerList) {\n\t\t\tSaOAuth2Strategy.instance.registerGrantTypeHandler(handler);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2BeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.oauth2;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * 注册 Sa-Token-OAuth2 所需要的Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@Condition(onClass=SaOAuth2Manager.class)\n@Configuration\npublic class SaOAuth2BeanRegister {\n\n\t/**\n\t * 获取 OAuth2 配置 Bean\n\t *\n\t * @return 配置对象\n\t */\n\t@Bean\n\tpublic SaOAuth2ServerConfig getSaOAuth2Config(@Inject(value = \"${sa-token.oauth2-server}\", required = false) SaOAuth2ServerConfig serverConfig) {\n\t\tif (serverConfig == null) {\n\t\t\treturn new SaOAuth2ServerConfig();\n\t\t} else {\n\t\t\treturn serverConfig;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/package-info.java",
    "content": "/**\n * sa-token 集成  solon 的各个组件\n */\n/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon;"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.sign;\n\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.config.SaSignManyConfigWrapper;\nimport cn.dev33.satoken.sign.template.SaSignTemplate;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\n\n/**\n * 注入 Sa-Token API 参数签名 所需要的 Bean\n * \n * @author click33\n * @since 1.43.0\n */\n@Configuration\n@Condition(onClass= SaSignManager.class)\npublic class SaSignBeanInject {\n\n\t/**\n\t * 注入 API 参数签名配置对象\n\t *\n\t * @param saSignConfig 配置对象\n\t */\n\t@Bean\n\t@Condition(onBean = SaSignConfig.class)\n\tpublic void setSignConfig(SaSignConfig saSignConfig) {\n\t\tSaSignManager.setConfig(saSignConfig);\n\t}\n\n\t/**\n\t * 注入 API 参数签名配置对象\n\t *\n\t * @param signManyConfigWrapper 配置对象\n\t */\n\t@Bean\n\t@Condition(onBean = SaSignManyConfigWrapper.class)\n\tpublic void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) {\n\t\tSaSignManager.setSignMany(signManyConfigWrapper.getSignMany());\n\t}\n\n\t/**\n\t * 注入自定义的 参数签名 模版方法 Bean\n\t *\n\t * @param saSignTemplate 参数签名 Bean\n\t */\n\t@Bean\n\t@Condition(onBean = SaSignTemplate.class)\n\tpublic void setSaSignTemplate(SaSignTemplate saSignTemplate) {\n\t\tSaSignManager.setSaSignTemplate(saSignTemplate);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sign/SaSignBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.sign;\n\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.config.SaSignManyConfigWrapper;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * 注册 Sa-Token API 参数签名所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@Configuration\n@Condition(onClass= SaSignManager.class)\npublic class SaSignBeanRegister {\n\n\t/**\n\t * 获取 API 参数签名配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\tpublic SaSignConfig getSaSignConfig(@Inject(value = \"${sa-token.sign}\", required = false) SaSignConfig saSignConfig) {\n\t\tif (saSignConfig == null) {\n\t\t\treturn new SaSignConfig();\n\t\t} else {\n\t\t\treturn saSignConfig;\n\t\t}\n\t}\n\n\t/**\n\t * 获取 API 参数签名 Many 配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\tpublic SaSignManyConfigWrapper getSaSignManyConfigWrapper(@Inject(value = \"${sa-token}\", required = false) SaSignManyConfigWrapper signManyConfigWrapper) {\n\t\tif (signManyConfigWrapper == null) {\n\t\t\treturn new SaSignManyConfigWrapper();\n\t\t} else {\n\t\t\treturn signManyConfigWrapper;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.sso;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\n\n/**\n * 注入 Sa-Token SSO 所需要的 Bean\n * \n * @author click33\n * @since 1.34.0\n */\n@Condition(onClass=SaSsoManager.class)\n@Configuration\npublic class SaSsoBeanInject {\n\n\t/**\n\t * 注入 Sa-Token SSO Server 端 配置类\n\t *\n\t * @param serverConfig 配置对象\n\t */\n\t@Condition(onBean = SaSsoServerConfig.class)\n\t@Bean\n\tpublic void setSaSsoServerConfig(SaSsoServerConfig serverConfig) {\n\t\tSaSsoManager.setServerConfig(serverConfig);\n\t}\n\n\t/**\n\t * 注入 Sa-Token SSO Client 端 配置类\n\t *\n\t * @param clientConfig 配置对象\n\t */\n\t@Condition(onBean = SaSsoClientConfig.class)\n\t@Bean\n\tpublic void setSaSsoClientConfig(SaSsoClientConfig clientConfig) {\n\t\tSaSsoManager.setClientConfig(clientConfig);\n\t}\n\n\t/**\n\t * 注入 SSO 模板代码类 (Server 端)\n\t *\n\t * @param ssoServerTemplate /\n\t */\n\t@Condition(onBean = SaSsoServerTemplate.class)\n\t@Bean\n\tpublic void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) {\n\t\tSaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate;\n\t}\n\n\t/**\n\t * 注入 SSO 模板代码类 (Client 端)\n\t *\n\t * @param ssoClientTemplate /\n\t */\n\t@Condition(onBean = SaSsoClientTemplate.class)\n\t@Bean\n\tpublic void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) {\n\t\tSaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate;\n\t}\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/sso/SaSsoBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.sso;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Condition;\nimport org.noear.solon.annotation.Configuration;\nimport org.noear.solon.annotation.Inject;\n\n/**\n * 注册 Sa-Token SSO 所需要的 Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@Condition(onClass=SaSsoManager.class)\n@Configuration\npublic class SaSsoBeanRegister {\n\n\t/**\n\t * 获取 SSO Server 端 配置对象\n\t *\n\t * @return 配置对象\n\t */\n\t@Bean\n\tpublic SaSsoServerConfig getSaSsoServerConfig(@Inject(value = \"${sa-token.sso-server}\", required = false) SaSsoServerConfig serverConfig) {\n\t\tif (serverConfig == null) {\n\t\t\treturn new SaSsoServerConfig();\n\t\t} else {\n\t\t\treturn serverConfig;\n\t\t}\n\t}\n\n\t/**\n\t * 获取 SSO Client 端 配置对象\n\t *\n\t * @return 配置对象\n\t */\n\t@Bean\n\tpublic SaSsoClientConfig getSaSsoClientConfig(@Inject(value = \"${sa-token.sso-client}\", required = false) SaSsoClientConfig clientConfig) {\n\t\tif (clientConfig == null) {\n\t\t\treturn new SaSsoClientConfig();\n\t\t} else {\n\t\t\treturn clientConfig;\n\t\t}\n\t}\n\n\t/**\n\t * 获取 SSO Server 端 SaSsoServerTemplate\n\t *\n\t * @return /\n\t */\n\t@Bean\n\t@Condition(onMissingBean = SaSsoServerTemplate.class)\n\tpublic SaSsoServerTemplate getSaSsoServerTemplate() {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate;\n\t}\n\n\t/**\n\t * 获取 SSO Client 端 SaSsoClientTemplate\n\t *\n\t * @return /\n\t */\n\t@Bean\n\t@Condition(onMissingBean = SaSsoClientTemplate.class)\n\tpublic SaSsoClientTemplate getSaSsoClientTemplate() {\n\t\treturn SaSsoClientProcessor.instance.ssoClientTemplate;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaSolonOperateUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.util;\n\nimport org.noear.solon.core.handle.Context;\n\n/**\n * Solon 操作工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaSolonOperateUtil {\n\n\t/**\n\t * 写入结果到输出流\n\t * @param ctx /\n\t * @param result /\n\t */\n\tpublic static void writeResult(Context ctx, Object result) throws Throwable {\n\t\tif (result != null) {\n\t\t\tctx.render(result);\n\t\t}\n\t\tctx.setHandled(true);\n\t}\n\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/util/SaTokenContextSolonUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.solon.util;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.fun.SaFunction;\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\nimport cn.dev33.satoken.solon.model.SaRequestForSolon;\nimport cn.dev33.satoken.solon.model.SaResponseForSolon;\nimport cn.dev33.satoken.solon.model.SaStorageForSolon;\nimport org.noear.solon.core.handle.Context;\n\n/**\n * SaTokenContext 上下文读写工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaTokenContextSolonUtil {\n\n\t/**\n\t * 写入当前上下文\n\t */\n\tpublic static void setContext(Context ctx) {\n\t\tSaRequest req = new SaRequestForSolon(ctx);\n\t\tSaResponse res = new SaResponseForSolon(ctx);\n\t\tSaStorage stg = new SaStorageForSolon(ctx);\n\t\tSaManager.getSaTokenContext().setContext(req, res, stg);\n\t}\n\n\t/**\n\t * 写入上下文对象, 并在执行函数后将其清除\n\t * @param ctx /\n\t * @param fun /\n\t */\n\tpublic static void setContext(Context ctx, SaFunction fun) {\n\t\ttry {\n\t\t\tsetContext(ctx);\n\t\t\tfun.run();\n\t\t} finally {\n\t\t\tclearContext();\n\t\t}\n\t}\n\n\t/**\n\t * 写入上下文对象, 并在执行函数后将其清除\n\t *\n\t * @param ctx /\n\t * @param fun /\n\t * @return /\n\t * @param <T> /\n\t */\n\tpublic static <T> T setContext(Context ctx, SaRetGenericFunction<T> fun) {\n\t\ttry {\n\t\t\tsetContext(ctx);\n\t\t\treturn fun.run();\n\t\t} finally {\n\t\t\tclearContext();\n\t\t}\n\t}\n\n\t/**\n\t * 清除当前上下文\n\t */\n\tpublic static void clearContext() {\n\t\tSaManager.getSaTokenContext().clearContext();\n\t}\n\n\t/**\n\t * 获取当前 ModelBox\n\t * @return /\n\t */\n\tpublic static SaTokenContextModelBox getModelBox() {\n\t\treturn SaManager.getSaTokenContext().getModelBox();\n\t}\n\n\t/**\n\t * 获取当前 Context\n\t * @return /\n\t */\n\tpublic static Context getContext() {\n\t\treturn (Context) getModelBox().getStorage().getSource();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/main/resources/META-INF/solon/cn.dev33.satoken.solon.properties",
    "content": "solon.plugin=cn.dev33.satoken.solon.SaSolonPlugin"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/test/java/demo/App.java",
    "content": "package demo;\n\nimport org.noear.solon.Solon;\n\n/**\n * @author noear 2022/3/30 created\n */\npublic class App {\n    public static void main(String[] args) {\n        Solon.start(App.class, args);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/test/java/demo/Config.java",
    "content": "//package demo;\n//\n//import org.noear.solon.annotation.Bean;\n//import org.noear.solon.annotation.Configuration;\n//import org.noear.solon.core.handle.Filter;\n//\n//import cn.dev33.satoken.router.SaRouter;\n//import cn.dev33.satoken.solon.integration.SaTokenPathFilter;\n//import cn.dev33.satoken.stp.StpUtil;\n//\n///**\n// * @author noear 2022/3/30 created\n// */\n//@Configuration\n//public class Config {\n//\n//\t@Bean\n//    public Filter saTokenFilter() {\n//        return new SaTokenPathFilter()\n//                // 指定 [拦截路由] 与 [放行路由]\n//                .addInclude(\"/**\").addExclude(\"/favicon.ico\")\n//\n//                // 认证函数: 每次请求执行\n//                .setAuth(s -> {\n//                    SaRouter.match(\"/**\", StpUtil::checkLogin);\n//\n//                    // 根据路由划分模块，不同模块不同鉴权\n//                    SaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n//                    SaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n//                    SaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n//                    SaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n//                })\n//\n//                // 异常处理函数：每次认证函数发生异常时执行此函数\n//                .setError(e -> {\n//                    System.out.println(\"---------- sa全局异常 \");\n//                    System.out.println(e.getMessage());\n//                    StpUtil.login(123);\n//                    return e.getMessage();\n//                });\n//    }\n//}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/App.java",
    "content": "package demo2;\n\nimport org.noear.solon.Solon;\n\n/**\n * @author noear 2022/3/30 created\n */\npublic class App {\n    public static void main(String[] args) {\n        Solon.start(App.class, args);\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/test/java/demo2/Config.java",
    "content": "package demo2;\n\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.noear.solon.Solon;\nimport org.noear.solon.annotation.Bean;\nimport org.noear.solon.annotation.Configuration;\n\n/**\n * @author noear 2022/7/11 created\n */\n@Configuration\npublic class Config {\n//    @Bean\n//    public void saTokenPathInterceptor() {\n//        Solon.app().before(new SaTokenPathInterceptor()\n//                // 指定 [拦截路由] 与 [放行路由]\n//                .addInclude(\"/**\").addExclude(\"/favicon.ico\")\n//\n//                // 认证函数: 每次请求执行\n//                .setAuth(s -> {\n//                    SaRouter.match(\"/**\", StpUtil::checkLogin);\n//\n//                    // 根据路由划分模块，不同模块不同鉴权\n//                    SaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n//                    SaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n//                    SaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n//                    SaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n//                })\n//\n//                // 异常处理函数：每次认证函数发生异常时执行此函数\n//                .setError(e -> {\n//                    System.out.println(\"---------- sa全局异常 \");\n//                    System.out.println(e.getMessage());\n//                    StpUtil.login(123);\n//                    return e.getMessage();\n//                })\n//        );\n//    }\n\n    @Bean\n    public void saTokenPathInterceptor2() {\n        Solon.app().routerInterceptor((ctx, mainHandler, chain) -> {\n            SaRouter.match(\"/**\", StpUtil::checkLogin);\n            // 根据路由划分模块，不同模块不同鉴权\n            SaRouter.match(\"/user/**\", r -> StpUtil.checkPermission(\"user\"));\n            SaRouter.match(\"/admin/**\", r -> StpUtil.checkPermission(\"admin\"));\n            SaRouter.match(\"/goods/**\", r -> StpUtil.checkPermission(\"goods\"));\n            SaRouter.match(\"/orders/**\", r -> StpUtil.checkPermission(\"orders\"));\n\n            chain.doIntercept(ctx, mainHandler);\n        });\n    }\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-solon-plugin/src/test/resources/app.yml",
    "content": "\n\n# sa-token 配置\nsa-token:\n  # token 名称 (同时也是 cookie 名称)\n  token-name: satoken\n  # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n  timeout: 2592000\n  # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n  active-timeout: -1\n  # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n  is-concurrent: true\n  # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n  is-share: false\n  # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n  token-style: uuid\n  # 是否输出操作日志 \n  is-log: true\n\n\nsa-token-dao: #名字可以随意取\n  redis:\n    server: \"localhost:6379\"\n    password: 123456\n    db: 1\n    maxTotal: 200\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-boot-reactor-v2v3v4-common</name>\n    <artifactId>sa-token-spring-boot-reactor-v2v3v4-common</artifactId>\n\t<description>sa-token springboot reactor v2/v3/v4 common</description>\n\n\t<dependencies>\n\t\t\n\t\t<!-- sa-token-core -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-core</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- spring-boot-starter (optional) -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter</artifactId>\n\t\t\t<optional>true</optional>\n        </dependency>\n\n\t\t<!-- spring-web (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-web</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t\t<!-- reactor-core (optional) -->\n\t    <dependency>\n\t    \t<groupId>io.projectreactor</groupId>\n\t    \t<artifactId>reactor-core</artifactId>\n\t\t\t<optional>true</optional>\n\t    </dependency>\n\t\t\n\t\t<!-- spring-boot-configuration-processor (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- sa-token-spring-boot-webmvc-reactor-v2v3v4-common -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<!-- 默认引入 sa-token springboot2 相关依赖版本定义，上层可以继续引入其它版本定义来覆盖本层，Maven 采用就近原则选择依赖版本 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot2-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n\t\t</dependencies>\n\t</dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.context;\n\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\nimport reactor.util.context.Context;\nimport reactor.util.context.ContextView;\n\n/**\n * Reactor 上下文操作（异步），持有当前请求的 ServerWebExchange 全局引用\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaReactorHolder {\n\n\t/**\n\t * ServerWebExchange key\n\t */\n\tpublic static final String EXCHANGE_KEY = \"SA_REACTOR_EXCHANGE_KEY\";\n\n\t/**\n\t * WebFilterChain key\n\t */\n\tpublic static final String CHAIN_KEY = \"SA_REACTOR__CHAIN_KEY\";\n\n\t/**\n\t * 在流式上下文写入 ServerWebExchange\n\t * @param ctx 必填\n\t * @param exchange 必填\n\t * @param chain 非必填\n\t * @return /\n\t */\n\tpublic static Context setContext(Context ctx, ServerWebExchange exchange, WebFilterChain chain) {\n\t\treturn ctx\n\t\t\t\t.put(EXCHANGE_KEY, exchange)\n\t\t\t\t.put(CHAIN_KEY, chain);\n\t}\n\n\t/**\n\t * 在流式上下文获取 ServerWebExchange\n\t * @param ctx /\n\t * @return /\n\t */\n\tpublic static ServerWebExchange getExchange(ContextView ctx) {\n\t\treturn ctx.get(EXCHANGE_KEY);\n\t}\n\n\t/**\n\t * 在流式上下文获取 WebFilterChain\n\t * @param ctx /\n\t * @return /\n\t */\n\tpublic static WebFilterChain getChain(ContextView ctx) {\n\t\treturn ctx.get(CHAIN_KEY);\n\t}\n\n\t/**\n\t * 获取 Mono < ServerWebExchange >\n\t * @return /\n\t */\n\tpublic static Mono<ServerWebExchange> getMonoExchange() {\n\t\treturn Mono.deferContextual(ctx -> Mono.just(getExchange(ctx)));\n\t}\n\n\t/**\n\t * 将 exchange 写入到同步上下文中，并执行一段代码，执行完毕清除上下文\n\t *\n\t * @return /\n\t */\n\tpublic static <R> Mono<R> sync(SaRetGenericFunction<R> fun) {\n\t\treturn Mono.deferContextual(ctx -> {\n\t\t\ttry {\n\t\t\t\tSaReactorSyncHolder.setContext(ctx.get(EXCHANGE_KEY));\n\t\t\t\treturn Mono.just(fun.run());\n\t\t\t} finally {\n\t\t\t\tSaReactorSyncHolder.clearContext();\n\t\t\t}\n\t\t});\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/context/SaReactorSyncHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.context;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.fun.SaRetGenericFunction;\nimport cn.dev33.satoken.reactor.model.SaRequestForReactor;\nimport cn.dev33.satoken.reactor.model.SaResponseForReactor;\nimport cn.dev33.satoken.reactor.model.SaStorageForReactor;\nimport org.springframework.web.server.ServerWebExchange;\n\n/**\n * Reactor上下文操作（同步），持有当前请求的 ServerWebExchange 全局引用\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SaReactorSyncHolder {\n\n\t/**\n\t * 在同步上下文写入 ServerWebExchange\n\t * @param exchange /\n\t */\n\tpublic static void setContext(ServerWebExchange exchange) {\n\t\tSaRequest request = new SaRequestForReactor(exchange.getRequest());\n\t\tSaResponse response = new SaResponseForReactor(exchange.getResponse());\n\t\tSaStorage storage = new SaStorageForReactor(exchange);\n\t\tSaManager.getSaTokenContext().setContext(request, response, storage);\n\t}\n\n\t/**\n\t * 在同步上下文清除 ServerWebExchange\n\t */\n\tpublic static void clearContext() {\n\t\tSaManager.getSaTokenContext().clearContext();\n\t}\n\n\t/**\n\t * 在同步上下文获取 ServerWebExchange\n\t * @return /\n\t */\n\tpublic static ServerWebExchange getExchange() {\n\t\tSaTokenContextModelBox box = SaManager.getSaTokenContext().getModelBox();\n\t\treturn (ServerWebExchange)box.getStorage().getSource();\n\t}\n\n\t/**\n\t * 将 exchange 写入到同步上下文中，并执行一段代码，执行完毕清除上下文\n\t * @param exchange /\n\t * @param fun /\n\t */\n\tpublic static <R>R setContext(ServerWebExchange exchange, SaRetGenericFunction<R> fun) {\n\t\ttry {\n\t\t\tsetContext(exchange);\n\t\t\treturn fun.run();\n\t\t} finally {\n\t\t\tclearContext();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaFirewallCheckFilterForReactor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.filter;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.FirewallCheckException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.reactor.model.SaRequestForReactor;\nimport cn.dev33.satoken.reactor.model.SaResponseForReactor;\nimport cn.dev33.satoken.reactor.util.SaReactorOperateUtil;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n/**\n * 防火墙校验过滤器 (Reactor版)\n *\n * @author click33\n * @since 1.37.0\n */\n@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER)\npublic class SaFirewallCheckFilterForReactor implements WebFilter {\n\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\n\t\tSaRequestForReactor saRequest = new SaRequestForReactor(exchange.getRequest());\n\t\tSaResponseForReactor saResponse = new SaResponseForReactor(exchange.getResponse());\n\n\t\ttry {\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSaFirewallStrategy.instance.check.execute(saRequest, saResponse, exchange);\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\treturn SaReactorOperateUtil.writeResult(exchange, e.getMessage());\n\t\t}\n\t\t// FirewallCheckException 异常则交由异常处理策略处理\n\t\tcatch (FirewallCheckException e) {\n\t\t\tif(SaFirewallStrategy.instance.checkFailHandle == null) {\n\t\t\t\treturn SaReactorOperateUtil.writeResult(exchange, e.getMessage());\n\t\t\t} else {\n\t\t\t\tSaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null);\n\t\t\t\treturn Mono.empty();\n\t\t\t}\n\t\t}\n\t\tfinally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\t\t// 更多异常则不处理，交由 Web 框架处理\n\n\t\t// 向下执行\n\t\treturn chain.filter(exchange);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.filter;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.filter.SaFilter;\nimport cn.dev33.satoken.filter.SaFilterAuthStrategy;\nimport cn.dev33.satoken.filter.SaFilterErrorStrategy;\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.reactor.util.SaReactorOperateUtil;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * Reactor 全局鉴权过滤器\n * <p>\n *     默认优先级为 -100，尽量保证在其它过滤器之前执行\n * </p>\n *\n * @author click33\n * @since 1.34.0\n */\n@Order(SaTokenConsts.ASSEMBLY_ORDER)\npublic class SaReactorFilter implements SaFilter, WebFilter {\n\n\t// ------------------------ 设置此过滤器 拦截 & 放行 的路由 \n\n\t/**\n\t * 拦截路由 \n\t */\n\tpublic List<String> includeList = new ArrayList<>();\n\n\t/**\n\t * 放行路由 \n\t */\n\tpublic List<String> excludeList = new ArrayList<>();\n\n\t@Override\n\tpublic SaReactorFilter addInclude(String... paths) {\n\t\tincludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaReactorFilter addExclude(String... paths) {\n\t\texcludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaReactorFilter setIncludeList(List<String> pathList) {\n\t\tincludeList = pathList;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaReactorFilter setExcludeList(List<String> pathList) {\n\t\texcludeList = pathList;\n\t\treturn this;\n\t}\n\n\n\t// ------------------------ 钩子函数\n\t\n\t/**\n\t * 认证函数：每次请求执行 \n\t */\n\tpublic SaFilterAuthStrategy auth = r -> {};\n\n\t/**\n\t * 异常处理函数：每次[认证函数]发生异常时执行此函数\n\t */\n\tpublic SaFilterErrorStrategy error = e -> {\n\t\tthrow new SaTokenException(e);\n\t};\n\n\t/**\n\t * 前置函数：在每次[认证函数]之前执行\n\t *      <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n\t */\n\tpublic SaFilterAuthStrategy beforeAuth = r -> {};\n\n\t@Override\n\tpublic SaReactorFilter setAuth(SaFilterAuthStrategy auth) {\n\t\tthis.auth = auth;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaReactorFilter setError(SaFilterErrorStrategy error) {\n\t\tthis.error = error;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaReactorFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n\t\tthis.beforeAuth = beforeAuth;\n\t\treturn this;\n\t}\n\n\t// ------------------------ filter\n\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\n\t\t// ---------- 全局认证处理\n\t\ttry {\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tbeforeAuth.run(null);\n\t\t\tSaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null));\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\treturn SaReactorOperateUtil.writeResult(exchange, e.getMessage());\n\t\t}\n\t\tcatch (Throwable e) {\n\t\t\treturn SaReactorOperateUtil.writeResult(exchange, String.valueOf(error.run(e)));\n\t\t}\n\t\tfinally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\n\t\treturn chain.filter(exchange);\n\t}\n}\n\n/*\n * 三种 Filter ：\n * \tWebFilter：\t\tSpring WebFlux 的过滤器，用于拦截 Web 请求\n * \tGlobalFilter：\tSpring Cloud Gateway 的全局过滤器，用于拦截 Gateway 请求\n * \tGatewayFilter：\tSpring Cloud Gateway 的局部过滤器，用于拦截 Gateway 请求\n */"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenContextFilterForReactor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.filter;\n\nimport cn.dev33.satoken.reactor.context.SaReactorHolder;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n/**\n * SaTokenContext 上下文初始化过滤器 (基于 Reactor)\n *\n * @author click33\n * @since 1.42.0\n */\n@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)\npublic class SaTokenContextFilterForReactor implements WebFilter {\n\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\t\t\treturn chain.filter(exchange)\n\t\t\t\t\t.contextWrite(ctx -> SaReactorHolder.setContext(ctx, exchange, chain))\n\t\t\t\t\t.doFinally(r -> {\n\t\t\t\t\t\t// 在流式上下文中保存的数据会随着流式操作的结束而销毁，所以此处无需手动清除数据\n\t\t\t\t\t});\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/filter/SaTokenCorsFilterForReactor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.filter;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.reactor.util.SaReactorOperateUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport reactor.core.publisher.Mono;\n\n/**\n * CORS 跨域策略过滤器 (基于 Reactor)\n *\n * @author click33\n * @since 1.42.0\n */\n@Order(SaTokenConsts.CORS_FILTER_ORDER)\npublic class SaTokenCorsFilterForReactor implements WebFilter {\n\n\t@Override\n\tpublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {\n\n\t\ttry {\n\t\t\tSaReactorSyncHolder.setContext(exchange);\n\t\t\tSaTokenContextModelBox box = SaHolder.getContext().getModelBox();\n\t\t\tSaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage());\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\treturn SaReactorOperateUtil.writeResult(exchange, e.getMessage());\n\t\t}\n\t\tfinally {\n\t\t\tSaReactorSyncHolder.clearContext();\n\t\t}\n\n\t\treturn chain.filter(exchange);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.model;\n\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.reactor.context.SaReactorHolder;\nimport cn.dev33.satoken.reactor.context.SaReactorSyncHolder;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.springframework.http.HttpCookie;\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilterChain;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * 对 SaRequest 包装类的实现（Reactor 响应式编程版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaRequestForReactor implements SaRequest {\n\n\t/**\n\t * 底层Request对象\n\t */\n\tprotected ServerHttpRequest request;\n\t\n\t/**\n\t * 实例化\n\t * @param request request对象 \n\t */\n\tpublic SaRequestForReactor(ServerHttpRequest request) {\n\t\tthis.request = request;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn request;\n\t}\n\n\t/**\n\t * 在 [请求体] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getParam(String name) {\n\t\treturn request.getQueryParams().getFirst(name);\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数名称\n\t * @return 参数名称列表\n\t */\n\t@Override\n\tpublic Collection<String> getParamNames(){\n\t\treturn request.getQueryParams().keySet();\n\t}\n\n\t/**\n\t * 获取 [请求体] 里提交的所有参数\n\t * @return 参数列表\n\t */\n\t@Override\n\tpublic Map<String, String> getParamMap(){\n\t\treturn request.getQueryParams().toSingleValueMap();\n\t}\n\n\t/**\n\t * 在 [请求头] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getHeader(String name) {\n\t\treturn request.getHeaders().getFirst(name);\n\t}\n\n\t/**\n\t * 在 [Cookie作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic String getCookieValue(String name) {\n\t\treturn getCookieLastValue(name);\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (第一个此名称的)\n\t */\n\t@Override\n\tpublic String getCookieFirstValue(String name){\n\t\tHttpCookie cookie = request.getCookies().getFirst(name);\n\t\tif(cookie == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn cookie.getValue();\n\t}\n\n\t/**\n\t * 在 [ Cookie作用域 ] 里获取一个值 (最后一个此名称的)\n\t * @param name 键\n\t * @return 值\n\t */\n\t@Override\n\tpublic String getCookieLastValue(String name){\n\t\tString value = null;\n\t\tString cookieStr = getHeader(\"Cookie\");\n\t\tif(SaFoxUtil.isNotEmpty(cookieStr)) {\n\t\t\tString[] cookieItems = cookieStr.split(\";\");\n\t\t\tfor (String item : cookieItems) {\n\t\t\t\tString[] kv = item.split(\"=\");\n\t\t\t\tif (kv.length == 2) {\n\t\t\t\t\tif (kv[0].trim().equals(name)) {\n\t\t\t\t\t\tvalue = kv[1].trim();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn value;\n\n\t\t// 此种写法无法获取到最后一个 Cookie，WebFlux 底层代码应该是有bug，前端提交多个同名Cookie时只能解析出第一个来\n\t\t//\t\tList<HttpCookie> cookies = request.getCookies().get(name);\n\t\t//\t\tif(cookies.isEmpty()) {\n\t\t//\t\t\treturn null;\n\t\t//\t\t}\n\t\t//\t\treturn cookies.get(cookies.size() - 1).getValue();\n\t}\n\n\t/**\n\t * 返回当前请求path (不包括上下文名称)  \n\t */\n\t@Override\n\tpublic String getRequestPath() {\n\t\treturn ApplicationInfo.cutPathPrefix(request.getPath().toString());\n\t}\n\n\t/**\n\t * 返回当前请求的url，例：http://xxx.com/test\n\t * @return see note\n\t */\n\tpublic String getUrl() {\n\t\tString currDomain = SaManager.getConfig().getCurrDomain();\n\t\tif( ! SaFoxUtil.isEmpty(currDomain)) {\n\t\t\treturn currDomain + this.getRequestPath();\n\t\t}\n\t\treturn request.getURI().toString();\n\t}\n\t\n\t/**\n\t * 返回当前请求的类型 \n\t */\n\t@Override\n\tpublic String getMethod() {\n\t\treturn request.getMethod().name();\n\t}\n\n\t/**\n\t * 查询请求 host\n\t */\n\t@Override\n\tpublic String getHost() {\n\t\treturn request.getURI().getHost();\n\t}\n\n\t/**\n\t * 转发请求 \n\t */\n\t@Override\n\tpublic Object forward(String path) {\n\t\tServerWebExchange exchange = SaReactorSyncHolder.getExchange();\n\t\tWebFilterChain chain = exchange.getAttribute(SaReactorHolder.CHAIN_KEY);\n\t\t\n\t\tServerHttpRequest newRequest = request.mutate().path(path).build();\n\t\tServerWebExchange newExchange = exchange.mutate().request(newRequest).build();\n\t\t\n\t\treturn chain.filter(newExchange); \n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaResponseForReactor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.model;\n\nimport java.net.URI;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.server.reactive.ServerHttpResponse;\n\nimport cn.dev33.satoken.context.model.SaResponse;\n\n/**\n * 对 SaResponse 包装类的实现（Reactor 响应式编程版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaResponseForReactor implements SaResponse {\n\n\t/**\n\t * 底层Response对象\n\t */\n\tprotected ServerHttpResponse response;\n\t\n\t/**\n\t * 实例化\n\t * @param response response对象 \n\t */\n\tpublic SaResponseForReactor(ServerHttpResponse response) {\n\t\tthis.response = response;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn response;\n\t}\n\n\t/**\n\t * 设置响应状态码 \n\t */\n\t@Override\n\tpublic SaResponse setStatus(int sc) {\n\t\tresponse.setStatusCode(HttpStatus.valueOf(sc));\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 在响应头里写入一个值 \n\t */\n\t@Override\n\tpublic SaResponse setHeader(String name, String value) {\n\t\tresponse.getHeaders().set(name, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在响应头里添加一个值 \n\t * @param name 名字\n\t * @param value 值 \n\t * @return 对象自身 \n\t */\n\tpublic SaResponse addHeader(String name, String value) {\n\t\tresponse.getHeaders().add(name, value);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 重定向 \n\t */\n\t@Override\n\tpublic Object redirect(String url) {\n\t\tresponse.setStatusCode(HttpStatus.FOUND);\n        response.getHeaders().setLocation(URI.create(url));\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/model/SaStorageForReactor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.model;\n\nimport org.springframework.web.server.ServerWebExchange;\n\nimport cn.dev33.satoken.context.model.SaStorage;\n\n/**\n * 对 SaStorage 包装类的实现（Reactor 响应式编程版）\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaStorageForReactor implements SaStorage {\n\n\t/**\n\t * 底层 ServerWebExchange 对象\n\t */\n\tprotected ServerWebExchange exchange;\n\t\n\t/**\n\t * 实例化\n\t * @param exchange exchange对象 \n\t */\n\tpublic SaStorageForReactor(ServerWebExchange exchange) {\n\t\tthis.exchange = exchange;\n\t}\n\t\n\t/**\n\t * 获取底层源对象 \n\t */\n\t@Override\n\tpublic Object getSource() {\n\t\treturn exchange;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里写入一个值 \n\t */\n\t@Override\n\tpublic SaStorageForReactor set(String key, Object value) {\n\t\texchange.getAttributes().put(key, value);\n\t\treturn this;\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里获取一个值 \n\t */\n\t@Override\n\tpublic Object get(String key) {\n\t\treturn exchange.getAttributes().get(key);\n\t}\n\n\t/**\n\t * 在 [Request作用域] 里删除一个值 \n\t */\n\t@Override\n\tpublic SaStorageForReactor delete(String key) {\n\t\texchange.getAttributes().remove(key);\n\t\treturn this;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token 集成 Reactor 响应式编程的各个组件\n */\npackage cn.dev33.satoken.reactor;\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextForSpringReactor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.spring;\n\nimport cn.dev33.satoken.context.SaTokenContextForThreadLocal;\n\n/**\n * <h2> 此为低版本(<1.42.0) 的上下文处理方案，仅做留档，如无必要请勿使用 </h2>\n *\n * Sa-Token 上下文处理器 [ Spring Reactor 版本实现 ] ，基于 SaTokenContextForThreadLocal 定制\n *\n * @author click33\n * @since 1.33.0\n */\npublic class SaTokenContextForSpringReactor extends SaTokenContextForThreadLocal {\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.spring;\n\nimport cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor;\nimport cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor;\nimport cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor;\nimport cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 Sa-Token 所需要的 Bean\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenContextRegister {\n\n\tpublic SaTokenContextRegister() {\n\t\t// 重写路由匹配算法\n\t\tSaStrategy.instance.routeMatcher = (pattern, path) -> {\n\t\t\treturn SaPathPatternParserUtil.match(pattern, path);\n\t\t};\n\t}\n\n\t/**\n\t * 上下文过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaTokenContextFilterForReactor saTokenContextFilterForServlet() {\n\t\treturn new SaTokenContextFilterForReactor();\n\t}\n\n\t/**\n\t * CORS 跨域策略过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaTokenCorsFilterForReactor saTokenCorsFilterForReactor() {\n\t\treturn new SaTokenCorsFilterForReactor();\n\t}\n\n\t/**\n\t * 防火墙过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaFirewallCheckFilterForReactor saFirewallCheckFilterForReactor() {\n\t\treturn new SaFirewallCheckFilterForReactor();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/reactor/util/SaReactorOperateUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.reactor.util;\n\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.web.server.ServerWebExchange;\nimport reactor.core.publisher.Mono;\n\n/**\n * Reactor 操作工具类\n *\n * @author click33\n * @since 1.42.0\n */\npublic class SaReactorOperateUtil {\n\n\t/**\n\t * 写入结果到输出流\n\t * @param exchange /\n\t * @param result /\n\t * @return /\n\t */\n\tpublic static Mono<Void> writeResult(ServerWebExchange exchange, String result) {\n\t\t// 写入输出流\n\t\t// \t\t请注意此处默认 Content-Type 为 text/plain，如果需要返回 JSON 信息，需要在 return 前自行设置 Content-Type 为 application/json\n\t\t// \t\t例如：SaHolder.getResponse().setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n\t\tif(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) {\n\t\t\texchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);\n\t\t}\n\t\treturn exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes())));\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-boot-starter</name>\n    <artifactId>sa-token-spring-boot-starter</artifactId>\n\t<description>springboot integrate sa-token</description>\n\n\t<dependencies>\n\t\t<!-- spring-boot-starter-web -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- config (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t\t<!-- sa-token-servlet -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-servlet</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- sa-token-spring-boot-webmvc-reactor-v2v3v4-common -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- sa-token-jackson: JSON serialization -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson</artifactId>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<!-- sa-token springboot2 相关依赖版本定义 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot2-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n\t\t</dependencies>\n\t</dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.FirewallCheckException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.util.SaServletOperateUtil;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\n\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * 防火墙校验过滤器 (基于 Servlet)\n *\n * @author click33\n * @since 1.37.0\n */\n@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER)\npublic class SaFirewallCheckFilterForServlet implements Filter {\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\n\t\tHttpServletRequest req = (HttpServletRequest) request;\n\t\tHttpServletResponse res = (HttpServletResponse) response;\n\t\tSaRequestForServlet saRequest = new SaRequestForServlet(req);\n\t\tSaResponseForServlet saResponse = new SaResponseForServlet(res);\n\n\t\ttry {\n\t\t\tSaFirewallStrategy.instance.check.execute(saRequest, saResponse, null);\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaServletOperateUtil.writeResult(response, e.getMessage());\n\t\t\treturn;\n\t\t}\n\t\tcatch (FirewallCheckException e) {\n\t\t\tif(SaFirewallStrategy.instance.checkFailHandle == null) {\n\t\t\t\tSaServletOperateUtil.writeResult(response, e.getMessage());\n            } else {\n\t\t\t\tSaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null);\n            }\n            return;\n        }\n\t\t// 更多异常则不处理，交由 Web 框架处理\n\t\t\n\t\t// 向内执行\n\t\tchain.doFilter(request, response);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.servlet.util.SaServletOperateUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\n\nimport javax.servlet.*;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 全局鉴权过滤器 (基于 Servlet)\n * <p>\n *     默认优先级为 -100，尽量保证在其它过滤器之前执行\n * </p>\n *\n * @author click33\n * @since 1.19.0\n */\n@Order(SaTokenConsts.ASSEMBLY_ORDER)\npublic class SaServletFilter implements SaFilter, Filter {\n\n\t// ------------------------ 设置此过滤器 拦截 & 放行 的路由 \n\n\t/**\n\t * 拦截路由 \n\t */\n\tpublic List<String> includeList = new ArrayList<>();\n\n\t/**\n\t * 放行路由 \n\t */\n\tpublic List<String> excludeList = new ArrayList<>();\n\n\t@Override\n\tpublic SaServletFilter addInclude(String... paths) {\n\t\tincludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter addExclude(String... paths) {\n\t\texcludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setIncludeList(List<String> pathList) {\n\t\tincludeList = pathList;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setExcludeList(List<String> pathList) {\n\t\texcludeList = pathList;\n\t\treturn this;\n\t}\n\n\n\t// ------------------------ 钩子函数\n\t\n\t/**\n\t * 认证函数：每次请求执行 \n\t */\n\tpublic SaFilterAuthStrategy auth = r -> {};\n\n\t/**\n\t * 异常处理函数：每次[认证函数]发生异常时执行此函数\n\t */\n\tpublic SaFilterErrorStrategy error = e -> {\n\t\tthrow new SaTokenException(e);\n\t};\n\n\t/**\n\t * 前置函数：在每次[认证函数]之前执行\n\t *      <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n\t */\n\tpublic SaFilterAuthStrategy beforeAuth = r -> {};\n\n\t@Override\n\tpublic SaServletFilter setAuth(SaFilterAuthStrategy auth) {\n\t\tthis.auth = auth;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setError(SaFilterErrorStrategy error) {\n\t\tthis.error = error;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n\t\tthis.beforeAuth = beforeAuth;\n\t\treturn this;\n\t}\n\n\t\n\t// ------------------------ doFilter\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\t\t\n\t\ttry {\n\t\t\t// 执行全局过滤器\n\t\t\tbeforeAuth.run(null);\n\t\t\tSaRouter.match(includeList).notMatch(excludeList).check(r -> {\n\t\t\t\tauth.run(null);\n\t\t\t});\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaServletOperateUtil.writeResult(response, e.getMessage());\n\t\t\treturn;\n\t\t}\n\t\tcatch (Throwable e) {\n\t\t\tSaServletOperateUtil.writeResult(response, String.valueOf(error.run(e)));\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 执行 \n\t\tchain.doFilter(request, response);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\n\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * SaTokenContext 上下文初始化过滤器 (基于 Servlet)\n *\n * @author click33\n * @since 1.42.0\n */\n@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)\npublic class SaTokenContextFilterForServlet implements Filter {\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\t\ttry {\n\t\t\tSaTokenContextServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response);\n\t\t\tchain.doFilter(request, response);\n\t\t} finally {\n\t\t\tSaTokenContextServletUtil.clearContext();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.servlet.util.SaServletOperateUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.springframework.core.annotation.Order;\n\nimport javax.servlet.*;\nimport java.io.IOException;\n\n/**\n * CORS 跨域策略过滤器 (基于 Servlet)\n *\n * @author click33\n * @since 1.42.0\n */\n@Order(SaTokenConsts.CORS_FILTER_ORDER)\npublic class SaTokenCorsFilterForServlet implements Filter {\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\t\t\n\t\ttry {\n\t\t\tSaTokenContextModelBox box = SaHolder.getContext().getModelBox();\n\t\t\tSaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage());\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaServletOperateUtil.writeResult(response, e.getMessage());\n\t\t\treturn;\n\t\t}\n\n\t\tchain.doFilter(request, response);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.interceptor;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.lang.reflect.Method;\n\n/**\n * Sa-Token 综合拦截器，提供注解鉴权和路由拦截鉴权能力 \n * \n * @author click33\n * @since 1.31.0\n */\npublic class SaInterceptor implements HandlerInterceptor {\n\n\t/**\n\t * 是否打开注解鉴权，配置为 true 时注解鉴权才会生效，配置为 false 时，即使写了注解也不会进行鉴权\n\t */\n\tpublic boolean isAnnotation = true;\n\t\n\t/**\n\t * 认证前置函数：在注解鉴权之前执行\n\t * <p> 参数：路由处理函数指针 \n\t */\n\tpublic SaParamFunction<Object> beforeAuth = handler -> {};\n\n\t/**\n\t * 认证函数：每次请求执行\n\t * <p> 参数：路由处理函数指针\n\t */\n\tpublic SaParamFunction<Object> auth = handler -> {};\n\n\t/**\n\t * 创建一个 Sa-Token 综合拦截器，默认带有注解鉴权能力 \n\t */\n\tpublic SaInterceptor() {\n\t}\n\n\t/**\n\t * 创建一个 Sa-Token 综合拦截器，默认带有注解鉴权能力 \n\t * @param auth 认证函数，每次请求执行 \n\t */\n\tpublic SaInterceptor(SaParamFunction<Object> auth) {\n\t\tthis.auth = auth;\n\t}\n\n\t/**\n\t * 设置是否打开注解鉴权：配置为 true 时注解鉴权才会生效，配置为 false 时，即使写了注解也不会进行鉴权\n\t * @param isAnnotation /\n\t * @return 对象自身\n\t */\n\tpublic SaInterceptor isAnnotation(boolean isAnnotation) {\n\t\tthis.isAnnotation = isAnnotation;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入 [ 认证前置函数 ]: 在注解鉴权之前执行\n\t * @param beforeAuth /\n\t * @return 对象自身\n\t */\n\tpublic SaInterceptor setBeforeAuth(SaParamFunction<Object> beforeAuth) {\n\t\tthis.beforeAuth = beforeAuth;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 写入 [ 认证函数 ]: 每次请求执行\n\t * @param auth /\n\t * @return 对象自身\n\t */\n\tpublic SaInterceptor setAuth(SaParamFunction<Object> auth) {\n\t\tthis.auth = auth;\n\t\treturn this;\n\t}\n\n\n\t// ----------------- 验证方法 ----------------- \n\n\t/**\n\t * 每次请求之前触发的方法 \n\t */\n\t@Override\n\t@SuppressWarnings(\"all\")\n\tpublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)\n\t\t\tthrows Exception {\n\n\t\ttry {\n\t\t\t// 前置函数：在注解鉴权之前执行\n\t\t\tbeforeAuth.run(handler);\n\n\t\t\t// 这里必须确保 handler 是 HandlerMethod 类型时，才能进行注解鉴权\n\t\t\tif(isAnnotation && handler instanceof HandlerMethod) {\n\t\t\t\tMethod method = ((HandlerMethod) handler).getMethod();\n\t\t\t\tSaAnnotationStrategy.instance.checkMethodAnnotation.accept(method);\n\t\t\t}\n\t\t\t\n\t\t\t// Auth 路由拦截鉴权校验\n\t\t\tauth.run(handler);\n\t\t\t\n\t\t} catch (StopMatchException e) {\n\t\t\t// StopMatchException 异常代表：停止匹配，进入Controller\n\n\t\t} catch (BackResultException e) {\n\t\t\t// BackResultException 异常代表：停止匹配，向前端输出结果\n\t\t\t// \t\t请注意此处默认 Content-Type 为 text/plain，如果需要返回 JSON 信息，需要在 back 前自行设置 Content-Type 为 application/json\n\t\t\t// \t\t例如：SaHolder.getResponse().setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n\t\t\tif(response.getContentType() == null) {\n\t\t\t\tresponse.setContentType(\"text/plain; charset=utf-8\"); \n\t\t\t}\n\t\t\tresponse.getWriter().print(e.getMessage());\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// 通过验证 \n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token 集成 SpringBoot 的各个组件 \n */\npackage cn.dev33.satoken;"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.context.SaTokenContextForReadOnly;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.model.SaStorageForServlet;\n\n/**\n * <h2> 此为低版本(<1.42.0) 的上下文处理方案，基于 Spring 内部工具类 RequestContextHolder 读写上下文，仅做留档，如无必要请勿使用 </h2>\n *\n * Sa-Token 上下文处理器 [ SpringMVC版本实现 ]。在 SpringMVC、SpringBoot 中使用 Sa-Token 时，必须注入此实现类，否则会出现上下文无效异常\n * \n * @author click33\n * @since 1.19.0\n */\npublic class SaTokenContextForSpring implements SaTokenContextForReadOnly {\n\n\t/**\n\t * 获取当前请求的 Request 包装对象\n\t */\n\t@Override\n\tpublic SaRequest getRequest() {\n\t\treturn new SaRequestForServlet(SpringMVCUtil.getRequest());\n\t}\n\n\t/**\n\t * 获取当前请求的 Response 包装对象\n\t */\n\t@Override\n\tpublic SaResponse getResponse() {\n\t\treturn new SaResponseForServlet(SpringMVCUtil.getResponse());\n\t}\n\n\t/**\n\t * 获取当前请求的 Storage 包装对象\n\t */\n\t@Override\n\tpublic SaStorage getStorage() {\n\t\treturn new SaStorageForServlet(SpringMVCUtil.getRequest());\n\t}\n\n\t/**\n\t * 判断：在本次请求中，此上下文是否可用。\n\t */\n\t@Override\n\tpublic boolean isValid() {\n\t\treturn SpringMVCUtil.isWeb();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.filter.SaFirewallCheckFilterForServlet;\nimport cn.dev33.satoken.filter.SaTokenContextFilterForServlet;\nimport cn.dev33.satoken.filter.SaTokenCorsFilterForServlet;\nimport cn.dev33.satoken.spring.pathmatch.SaPatternsRequestConditionHolder;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 Sa-Token 框架所需要的 Bean\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenContextRegister {\n\n\tpublic SaTokenContextRegister() {\n\t\t// 重写路由匹配算法\n\t\tSaStrategy.instance.routeMatcher = (pattern, path) -> {\n\t\t\treturn SaPatternsRequestConditionHolder.match(pattern, path);\n\t\t};\n\t}\n\n\t/**\n\t * 上下文过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaTokenContextFilterForServlet saTokenContextFilterForServlet() {\n\t\treturn new SaTokenContextFilterForServlet();\n\t}\n\n\t/**\n\t * CORS 跨域策略过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaTokenCorsFilterForServlet saTokenCorsFilterForServlet() {\n\t\treturn new SaTokenCorsFilterForServlet();\n\t}\n\n\t/**\n\t * 防火墙过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaFirewallCheckFilterForServlet saFirewallCheckFilterForServlet() {\n\t\treturn new SaFirewallCheckFilterForServlet();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringBootVersionCompatibilityChecker.java",
    "content": "package cn.dev33.satoken.spring;\r\n\r\nimport cn.dev33.satoken.exception.SaTokenException;\r\nimport cn.dev33.satoken.util.SaFoxUtil;\r\nimport org.springframework.boot.SpringBootVersion;\r\n\r\n/**\r\n * SpringBoot 版本与 Sa-Token 版本兼容检查器，当开发者错误的在 SpringBoot3/4.x 项目中引入当前集成包时，将在控制台做出提醒并阻断项目启动\r\n *\r\n * @author Uncarbon\r\n * @since 1.38.0\r\n */\r\npublic class SpringBootVersionCompatibilityChecker {\r\n\r\n    public SpringBootVersionCompatibilityChecker() {\r\n        String version = SpringBootVersion.getVersion();\r\n        if (SaFoxUtil.isEmpty(version) || version.startsWith(\"1.\") || version.startsWith(\"2.\")) {\r\n            return;\r\n        }\r\n        String str = \"当前 SpringBoot 版本（\" + version + \"）与 Sa-Token 依赖不兼容，\" +\r\n                \"请将依赖 sa-token-spring-boot-starter 修改为：sa-token-spring-boot3/4-starter\";\r\n        System.err.println(str);\r\n        throw new SaTokenException(str);\r\n    }\r\n}\r\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.exception.NotWebContextException;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * SpringMVC 相关操作工具类，快速获取当前会话的 HttpServletRequest、HttpServletResponse 对象\n *\n * @author click33\n * @since 1.19.0\n */\npublic class SpringMVCUtil {\n\t\n\tprivate SpringMVCUtil() {\n\t}\n\t\n\t/**\n\t * 获取当前会话的 request 对象\n\t * @return request\n\t */\n\tpublic static HttpServletRequest getRequest() {\n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n\t\tif(servletRequestAttributes == null) {\n\t\t\tthrow new NotWebContextException(\"非 web 上下文无法获取 HttpServletRequest\");\n\t\t}\n\t\treturn servletRequestAttributes.getRequest();\n\t}\n\t\n\t/**\n\t * 获取当前会话的 response 对象\n\t * @return response\n\t */\n\tpublic static HttpServletResponse getResponse() {\n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n\t\tif(servletRequestAttributes == null) {\n\t\t\tthrow new NotWebContextException(\"非 web 上下文无法获取 HttpServletResponse\");\n\t\t}\n\t\treturn servletRequestAttributes.getResponse();\n\t}\n\n\t/**\n\t * 判断当前是否处于 Web 上下文中  \n\t * @return /\n\t */\n\tpublic static boolean isWeb() {\n\t\treturn RequestContextHolder.getRequestAttributes() != null;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.spring.SpringBootVersionCompatibilityChecker"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-starter/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncn.dev33.satoken.spring.SaTokenContextRegister"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</name>\n    <artifactId>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</artifactId>\n\t<description>sa-token springboot webmvc/reactor v2/v3/v4 common</description>\n\n\t<dependencies>\n\t\t\n\t\t<!-- spring-boot-starter (optional) -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter</artifactId>\n\t\t\t<optional>true</optional>\n        </dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-webmvc</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t\t<!-- config (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- SSO (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-sso</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- OAuth2.0 (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-oauth2</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- API Key (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-apikey</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t\t<!-- API Sign (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-sign</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\n\t</dependencies>\n\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<!-- 默认引入 sa-token springboot2 相关依赖版本定义，上层可以继续引入其它版本定义来覆盖本层，Maven 采用就近原则选择依赖版本 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot2-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n\t\t</dependencies>\n\t</dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token 集成 SpringBoot 的各个组件\n */\npackage cn.dev33.satoken;"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.context.SaTokenContext;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;\nimport cn.dev33.satoken.http.SaHttpTemplate;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate;\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestTemplate;\nimport cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil;\nimport cn.dev33.satoken.json.SaJsonTemplate;\nimport cn.dev33.satoken.listener.SaTokenEventCenter;\nimport cn.dev33.satoken.listener.SaTokenListener;\nimport cn.dev33.satoken.log.SaLog;\nimport cn.dev33.satoken.plugin.SaTokenPlugin;\nimport cn.dev33.satoken.plugin.SaTokenPluginHolder;\nimport cn.dev33.satoken.same.SaSameTemplate;\nimport cn.dev33.satoken.secure.totp.SaTotpTemplate;\nimport cn.dev33.satoken.serializer.SaSerializerTemplate;\nimport cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder;\nimport cn.dev33.satoken.stp.StpInterface;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook;\nimport cn.dev33.satoken.temp.SaTempTemplate;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.util.PathMatcher;\n\nimport java.util.List;\n\n/**\n * 注入 Sa-Token 所需要的 Bean\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaBeanInject {\n\n\t/**\n\t * 组件注入 \n\t * <p> 为确保 Log 组件正常打印，必须将 SaLog 和 SaTokenConfig 率先初始化 </p> \n\t * \n\t * @param log log 对象\n\t * @param saTokenConfig 配置对象\n\t */\n\tpublic SaBeanInject(\n\t\t\t@Autowired(required = false) SaLog log,\n\t\t\t@Autowired(required = false) SaTokenConfig saTokenConfig,\n\t\t\t@Autowired(required = false) SaTokenPluginHolder pluginHolder\n\t\t\t){\n\t\tif(log != null) {\n\t\t\tSaManager.setLog(log);\n\t\t}\n\t\tif(saTokenConfig != null) {\n\t\t\tSaManager.setConfig(saTokenConfig);\n\t\t}\n\t\t// 初始化 Sa-Token SPI 插件\n\t\tif (pluginHolder == null) {\n\t\t\tpluginHolder = SaTokenPluginHolder.instance;\n\t\t}\n\t\tpluginHolder.init();\n\t\tSaTokenPluginHolder.instance = pluginHolder;\n\t}\n\t\n\t/**\n\t * 注入持久化Bean\n\t * \n\t * @param saTokenDao SaTokenDao对象 \n\t */\n\t@Autowired(required = false)\n\tpublic void setSaTokenDao(SaTokenDao saTokenDao) {\n\t\tSaManager.setSaTokenDao(saTokenDao);\n\t}\n\n\t/**\n\t * 注入权限认证Bean\n\t * \n\t * @param stpInterface StpInterface对象 \n\t */\n\t@Autowired(required = false)\n\tpublic void setStpInterface(StpInterface stpInterface) {\n\t\tSaManager.setStpInterface(stpInterface);\n\t}\n\n\t/**\n\t * 注入上下文Bean\n\t * \n\t * @param saTokenContext SaTokenContext对象 \n\t */\n\t@Autowired(required = false)\n\tpublic void setSaTokenContext(SaTokenContext saTokenContext) {\n\t\tSaManager.setSaTokenContext(saTokenContext);\n\t}\n\n\t/**\n\t * 注入侦听器Bean\n\t * \n\t * @param listenerList 侦听器集合 \n\t */\n\t@Autowired(required = false)\n\tpublic void setSaTokenListener(List<SaTokenListener> listenerList) {\n\t\tSaTokenEventCenter.registerListenerList(listenerList);\n\t}\n\n\t/**\n\t * 注入自定义注解处理器\n\t *\n\t * @param handlerList 自定义注解处理器集合\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaAnnotationHandler(List<SaAnnotationHandlerInterface<?>> handlerList) {\n\t\tfor (SaAnnotationHandlerInterface<?> handler : handlerList) {\n\t\t\tSaAnnotationStrategy.instance.registerAnnotationHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * 注入临时令牌验证模块 Bean\n\t * \n\t * @param saTempTemplate /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaTempTemplate(SaTempTemplate saTempTemplate) {\n\t\tSaManager.setSaTempTemplate(saTempTemplate);\n\t}\n\n\t/**\n\t * 注入 Same-Token 模块 Bean\n\t * \n\t * @param saSameTemplate saSameTemplate对象 \n\t */\n\t@Autowired(required = false)\n\tpublic void setSaIdTemplate(SaSameTemplate saSameTemplate) {\n\t\tSaManager.setSaSameTemplate(saSameTemplate);\n\t}\n\n\t/**\n\t * 注入 Sa-Token Http Basic 认证模块 \n\t * \n\t * @param saBasicTemplate saBasicTemplate对象 \n\t */\n\t@Autowired(required = false)\n\tpublic void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) {\n\t\tSaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate;\n\t}\n\n\t/**\n\t * 注入 Sa-Token Http Digest 认证模块\n\t *\n\t * @param saHttpDigestTemplate saHttpDigestTemplate 对象\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaHttpDigestTemplate(SaHttpDigestTemplate saHttpDigestTemplate) {\n\t\tSaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate;\n\t}\n\n\t/**\n\t * 注入自定义的 JSON 转换器 Bean \n\t * \n\t * @param saJsonTemplate JSON 转换器 \n\t */\n\t@Autowired(required = false)\n\tpublic void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) {\n\t\tSaManager.setSaJsonTemplate(saJsonTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 Http 转换器 Bean\n\t *\n\t * @param saHttpTemplate /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaHttpTemplate(SaHttpTemplate saHttpTemplate) {\n\t\tSaManager.setSaHttpTemplate(saHttpTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的序列化器 Bean\n\t *\n\t * @param saSerializerTemplate 序列化器\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaSerializerTemplate(SaSerializerTemplate saSerializerTemplate) {\n\t\tSaManager.setSaSerializerTemplate(saSerializerTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 TOTP 算法 Bean\n\t *\n\t * @param totpTemplate TOTP 算法类\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaTotpTemplate(SaTotpTemplate totpTemplate) {\n\t\tSaManager.setSaTotpTemplate(totpTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 StpLogic \n\t * @param stpLogic / \n\t */\n\t@Autowired(required = false)\n\tpublic void setStpLogic(StpLogic stpLogic) {\n\t\tStpUtil.setStpLogic(stpLogic);\n\t}\n\t\n\t/**\n\t * 利用自动注入特性，获取Spring框架内部使用的路由匹配器\n\t * \n\t * @param pathMatcher 要设置的 pathMatcher\n\t */\n\t@Autowired(required = false)\n\t@Qualifier(\"mvcPathMatcher\")\n\tpublic void setPathMatcher(PathMatcher pathMatcher) {\n\t\tSaPathMatcherHolder.setPathMatcher(pathMatcher);\n\t}\n\n\t/**\n\t * 注入自定义防火墙校验 hook 集合\n\t *\n\t * @param hooks /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaFirewallCheckHooks(List<SaFirewallCheckHook> hooks) {\n\t\tfor (SaFirewallCheckHook hook : hooks) {\n\t\t\tSaFirewallStrategy.instance.registerHook(hook);\n\t\t}\n\t}\n\n\t/**\n\t * 注入CORS 策略处理函数\n\t *\n\t * @param corsHandle /\n\t */\n\t@Autowired(required = false)\n\tpublic void setCorsHandle(SaCorsHandleFunction corsHandle) {\n\t\tSaStrategy.instance.corsHandle = corsHandle;\n\t}\n\n\t/**\n\t * 注入自定义插件集合\n\t *\n\t * @param plugins /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaTokenPluginList(List<SaTokenPlugin> plugins) {\n\t\tfor (SaTokenPlugin plugin : plugins) {\n\t\t\tSaTokenPluginHolder.instance.installPlugin(plugin);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.spring.context.path.ApplicationContextPathLoading;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册Sa-Token所需要的Bean \n * <p> Bean 的注册与注入应该分开在两个文件中，否则在某些场景下会造成循环依赖 \n * @author click33\n *\n */\npublic class SaBeanRegister {\n\n\t/**\n\t * 获取配置Bean\n\t * \n\t * @return 配置对象\n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = \"sa-token\")\n\tpublic SaTokenConfig getSaTokenConfig() {\n\t\treturn new SaTokenConfig();\n\t}\n\n\t/**\n\t * 应用上下文路径加载器\n\t * @return /\n\t */\n\t@Bean\n\tpublic ApplicationContextPathLoading getApplicationContextPathLoading() {\n\t\treturn new ApplicationContextPathLoading();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.apikey;\n\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.config.SaApiKeyConfig;\nimport cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader;\nimport cn.dev33.satoken.apikey.template.SaApiKeyTemplate;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\n\n/**\n * 注入 Sa-Token API Key 所需要的 Bean\n * \n * @author click33\n * @since 1.43.0\n */\n@ConditionalOnClass(SaApiKeyManager.class)\npublic class SaApiKeyBeanInject {\n\n\t/**\n\t * 注入 API Key 配置对象\n\t *\n\t * @param saApiKeyConfig 配置对象\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaApiKeyConfig(SaApiKeyConfig saApiKeyConfig) {\n\t\tSaApiKeyManager.setConfig(saApiKeyConfig);\n\t}\n\n\t/**\n\t * 注入自定义的 API Key 模版方法 Bean\n\t *\n\t * @param apiKeyTemplate /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) {\n\t\tSaApiKeyManager.setSaApiKeyTemplate(apiKeyTemplate);\n\t}\n\n\t/**\n\t * 注入自定义的 API Key 数据加载器 Bean\n\t *\n\t * @param apiKeyDataLoader /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) {\n\t\tSaApiKeyManager.setSaApiKeyDataLoader(apiKeyDataLoader);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/SaApiKeyBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.apikey;\n\nimport cn.dev33.satoken.apikey.SaApiKeyManager;\nimport cn.dev33.satoken.apikey.config.SaApiKeyConfig;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 Sa-Token API Key 所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@ConditionalOnClass(SaApiKeyManager.class)\npublic class SaApiKeyBeanRegister {\n\n\t/**\n\t * 获取 API Key 配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = \"sa-token.api-key\")\n\tpublic SaApiKeyConfig getSaApiKeyConfig() {\n\t\treturn new SaApiKeyConfig();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/apikey/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * sa-token-apikey 模块自动化配置（只有引入了 sa-token-apikey 模块后，此包下的代码才会开始工作）\n */\npackage cn.dev33.satoken.spring.apikey;"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/context/path/ApplicationContextPathLoading.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.context.path;\n\nimport cn.dev33.satoken.application.ApplicationInfo;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\n\n/**\n * 应用上下文路径加载器\n *\n * @author click33\n * @since 1.37.0\n */\npublic class ApplicationContextPathLoading implements ApplicationRunner {\n\n    @Value(\"${server.servlet.context-path:}\")\n    String contextPath;\n\n    @Value(\"${spring.mvc.servlet.path:}\")\n    String servletPath;\n\n    @Override\n    public void run(ApplicationArguments args) throws Exception {\n\n        String routePrefix = \"\";\n\n        if(SaFoxUtil.isNotEmpty(contextPath)) {\n            if(! contextPath.startsWith(\"/\")){\n                contextPath = \"/\" + contextPath;\n            }\n            if (contextPath.endsWith(\"/\")) {\n                contextPath = contextPath.substring(0, contextPath.length() - 1);\n            }\n            routePrefix += contextPath;\n        }\n\n        if(SaFoxUtil.isNotEmpty(servletPath)) {\n            if(! servletPath.startsWith(\"/\")){\n                servletPath = \"/\" + servletPath;\n            }\n            if (servletPath.endsWith(\"/\")) {\n                servletPath = servletPath.substring(0, servletPath.length() - 1);\n            }\n            routePrefix += servletPath;\n        }\n\n        if(SaFoxUtil.isNotEmpty(routePrefix) && ! routePrefix.equals(\"/\") ){\n            ApplicationInfo.routePrefix = routePrefix;\n        }\n    }\n\n}"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.oauth2;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport cn.dev33.satoken.oauth2.dao.SaOAuth2Dao;\nimport cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter;\nimport cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;\nimport cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;\nimport cn.dev33.satoken.oauth2.data.resolver.SaOAuth2DataResolver;\nimport cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface;\nimport cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;\nimport cn.dev33.satoken.oauth2.scope.handler.SaOAuth2ScopeHandlerInterface;\nimport cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy;\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Template;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\n\nimport java.util.List;\n\n\n// 小提示：如果你在 idea 中运行源码时出现异常：java: 程序包cn.dev33.satoken.oauth2不存在。\n// 在项目根目录进入 cmd，执行 mvn package 即可解决\n\n\n/**\n * 注入 Sa-Token-OAuth2 所需要的组件\n * \n * @author click33\n * @since 1.34.0\n */\n@ConditionalOnClass(SaOAuth2Manager.class)\npublic class SaOAuth2BeanInject {\n\n\t/**\n\t * 注入 OAuth2 配置对象\n\t * \n\t * @param saOAuth2Config 配置对象 \n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) {\n\t\tSaOAuth2Manager.setServerConfig(saOAuth2Config);\n\t}\n\n\t/**\n\t * 注入 OAuth2 模板代码类\n\t * \n\t * @param saOAuth2Template 模板代码类\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2Template(SaOAuth2Template saOAuth2Template) {\n\t\tSaOAuth2Manager.setTemplate(saOAuth2Template);\n\t}\n\n\t/**\n\t * 注入 OAuth2 请求处理器\n\t *\n\t * @param serverProcessor 请求处理器\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2Template(SaOAuth2ServerProcessor serverProcessor) {\n\t\tSaOAuth2ServerProcessor.instance = serverProcessor;\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据加载器\n\t *\n\t * @param dataLoader /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2DataLoader(SaOAuth2DataLoader dataLoader) {\n\t\tSaOAuth2Manager.setDataLoader(dataLoader);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据解析器 Bean\n\t *\n\t * @param dataResolver /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2DataResolver(SaOAuth2DataResolver dataResolver) {\n\t\tSaOAuth2Manager.setDataResolver(dataResolver);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据格式转换器 Bean\n\t *\n\t * @param dataConverter /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2DataConverter(SaOAuth2DataConverter dataConverter) {\n\t\tSaOAuth2Manager.setDataConverter(dataConverter);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据构建器 Bean\n\t *\n\t * @param dataGenerate /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2DataGenerate(SaOAuth2DataGenerate dataGenerate) {\n\t\tSaOAuth2Manager.setDataGenerate(dataGenerate);\n\t}\n\n\t/**\n\t * 注入 OAuth2 数据持久 Bean\n\t *\n\t * @param dao /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2Dao(SaOAuth2Dao dao) {\n\t\tSaOAuth2Manager.setDao(dao);\n\t}\n\n\t/**\n\t * 注入自定义 scope 处理器\n\t *\n\t * @param handlerList 自定义 scope 处理器集合\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2ScopeHandler(List<SaOAuth2ScopeHandlerInterface> handlerList) {\n\t\tfor (SaOAuth2ScopeHandlerInterface handler : handlerList) {\n\t\t\tSaOAuth2Strategy.instance.registerScopeHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * 注入自定义 grant_type 处理器\n\t *\n\t * @param handlerList 自定义 grant_type 处理器集合\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaOAuth2GrantTypeHandlerInterface(List<SaOAuth2GrantTypeHandlerInterface> handlerList) {\n\t\tfor (SaOAuth2GrantTypeHandlerInterface handler : handlerList) {\n\t\t\tSaOAuth2Strategy.instance.registerGrantTypeHandler(handler);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.oauth2;\n\nimport cn.dev33.satoken.oauth2.SaOAuth2Manager;\nimport cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 Sa-Token-OAuth2 所需要的Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@ConditionalOnClass(SaOAuth2Manager.class)\npublic class SaOAuth2BeanRegister {\n\n\t/**\n\t * 获取 OAuth2 配置 Bean\n\t *\n\t * @return 配置对象 \n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = \"sa-token.oauth2-server\")\n\tpublic SaOAuth2ServerConfig getSaOAuth2Config() {\n\t\treturn new SaOAuth2ServerConfig();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/oauth2/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token-OAuth2 模块自动化配置（只有引入了Sa-Token-OAuth2模块后，此包下的代码才会开始工作）\n */\npackage cn.dev33.satoken.spring.oauth2;"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathMatcherHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.pathmatch;\n\nimport org.springframework.util.AntPathMatcher;\nimport org.springframework.util.PathMatcher;\n\n/**\n * 路由匹配工具类：持有 PathMatcher 全局引用，方便快捷的调用 PathMatcher 相关方法\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SaPathMatcherHolder {\n\t\n\tprivate SaPathMatcherHolder() {\n\t}\n\n\t/**\n\t * 路由匹配器\n\t */\n\tpublic static PathMatcher pathMatcher;\n\n\t/**\n\t * 获取路由匹配器\n\t * @return 路由匹配器\n\t */\n\tpublic static PathMatcher getPathMatcher() {\n\t\tif(pathMatcher == null) {\n\t\t\tpathMatcher = new AntPathMatcher();\n\t\t}\n\t\treturn pathMatcher;\n\t}\n\t\n\t/**\n\t * 写入路由匹配器\n\t * @param pathMatcher 路由匹配器\n\t */\n\tpublic static void setPathMatcher(PathMatcher pathMatcher) {\n\t\tSaPathMatcherHolder.pathMatcher = pathMatcher;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPathPatternParserUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.pathmatch;\n\nimport org.springframework.http.server.PathContainer;\nimport org.springframework.web.util.pattern.PathPattern;\nimport org.springframework.web.util.pattern.PathPatternParser;\n\n/**\n * 路由匹配工具类：使用 PathPatternParser 模式匹配\n *\n * @author click33\n * @since 1.35.1\n */\npublic class SaPathPatternParserUtil {\n\n\tprivate SaPathPatternParserUtil() {\n\t}\n\n\t/**\n\t * 判断：指定路由匹配符是否可以匹配成功指定路径\n\t * @param pattern 路由匹配符\n\t * @param path 要匹配的路径\n\t * @return 是否匹配成功\n\t */\n\tpublic static boolean match(String pattern, String path) {\n\t\tPathPattern pathPattern = PathPatternParser.defaultInstance.parse(pattern);\n\t\tPathContainer pathContainer = PathContainer.parsePath(path);\n\t\treturn pathPattern.matches(pathContainer);\n    }\n\n\t/*\n\t\t表现：\n\t\t\tspringboot 2.x SpringMVC\tmatch(\"/test/test\", \"/test/test/\")  // true\n\t\t\tspringboot 2.x WebFlux\t\tmatch(\"/test/test\", \"/test/test/\")  // true\n\t\t\tspringboot 3.x SpringMVC\tmatch(\"/test/test\", \"/test/test/\")  // false\n\t\t\tspringboot 3.x WebFlux\t\tmatch(\"/test/test\", \"/test/test/\")  // false\n\t */\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/pathmatch/SaPatternsRequestConditionHolder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.pathmatch;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\n/**\n * 路由匹配工具类\n *\n * @author click33\n * @since 1.35.1\n */\npublic class SaPatternsRequestConditionHolder {\n\n\tprivate SaPatternsRequestConditionHolder() {\n\t}\n\n\tpublic static PatternsRequestCondition patternsRequestCondition;\n\n\tpublic static Method matcherMethod;\n\n\tstatic {\n\t\ttry {\n\t\t\tpatternsRequestCondition = new PatternsRequestCondition();\n\t\t\tmatcherMethod = PatternsRequestCondition.class.getDeclaredMethod(\"getMatchingPattern\", String.class, String.class);\n\t\t\tmatcherMethod.setAccessible(true);\n\t\t} catch (NoSuchMethodException e) {\n\t\t\tthrow new SaTokenException(\"路由匹配器初始化失败\", e);\n\t\t}\n\t}\n\n\t/**\n\t * 判断：指定路由匹配符是否可以匹配成功指定路径\n\t * @param pattern 路由匹配符\n\t * @param lookupPath 要匹配的路径\n\t * @return 是否匹配成功\n\t */\n\tpublic static boolean match(String pattern, String lookupPath) {\n\t\ttry {\n\t\t\treturn matcherMethod.invoke(patternsRequestCondition, pattern, lookupPath) != null;\n\t\t} catch (IllegalAccessException | InvocationTargetException e) {\n\t\t\tthrow new SaTokenException(\"路由匹配器调用失败\", e);\n\t\t}\n    }\n\n\t/*\n\t \t性能测试：\n\t\t\t100万次\n\t\t\tnew 对象方式，耗时：3.685s\t\t最慢\n\t\t\t反射调方法方式，耗时：1.311s  \t中等\n\t\t\t原始方式，耗时：0.445s  \t\t最快，但有bug\n\t */\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.sign;\n\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.config.SaSignManyConfigWrapper;\nimport cn.dev33.satoken.sign.template.SaSignTemplate;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\n\n/**\n * 注入 Sa-Token API 参数签名 所需要的 Bean\n * \n * @author click33\n * @since 1.43.0\n */\n@ConditionalOnClass(SaSignManager.class)\npublic class SaSignBeanInject {\n\n\t/**\n\t * 注入 API 参数签名配置对象\n\t *\n\t * @param saSignConfig 配置对象\n\t */\n\t@Autowired(required = false)\n\tpublic void setSignConfig(SaSignConfig saSignConfig) {\n\t\tSaSignManager.setConfig(saSignConfig);\n\t}\n\n\t/**\n\t * 注入 API 参数签名配置对象\n\t *\n\t * @param signManyConfigWrapper 配置对象\n\t */\n\t@Autowired(required = false)\n\tpublic void setSignManyConfig(SaSignManyConfigWrapper signManyConfigWrapper) {\n\t\tSaSignManager.setSignMany(signManyConfigWrapper.getSignMany());\n\t}\n\n\t/**\n\t * 注入自定义的 参数签名 模版方法 Bean\n\t *\n\t * @param saSignTemplate 参数签名 Bean\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaSignTemplate(SaSignTemplate saSignTemplate) {\n\t\tSaSignManager.setSaSignTemplate(saSignTemplate);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/SaSignBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.sign;\n\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.sign.config.SaSignManyConfigWrapper;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 Sa-Token API 参数签名所需要的 Bean\n *\n * @author click33\n * @since 1.43.0\n */\n@ConditionalOnClass(SaSignManager.class)\npublic class SaSignBeanRegister {\n\n\t/**\n\t * 获取 API 参数签名配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = \"sa-token.sign\")\n\tpublic SaSignConfig getSaSignConfig() {\n\t\treturn new SaSignConfig();\n\t}\n\n\t/**\n\t * 获取 API 参数签名 Many 配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = \"sa-token\")\n\tpublic SaSignManyConfigWrapper getSaSignManyConfigWrapper() {\n\t\treturn new SaSignManyConfigWrapper();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sign/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * sa-token-sign 模块自动化配置（只有引入了 sa-token-sign 模块后，此包下的代码才会开始工作）\n */\npackage cn.dev33.satoken.spring.sign;"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanInject.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.sso;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\n\n/**\n * 注入 Sa-Token SSO 所需要的 Bean\n * \n * @author click33\n * @since 1.34.0\n */\n@ConditionalOnClass(SaSsoManager.class)\npublic class SaSsoBeanInject {\n\n\t/**\n\t * 注入 Sa-Token SSO Server 端 配置类\n\t * \n\t * @param serverConfig 配置对象\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaSsoServerConfig(SaSsoServerConfig serverConfig) {\n\t\tSaSsoManager.setServerConfig(serverConfig);\n\t}\n\n\t/**\n\t * 注入 Sa-Token SSO Client 端 配置类\n\t *\n\t * @param clientConfig 配置对象\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaSsoClientConfig(SaSsoClientConfig clientConfig) {\n\t\tSaSsoManager.setClientConfig(clientConfig);\n\t}\n\n\t/**\n\t * 注入 SSO 模板代码类 (Server 端)\n\t *\n\t * @param ssoServerTemplate /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaSsoServerTemplate(SaSsoServerTemplate ssoServerTemplate) {\n\t\tSaSsoServerProcessor.instance.ssoServerTemplate = ssoServerTemplate;\n\t}\n\n\t/**\n\t * 注入 SSO 模板代码类 (Client 端)\n\t *\n\t * @param ssoClientTemplate /\n\t */\n\t@Autowired(required = false)\n\tpublic void setSaSsoClientTemplate(SaSsoClientTemplate ssoClientTemplate) {\n\t\tSaSsoClientProcessor.instance.ssoClientTemplate = ssoClientTemplate;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/SaSsoBeanRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring.sso;\n\nimport cn.dev33.satoken.sso.SaSsoManager;\nimport cn.dev33.satoken.sso.config.SaSsoClientConfig;\nimport cn.dev33.satoken.sso.config.SaSsoServerConfig;\nimport cn.dev33.satoken.sso.processor.SaSsoClientProcessor;\nimport cn.dev33.satoken.sso.processor.SaSsoServerProcessor;\nimport cn.dev33.satoken.sso.template.SaSsoClientTemplate;\nimport cn.dev33.satoken.sso.template.SaSsoServerTemplate;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 Sa-Token SSO 所需要的 Bean\n *\n * @author click33\n * @since 1.34.0\n */\n@ConditionalOnClass(SaSsoManager.class)\npublic class SaSsoBeanRegister {\n\n\t/**\n\t * 获取 SSO Server 端 配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = \"sa-token.sso-server\")\n\tpublic SaSsoServerConfig getSaSsoServerConfig() {\n\t\treturn new SaSsoServerConfig();\n\t}\n\n\t/**\n\t * 获取 SSO Client 端 配置对象\n\t * @return 配置对象\n\t */\n\t@Bean\n\t@ConfigurationProperties(prefix = \"sa-token.sso-client\")\n\tpublic SaSsoClientConfig getSaSsoClientConfig() {\n\t\treturn new SaSsoClientConfig();\n\t}\n\n\t/**\n\t * 获取 SSO Server 端 SaSsoServerTemplate\n\t *\n\t * @return /\n\t */\n\t@Bean\n\t@ConditionalOnMissingBean(SaSsoServerTemplate.class)\n\tpublic SaSsoServerTemplate getSaSsoServerTemplate() {\n\t\treturn SaSsoServerProcessor.instance.ssoServerTemplate;\n\t}\n\n\t/**\n\t * 获取 SSO Client 端 SaSsoClientTemplate\n\t *\n\t * @return /\n\t */\n\t@Bean\n\t@ConditionalOnMissingBean(SaSsoClientTemplate.class)\n\tpublic SaSsoClientTemplate getSaSsoClientTemplate() {\n\t\treturn SaSsoClientProcessor.instance.ssoClientTemplate;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/java/cn/dev33/satoken/spring/sso/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token-SSO 模块自动化配置（只有引入了 sa-token-sso 模块后，此包下的代码才会开始工作）\n */\npackage cn.dev33.satoken.spring.sso;"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.spring.SaBeanRegister\ncn.dev33.satoken.spring.SaBeanInject\ncn.dev33.satoken.spring.sso.SaSsoBeanRegister\ncn.dev33.satoken.spring.sso.SaSsoBeanInject\ncn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister\ncn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject\ncn.dev33.satoken.spring.apikey.SaApiKeyBeanRegister\ncn.dev33.satoken.spring.apikey.SaApiKeyBeanInject\ncn.dev33.satoken.spring.sign.SaSignBeanRegister\ncn.dev33.satoken.spring.sign.SaSignBeanInject"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-reactor-v2v3v4-common/src/main/resources/META-INF/spring.factories",
    "content": "org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncn.dev33.satoken.spring.SaBeanRegister,\\\ncn.dev33.satoken.spring.SaBeanInject,\\\ncn.dev33.satoken.spring.sso.SaSsoBeanRegister,\\\ncn.dev33.satoken.spring.sso.SaSsoBeanInject,\\\ncn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister,\\\ncn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject,\\\ncn.dev33.satoken.spring.apikey.SaApiKeyBeanRegister,\\\ncn.dev33.satoken.spring.apikey.SaApiKeyBeanInject,\\\ncn.dev33.satoken.spring.sign.SaSignBeanRegister,\\\ncn.dev33.satoken.spring.sign.SaSignBeanInject"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-boot-webmvc-v3v4-common</name>\n    <artifactId>sa-token-spring-boot-webmvc-v3v4-common</artifactId>\n\t<description>sa-token springboot webmvc v3v4 common</description>\n\n\t<dependencies>\n\t\t<!-- spring-boot-starter-web -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t\t<!-- config (optional) -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t\t\n\t\t<!-- sa-token-jakarta-servlet -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jakarta-servlet</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- sa-token-spring-boot-webmvc-reactor-v2v3v4-common -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-webmvc-reactor-v2v3v4-common</artifactId>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<!-- 默认引入 sa-token springboot3 相关依赖版本定义，上层可以继续引入其它版本定义来覆盖本层，Maven 采用就近原则选择依赖版本 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot3-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n\t\t</dependencies>\n\t</dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaFirewallCheckFilterForJakartaServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.FirewallCheckException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil;\nimport cn.dev33.satoken.strategy.SaFirewallStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport jakarta.servlet.*;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport org.springframework.core.annotation.Order;\n\nimport java.io.IOException;\n\n/**\n * 防火墙校验过滤器 (基于 Jakarta-Servlet)\n *\n * @author click33\n * @since 1.37.0\n */\n@Order(SaTokenConsts.FIREWALL_CHECK_FILTER_ORDER)\npublic class SaFirewallCheckFilterForJakartaServlet implements Filter {\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\n\t\tHttpServletRequest req = (HttpServletRequest) request;\n\t\tHttpServletResponse res = (HttpServletResponse) response;\n\t\tSaRequestForServlet saRequest = new SaRequestForServlet(req);\n\t\tSaResponseForServlet saResponse = new SaResponseForServlet(res);\n\n\t\ttry {\n\t\t\tSaFirewallStrategy.instance.check.execute(saRequest, saResponse, null);\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaJakartaServletOperateUtil.writeResult(response, e.getMessage());\n\t\t\treturn;\n\t\t}\n\t\tcatch (FirewallCheckException e) {\n\t\t\tif(SaFirewallStrategy.instance.checkFailHandle == null) {\n\t\t\t\tSaJakartaServletOperateUtil.writeResult(response, e.getMessage());\n\t\t\t} else {\n\t\t\t\tSaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// 更多异常则不处理，交由 Web 框架处理\n\n\t\t// 向内执行\n\t\tchain.doFilter(request, response);\n\t}\n\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport jakarta.servlet.*;\nimport org.springframework.core.annotation.Order;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 全局鉴权过滤器 (基于 Jakarta-Servlet)\n * <p>\n *     默认优先级为 -100，尽量保证在其它过滤器之前执行\n * </p>\n *\n * @author click33\n * @since 1.34.0\n */\n@Order(SaTokenConsts.ASSEMBLY_ORDER)\npublic class SaServletFilter implements SaFilter, Filter {\n\n\t// ------------------------ 设置此过滤器 拦截 & 放行 的路由 \n\n\t/**\n\t * 拦截路由 \n\t */\n\tpublic List<String> includeList = new ArrayList<>();\n\n\t/**\n\t * 放行路由 \n\t */\n\tpublic List<String> excludeList = new ArrayList<>();\n\n\t@Override\n\tpublic SaServletFilter addInclude(String... paths) {\n\t\tincludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter addExclude(String... paths) {\n\t\texcludeList.addAll(Arrays.asList(paths));\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setIncludeList(List<String> pathList) {\n\t\tincludeList = pathList;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setExcludeList(List<String> pathList) {\n\t\texcludeList = pathList;\n\t\treturn this;\n\t}\n\n\n\t// ------------------------ 钩子函数\n\t\n\t/**\n\t * 认证函数：每次请求执行 \n\t */\n\tpublic SaFilterAuthStrategy auth = r -> {};\n\n\t/**\n\t * 异常处理函数：每次[认证函数]发生异常时执行此函数\n\t */\n\tpublic SaFilterErrorStrategy error = e -> {\n\t\tthrow new SaTokenException(e);\n\t};\n\n\t/**\n\t * 前置函数：在每次[认证函数]之前执行\n\t *      <b>注意点：前置认证函数将不受 includeList 与 excludeList 的限制，所有路由的请求都会进入 beforeAuth</b>\n\t */\n\tpublic SaFilterAuthStrategy beforeAuth = r -> {};\n\n\t@Override\n\tpublic SaServletFilter setAuth(SaFilterAuthStrategy auth) {\n\t\tthis.auth = auth;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setError(SaFilterErrorStrategy error) {\n\t\tthis.error = error;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) {\n\t\tthis.beforeAuth = beforeAuth;\n\t\treturn this;\n\t}\n\n\t\n\t// ------------------------ doFilter\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\t\t\n\t\ttry {\n\t\t\t// 执行全局过滤器\n\t\t\tbeforeAuth.run(null);\n\t\t\tSaRouter.match(includeList).notMatch(excludeList).check(r -> {\n\t\t\t\tauth.run(null);\n\t\t\t});\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaJakartaServletOperateUtil.writeResult(response, e.getMessage());\n\t\t\treturn;\n\t\t}\n\t\tcatch (Throwable e) {\n\t\t\tSaJakartaServletOperateUtil.writeResult(response, String.valueOf(error.run(e)));\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 执行 \n\t\tchain.doFilter(request, response);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForJakartaServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.servlet.util.SaTokenContextJakartaServletUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport jakarta.servlet.*;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport org.springframework.core.annotation.Order;\n\nimport java.io.IOException;\n\n/**\n * SaTokenContext 上下文初始化过滤器 (基于 Jakarta-Servlet)\n *\n * @author click33\n * @since 1.42.0\n */\n@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)\npublic class SaTokenContextFilterForJakartaServlet implements Filter {\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\t\ttry {\n\t\t\tSaTokenContextJakartaServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response);\n\t\t\tchain.doFilter(request, response);\n\t\t} finally {\n\t\t\tSaTokenContextJakartaServletUtil.clearContext();\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/filter/SaTokenCorsFilterForJakartaServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.filter;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaTokenContextModelBox;\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.servlet.util.SaJakartaServletOperateUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport jakarta.servlet.*;\nimport org.springframework.core.annotation.Order;\n\nimport java.io.IOException;\n\n/**\n * CORS 跨域策略过滤器 (基于 Jakarta-Servlet)\n *\n * @author click33\n * @since 1.42.0\n */\n@Order(SaTokenConsts.CORS_FILTER_ORDER)\npublic class SaTokenCorsFilterForJakartaServlet implements Filter {\n\n\t@Override\n\tpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n\t\t\n\t\ttry {\n\t\t\tSaTokenContextModelBox box = SaHolder.getContext().getModelBox();\n\t\t\tSaStrategy.instance.corsHandle.execute(box.getRequest(), box.getResponse(), box.getStorage());\n\t\t}\n\t\tcatch (StopMatchException ignored) {}\n\t\tcatch (BackResultException e) {\n\t\t\tSaJakartaServletOperateUtil.writeResult(response, e.getMessage());\n\t\t\treturn;\n\t\t}\n\n\t\tchain.doFilter(request, response);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/interceptor/SaInterceptor.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.interceptor;\n\nimport cn.dev33.satoken.exception.BackResultException;\nimport cn.dev33.satoken.exception.StopMatchException;\nimport cn.dev33.satoken.fun.SaParamFunction;\nimport cn.dev33.satoken.strategy.SaAnnotationStrategy;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport java.lang.reflect.Method;\n\n/**\n * Sa-Token 综合拦截器，提供注解鉴权和路由拦截鉴权能力 \n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaInterceptor implements HandlerInterceptor {\n\n\t/**\n\t * 是否打开注解鉴权 \n\t */\n\tpublic boolean isAnnotation = true;\n\t\n\t/**\n\t * 认证函数：每次请求执行 \n\t * <p> 参数：路由处理函数指针 \n\t */\n\tpublic SaParamFunction<Object> auth = handler -> {};\n\n\t/**\n\t * 创建一个 Sa-Token 综合拦截器，默认带有注解鉴权能力 \n\t */\n\tpublic SaInterceptor() {\n\t}\n\n\t/**\n\t * 创建一个 Sa-Token 综合拦截器，默认带有注解鉴权能力 \n\t * @param auth 认证函数，每次请求执行 \n\t */\n\tpublic SaInterceptor(SaParamFunction<Object> auth) {\n\t\tthis.auth = auth;\n\t}\n\n\t/**\n\t * 设置是否打开注解鉴权 \n\t * @param isAnnotation /\n\t * @return 对象自身 \n\t */\n\tpublic SaInterceptor isAnnotation(boolean isAnnotation) {\n\t\tthis.isAnnotation = isAnnotation;\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 写入[认证函数]: 每次请求执行 \n\t * @param auth / \n\t * @return 对象自身 \n\t */\n\tpublic SaInterceptor setAuth(SaParamFunction<Object> auth) {\n\t\tthis.auth = auth;\n\t\treturn this;\n\t}\n\t\n\t\n\t// ----------------- 验证方法 ----------------- \n\n\t/**\n\t * 每次请求之前触发的方法 \n\t */\n\t@Override\n\t@SuppressWarnings(\"all\")\n\tpublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)\n\t\t\tthrows Exception {\n\t\t\n\t\ttry {\n\n\t\t\t// 这里必须确保 handler 是 HandlerMethod 类型时，才能进行注解鉴权\n\t\t\tif(isAnnotation && handler instanceof HandlerMethod) {\n\t\t\t\tMethod method = ((HandlerMethod) handler).getMethod();\n\t\t\t\tSaAnnotationStrategy.instance.checkMethodAnnotation.accept(method);\n\t\t\t}\n\t\t\t\n\t\t\t// Auth 校验  \n\t\t\tauth.run(handler);\n\t\t\t\n\t\t} catch (StopMatchException e) {\n\t\t\t// StopMatchException 异常代表：停止匹配，进入Controller\n\n\t\t} catch (BackResultException e) {\n\t\t\t// BackResultException 异常代表：停止匹配，向前端输出结果\n\t\t\t// \t\t请注意此处默认 Content-Type 为 text/plain，如果需要返回 JSON 信息，需要在 back 前自行设置 Content-Type 为 application/json\n\t\t\t// \t\t例如：SaHolder.getResponse().setHeader(\"Content-Type\", \"application/json;charset=UTF-8\");\n\t\t\tif(response.getContentType() == null) {\n\t\t\t\tresponse.setContentType(\"text/plain; charset=utf-8\"); \n\t\t\t}\n\t\t\tresponse.getWriter().print(e.getMessage());\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t// 通过验证 \n\t\treturn true;\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/package-info.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Sa-Token 集成 SpringBoot3 的各个组件 \n */\npackage cn.dev33.satoken;"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpringInJakartaServlet.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.context.SaTokenContextForReadOnly;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.context.model.SaResponse;\nimport cn.dev33.satoken.context.model.SaStorage;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.servlet.model.SaResponseForServlet;\nimport cn.dev33.satoken.servlet.model.SaStorageForServlet;\n\n/**\n * <h2> 此为低版本(<1.42.0) 的上下文处理方案，基于 Spring 内部工具类 RequestContextHolder 读写上下文，仅做留档，如无必要请勿使用 </h2>\n *\n * Sa-Token 上下文处理器 [ SpringBoot3 Jakarta Servlet 版 ]，在 SpringBoot3 中使用 Sa-Token 时，必须注入此实现类，否则会出现上下文无效异常\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenContextForSpringInJakartaServlet implements SaTokenContextForReadOnly {\n\n\t/**\n\t * 获取当前请求的 Request 包装对象\n\t */\n\t@Override\n\tpublic SaRequest getRequest() {\n\t\treturn new SaRequestForServlet(SpringMVCUtil.getRequest());\n\t}\n\n\t/**\n\t * 获取当前请求的 Response 包装对象\n\t */\n\t@Override\n\tpublic SaResponse getResponse() {\n\t\treturn new SaResponseForServlet(SpringMVCUtil.getResponse());\n\t}\n\n\t/**\n\t * 获取当前请求的 Storage 包装对象\n\t */\n\t@Override\n\tpublic SaStorage getStorage() {\n\t\treturn new SaStorageForServlet(SpringMVCUtil.getRequest());\n\t}\n\n\t/**\n\t * 判断：在本次请求中，此上下文是否可用。\n\t */\n\t@Override\n\tpublic boolean isValid() {\n\t\treturn SpringMVCUtil.isWeb();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.filter.SaFirewallCheckFilterForJakartaServlet;\nimport cn.dev33.satoken.filter.SaTokenContextFilterForJakartaServlet;\nimport cn.dev33.satoken.filter.SaTokenCorsFilterForJakartaServlet;\nimport cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil;\nimport cn.dev33.satoken.strategy.SaStrategy;\nimport org.springframework.context.annotation.Bean;\n\n/**\n * 注册 Sa-Token 框架所需要的 Bean\n * \n * @author click33\n * @since 1.34.0\n */\npublic class SaTokenContextRegister {\n\n\tpublic SaTokenContextRegister() {\n\t\t// 重写路由匹配算法\n\t\tSaStrategy.instance.routeMatcher = (pattern, path) -> {\n\t\t\treturn SaPathPatternParserUtil.match(pattern, path);\n\t\t};\n\t}\n\n\t/**\n\t * 上下文过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaTokenContextFilterForJakartaServlet saTokenContextFilterForServlet() {\n\t\treturn new SaTokenContextFilterForJakartaServlet();\n\t}\n\n\t/**\n\t * CORS 跨域策略过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaTokenCorsFilterForJakartaServlet saTokenCorsFilterForJakartaServlet() {\n\t\treturn new SaTokenCorsFilterForJakartaServlet();\n\t}\n\n\t/**\n\t * 防火墙过滤器\n\t *\n\t * @return /\n\t */\n\t@Bean\n\tpublic SaFirewallCheckFilterForJakartaServlet saFirewallCheckFilterForJakartaServlet() {\n\t\treturn new SaFirewallCheckFilterForJakartaServlet();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/java/cn/dev33/satoken/spring/SpringMVCUtil.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.spring;\n\nimport cn.dev33.satoken.exception.NotWebContextException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\n/**\n * SpringMVC 相关操作工具类，快速获取当前会话的 HttpServletRequest、HttpServletResponse 对象\n *\n * @author click33\n * @since 1.34.0\n */\npublic class SpringMVCUtil {\n\t\n\tprivate SpringMVCUtil() {\n\t}\n\t\n\t/**\n\t * 获取当前会话的 request \n\t * @return request\n\t */\n\tpublic static HttpServletRequest getRequest() {\n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n\t\tif(servletRequestAttributes == null) {\n\t\t\tthrow new NotWebContextException(\"非 web 上下文无法获取 HttpServletRequest\");\n\t\t}\n\t\treturn servletRequestAttributes.getRequest();\n\t}\n\t\n\t/**\n\t * 获取当前会话的 response\n\t * @return response\n\t */\n\tpublic static HttpServletResponse getResponse() {\n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n\t\tif(servletRequestAttributes == null) {\n\t\t\tthrow new NotWebContextException(\"非 web 上下文无法获取 HttpServletRequest\");\n\t\t}\n\t\treturn servletRequestAttributes.getResponse();\n\t}\n\n\t/**\n\t * 判断当前是否处于 Web 上下文中  \n\t * @return request\n\t */\n\tpublic static boolean isWeb() {\n\t\treturn RequestContextHolder.getRequestAttributes() != null;\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot-webmvc-v3v4-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
    "content": "cn.dev33.satoken.spring.SaTokenContextRegister\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot3-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-boot3-starter</name>\n    <artifactId>sa-token-spring-boot3-starter</artifactId>\n\t<description>springboot3 integrate sa-token</description>\n    \n\t<dependencies>\n\n\t\t<!-- sa-token-spring-boot-webmvc-v3v4-common -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-webmvc-v3v4-common</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- sa-token-jackson: JSON serialization for Spring Boot 3 (Jackson 2.x) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<!-- sa-token springboot3 相关依赖版本定义 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot3-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n\t\t</dependencies>\n\t</dependencyManagement>\n\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/Placeholder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.dev33.satoken;\n\n\n/**\n * 占位符\n *\n * @author click33\n * @since 1.45.0\n */\npublic class Placeholder {\n\n}\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot4-starter/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-starter</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-spring-boot4-starter</name>\n    <artifactId>sa-token-spring-boot4-starter</artifactId>\n\t<description>springboot4 integrate sa-token</description>\n    \n\t<dependencies>\n\n\t\t<!-- sa-token-spring-boot-webmvc-v3v4-common -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-webmvc-v3v4-common</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<!-- sa-token-jackson3: JSON serialization for Spring Boot 4 (Jackson 3) -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson3</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n    <dependencyManagement>\n        <dependencies>\n\n\t\t\t<!-- sa-token springboot4 相关依赖版本定义 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot4-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\t\t\t\n        </dependencies>\n    </dependencyManagement>\n\n</project>\n"
  },
  {
    "path": "sa-token-starter/sa-token-spring-boot4-starter/src/main/java/cn/dev33/satoken/Placeholder.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage cn.dev33.satoken;\n\n\n/**\n * 占位符\n *\n * @author click33\n * @since 1.45.0\n */\npublic class Placeholder {\n\n}\n"
  },
  {
    "path": "sa-token-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-parent</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>pom</packaging>\n\n\t<!-- Sa-Token 单元测试合集 -->\n\t<name>sa-token-test</name>\n    <artifactId>sa-token-test</artifactId>\n\t<description>sa-token-test</description>\n\t\n\t\n\t<!-- 所有子模块 -->\n    <modules>\n        <!-- <module>sa-token-core-test</module> -->\n\t\t<module>sa-token-easy-test</module>\n\t\t<module>sa-token-springboot-test</module>\n        <!-- <module>sa-token-springboot-integrate-test</module> -->\n        <module>sa-token-jwt-test</module>\n\t\t<module>sa-token-temp-jwt-test</module>\n\t\t<module>sa-token-json-test</module>\n\t\t<module>sa-token-jackson3-test</module>\n\t\t<module>sa-token-serializer-test</module>\n    </modules>\n\n\t<dependencies>\n\t\t<!-- test -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-test</artifactId>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<!-- config -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-configuration-processor</artifactId>\n\t\t\t<optional>true</optional>\n\t\t</dependency>\n\t</dependencies>\n\n\t<dependencyManagement>\n\t\t<dependencies>\n\n\t\t\t<dependency>\n\t\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t\t<artifactId>sa-token-spring-boot2-dependencies</artifactId>\n\t\t\t\t<version>${revision}</version>\n\t\t\t\t<type>pom</type>\n\t\t\t\t<scope>import</scope>\n\t\t\t</dependency>\n\n\t\t\t<!-- 不在这放一个这个，maven clean 一直报错，原因未知 -->\n\t\t\t<dependency>\n\t\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t\t<artifactId>spring-boot-starter-test</artifactId>\n\t\t\t\t<version>2.7.18</version>\n\t\t\t</dependency>\n\n\t\t</dependencies>\n\t</dependencyManagement>\n\n\t<build>\n\t    <plugins>\n\t    \t<!-- 单元测试报告生成 -->\n\t        <plugin>\n\t            <groupId>org.jacoco</groupId>\n\t            <artifactId>jacoco-maven-plugin</artifactId>\n\t            <version>0.8.6</version>\n\t            <executions>\n\t                <execution>\n\t                    <id>prepare-agent</id>\n\t                    <goals>\n\t                        <goal>prepare-agent</goal>\n\t                    </goals>\n\t                </execution>\n\t                <execution>\n\t                    <id>report-aggregate</id>\n\t                    <phase>test</phase>\n\t                    <goals>\n\t                        <goal>report-aggregate</goal>\n\t                    </goals>\n\t                </execution>\n\t            </executions>\n\t        </plugin>\n\t        <plugin>\n\t            <groupId>org.apache.maven.plugins</groupId>\n\t            <artifactId>maven-surefire-plugin</artifactId>\n\t\t\t\t<version>2.22.2</version>\n\t            <configuration>\n\t            \t<!-- 命令行执行 mvn test 时默认字符集为GBK，与项目设置的utf-8造成冲突，所以这里需要指定字符集为utf-8 -->\n\t                <argLine>${argLine} -Xms256m -Xmx2048m -Dfile.encoding=utf-8</argLine>\n\t                <forkCount>1</forkCount>\n\t                <runOrder>random</runOrder>\n\t            </configuration>\n\t        </plugin>\n\t    </plugins>\n\t</build>\n\t\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-easy-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-test</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-easy-test</name>\n    <artifactId>sa-token-easy-test</artifactId>\n\t<description>sa-token-easy-test</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson3</artifactId>\n\t\t\t<version>${revision}</version>\n\t\t</dependency>\n\t\t<!-- Jackson 3（testJackson3 需要，sa-token-jackson3 中为 optional） -->\n\t\t<dependency>\n\t\t\t<groupId>tools.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t\t<version>3.0.0</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.datatype</groupId>\n\t\t\t<artifactId>jackson-datatype-jsr310</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/SaJsonTemplateTest.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.exception.NotImplException;\nimport cn.dev33.satoken.json.SaJsonTemplateDefaultImpl;\nimport cn.dev33.satoken.json.SaJsonTemplateForJackson3;\nimport com.pj.test.model.SysUser;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token json 序列化模块测试\n * \n * @author click33 \n *\n */\npublic class SaJsonTemplateTest {\n\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaJsonTemplateTest star ...\");\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaJsonTemplateTest end ... \\n\");\n    }\n\n    // 测试：DefaultImpl\n    @Test\n    public void testDefaultImpl() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateDefaultImpl());\n        Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateDefaultImpl.class);\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().objectToJson(user) );\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject(\"xxx\", SysUser.class) );\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject(\"xxx\") );\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToMap(\"xxx\") );\n    }\n\n    // 测试：Jackson3\n    @Test\n    public void testJackson3() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3());\n        Assertions.assertEquals(SaJsonTemplateForJackson3.class, SaManager.getSaJsonTemplate().getClass());\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectJson = SaManager.getSaJsonTemplate().objectToJson(user);\n        // 与 json2 不同点：Jackson 3 默认按字母序排列属性\n        Assertions.assertEquals(\"{\\\"@class\\\":\\\"com.pj.test.model.SysUser\\\",\\\"age\\\":18,\\\"id\\\":10001,\\\"name\\\":\\\"张三\\\",\\\"role\\\":null}\", objectJson);\n\n        // test   Json -> Object\n        SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson);\n        Assertions.assertEquals(user3.toString(), user.toString());\n\n        // more\n        testNull();\n        testMap();\n    }\n\n    // 测试 Map 的转换\n    private void testMap() {\n\n        // test   Map -> Json\n        Map<String, Object> map = new HashMap<>();\n        map.put(\"id\", 10001);\n        map.put(\"name\", \"张三\");\n        map.put(\"age\", 18);\n        String mapJson = SaManager.getSaJsonTemplate().objectToJson(map);\n        Assertions.assertEquals(\"{\\\"name\\\":\\\"张三\\\",\\\"id\\\":10001,\\\"age\\\":18}\", mapJson);\n\n        // test   Json -> Map\n        Map<String, Object> map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson);\n        Assertions.assertEquals(map2.toString(), map.toString());\n\n    }\n\n    // 测试 Null 值\n    private void testNull() {\n        Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null));\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysRole.java",
    "content": "package com.pj.test.model;\n\n/**\n * Role 实体类\n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysRole {\n//\n//\tpublic SysRole() {\n//\t}\n//\n//\tpublic SysRole(long id, String name) {\n//\t\tsuper();\n//\t\tthis.id = id;\n//\t\tthis.name = name;\n//\t}\n//\n//\n//\t/**\n//\t * 角色id\n//\t */\n//\tprivate long id;\n//\n//\t/**\n//\t * 角色名称\n//\t */\n//\tprivate String name;\n//\n//\t/**\n//\t * @return id\n//\t */\n//\tpublic long getId() {\n//\t\treturn id;\n//\t}\n//\n//\t/**\n//\t * @param id 要设置的 id\n//\t */\n//\tpublic void setId(long id) {\n//\t\tthis.id = id;\n//\t}\n//\n//\t/**\n//\t * @return name\n//\t */\n//\tpublic String getName() {\n//\t\treturn name;\n//\t}\n//\n//\t/**\n//\t * @param name 要设置的 name\n//\t */\n//\tpublic void setName(String name) {\n//\t\tthis.name = name;\n//\t}\n//\n//\t@Override\n//\tpublic String toString() {\n//\t\treturn \"SysRole [id=\" + id + \", name=\" + name + \"]\";\n//\t}\n//\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-easy-test/src/test/java/com/pj/test/model/SysUser.java",
    "content": "package com.pj.test.model;\n\n/**\n * User 实体类 \n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysUser {\n\n\tpublic SysUser() {\n\t}\n\t\n\tpublic SysUser(long id, String name, int age) {\n\t\tsuper();\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.age = age;\n\t}\n\t\n\n\t/**\n\t * 用户id\n\t */\n\tprivate long id;\n\t\n\t/**\n\t * 用户名称\n\t */\n\tprivate String name;\n\t\n\t/**\n\t * 用户年龄\n\t */\n\tprivate int age;\n\n\t/**\n\t * 用户角色\n\t */\n\tprivate SysRole role;\n\n\t/**\n\t * @return id\n\t */\n\tpublic long getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id 要设置的 id\n\t */\n\tpublic void setId(long id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return name\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 要设置的 name\n\t */\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return age\n\t */\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\t/**\n\t * @param age 要设置的 age\n\t */\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\tpublic SysRole getRole() {\n\t\treturn role;\n\t}\n\n\tpublic SysUser setRole(SysRole role) {\n\t\tthis.role = role;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SysUser{\" +\n\t\t\t\t\"id=\" + id +\n\t\t\t\t\", name='\" + name + '\\'' +\n\t\t\t\t\", age=\" + age +\n\t\t\t\t\", role=\" + role +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jackson3-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-test</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-jackson3-test</name>\n    <artifactId>sa-token-jackson3-test</artifactId>\n\t<description>sa-token-jackson3-test</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson3</artifactId>\n\t\t\t<version>${revision}</version>\n\t\t</dependency>\n\t\t<!-- Jackson 3（testJackson3 需要，sa-token-jackson3 中为 optional） -->\n\t\t<dependency>\n\t\t\t<groupId>tools.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t\t<version>3.0.0</version>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.datatype</groupId>\n\t\t\t<artifactId>jackson-datatype-jsr310</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/SaJsonTemplateForJackson3Test.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.json.SaJsonTemplateForJackson3;\nimport com.pj.test.model.SysUser;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token-jackson3 序列化模块测试\n *\n * <pre>\n * 为什么单独写一个模块来测试 Jackson 3 ？\n *\n * 在同一个项目里同时引入 jackson 2 和 jackson 3 后，\n * 执行：\n *      SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson);\n * 会报错：\n *      java.lang.NoSuchFieldError: POJO\n * \t        at tools.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:399)\n * \t        at tools.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:361)\n * \t        at tools.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:265)\n * \t        at tools.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)\n * \t        at tools.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:158)\n * \t        at tools.jackson.databind.DeserializationContext.findNonContextualValueDeserializer(DeserializationContext.java:733)\n * \t        at tools.jackson.databind.deser.jdk.UntypedObjectDeserializer._findCustomDeser(UntypedObjectDeserializer.java:179)\n * \t        at tools.jackson.databind.deser.jdk.UntypedObjectDeserializer.resolve(UntypedObjectDeserializer.java:152)\n *\n * 暂未找到解决方案，所以只能单独写一个测试类来测试 Jackson 3 的功能了。\n *\n * </pre>\n * @author click33 \n *\n */\npublic class SaJsonTemplateForJackson3Test {\n\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaJsonTemplateForJackson3 Test star ...\");\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaJsonTemplateForJackson3 Test end ... \\n\");\n    }\n\n    // 测试：Jackson3\n    @Test\n    public void testJackson3() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson3());\n        Assertions.assertEquals(SaJsonTemplateForJackson3.class, SaManager.getSaJsonTemplate().getClass());\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectJson = SaManager.getSaJsonTemplate().objectToJson(user);\n        // 与 json2 不同点：Jackson 3 默认按字母序排列属性\n        Assertions.assertEquals(\"{\\\"@class\\\":\\\"com.pj.test.model.SysUser\\\",\\\"age\\\":18,\\\"id\\\":10001,\\\"name\\\":\\\"张三\\\",\\\"role\\\":null}\", objectJson);\n\n        // test   Json -> Object\n        SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson);\n        Assertions.assertEquals(user3.toString(), user.toString());\n\n        // more\n        testNull();\n        testMap();\n    }\n\n    // 测试 Map 的转换\n    private void testMap() {\n\n        // test   Map -> Json\n        Map<String, Object> map = new HashMap<>();\n        map.put(\"id\", 10001);\n        map.put(\"name\", \"张三\");\n        map.put(\"age\", 18);\n        String mapJson = SaManager.getSaJsonTemplate().objectToJson(map);\n        Assertions.assertEquals(\"{\\\"name\\\":\\\"张三\\\",\\\"id\\\":10001,\\\"age\\\":18}\", mapJson);\n\n        // test   Json -> Map\n        Map<String, Object> map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson);\n        Assertions.assertEquals(map2.toString(), map.toString());\n\n    }\n\n    // 测试 Null 值\n    private void testNull() {\n        Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null));\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysRole.java",
    "content": "package com.pj.test.model;\n\n/**\n * Role 实体类\n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysRole {\n//\n//\tpublic SysRole() {\n//\t}\n//\n//\tpublic SysRole(long id, String name) {\n//\t\tsuper();\n//\t\tthis.id = id;\n//\t\tthis.name = name;\n//\t}\n//\n//\n//\t/**\n//\t * 角色id\n//\t */\n//\tprivate long id;\n//\n//\t/**\n//\t * 角色名称\n//\t */\n//\tprivate String name;\n//\n//\t/**\n//\t * @return id\n//\t */\n//\tpublic long getId() {\n//\t\treturn id;\n//\t}\n//\n//\t/**\n//\t * @param id 要设置的 id\n//\t */\n//\tpublic void setId(long id) {\n//\t\tthis.id = id;\n//\t}\n//\n//\t/**\n//\t * @return name\n//\t */\n//\tpublic String getName() {\n//\t\treturn name;\n//\t}\n//\n//\t/**\n//\t * @param name 要设置的 name\n//\t */\n//\tpublic void setName(String name) {\n//\t\tthis.name = name;\n//\t}\n//\n//\t@Override\n//\tpublic String toString() {\n//\t\treturn \"SysRole [id=\" + id + \", name=\" + name + \"]\";\n//\t}\n//\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jackson3-test/src/test/java/com/pj/test/model/SysUser.java",
    "content": "package com.pj.test.model;\n\n/**\n * User 实体类 \n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysUser {\n\n\tpublic SysUser() {\n\t}\n\t\n\tpublic SysUser(long id, String name, int age) {\n\t\tsuper();\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.age = age;\n\t}\n\t\n\n\t/**\n\t * 用户id\n\t */\n\tprivate long id;\n\t\n\t/**\n\t * 用户名称\n\t */\n\tprivate String name;\n\t\n\t/**\n\t * 用户年龄\n\t */\n\tprivate int age;\n\n\t/**\n\t * 用户角色\n\t */\n\tprivate SysRole role;\n\n\t/**\n\t * @return id\n\t */\n\tpublic long getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id 要设置的 id\n\t */\n\tpublic void setId(long id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return name\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 要设置的 name\n\t */\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return age\n\t */\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\t/**\n\t * @param age 要设置的 age\n\t */\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\tpublic SysRole getRole() {\n\t\treturn role;\n\t}\n\n\tpublic SysUser setRole(SysRole role) {\n\t\tthis.role = role;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SysUser{\" +\n\t\t\t\t\"id=\" + id +\n\t\t\t\t\", name='\" + name + '\\'' +\n\t\t\t\t\", age=\" + age +\n\t\t\t\t\", role=\" + role +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-json-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-test</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-json-test</name>\n    <artifactId>sa-token-json-test</artifactId>\n\t<description>sa-token-json-test</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jackson</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-fastjson</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-fastjson2</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-snack3</artifactId>\n\t\t</dependency>\n\n\t\t<!-- jackson more -->\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.datatype</groupId>\n\t\t\t<artifactId>jackson-datatype-jsr310</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-json-test/src/test/java/com/pj/test/SaJsonTemplateTest.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.exception.NotImplException;\nimport cn.dev33.satoken.json.*;\nimport com.pj.test.model.SysUser;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Sa-Token json 序列化模块测试\n * \n * @author click33 \n *\n */\npublic class SaJsonTemplateTest {\n\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaJsonTemplateTest star ...\");\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaJsonTemplateTest end ... \\n\");\n    }\n\n    // 测试：DefaultImpl\n    @Test\n    public void testDefaultImpl() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateDefaultImpl());\n        Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateDefaultImpl.class);\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().objectToJson(user) );\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject(\"xxx\", SysUser.class) );\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToObject(\"xxx\") );\n        Assertions.assertThrows(NotImplException.class, () -> SaManager.getSaJsonTemplate().jsonToMap(\"xxx\") );\n    }\n\n    // 测试：Jackson\n    @Test\n    public void testJackson() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForJackson());\n        Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForJackson.class);\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectJson = SaManager.getSaJsonTemplate().objectToJson(user);\n        Assertions.assertEquals(\"{\\\"@class\\\":\\\"com.pj.test.model.SysUser\\\",\\\"id\\\":10001,\\\"name\\\":\\\"张三\\\",\\\"age\\\":18,\\\"role\\\":null}\", objectJson);\n\n        // test   Json -> Object\n        SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        SysUser user3 = (SysUser)SaManager.getSaJsonTemplate().jsonToObject(objectJson);\n        Assertions.assertEquals(user3.toString(), user.toString());\n\n        // more\n        testNull();\n        testMap();\n    }\n\n    // 测试：Fastjson\n    @Test\n    public void testFastjson() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson());\n        Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForFastjson.class);\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectJson = SaManager.getSaJsonTemplate().objectToJson(user);\n        Assertions.assertEquals(\"{\\\"age\\\":18,\\\"id\\\":10001,\\\"name\\\":\\\"张三\\\"}\", objectJson);\n\n        // test   Json -> Object\n        SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n        testMap();\n    }\n\n    // 测试：Fastjson2\n    @Test\n    public void testFastjson2() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForFastjson2());\n        Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForFastjson2.class);\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectJson = SaManager.getSaJsonTemplate().objectToJson(user);\n        Assertions.assertEquals(\"{\\\"age\\\":18,\\\"id\\\":10001,\\\"name\\\":\\\"张三\\\"}\", objectJson);\n\n        // test   Json -> Object\n        SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n        testMap();\n    }\n\n    // 测试：Snack3\n    @Test\n    public void testSnack3() {\n        SaManager.setSaJsonTemplate(new SaJsonTemplateForSnack3());\n        Assertions.assertEquals(SaManager.getSaJsonTemplate().getClass(), SaJsonTemplateForSnack3.class);\n\n        // test   Object -> Json\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectJson = SaManager.getSaJsonTemplate().objectToJson(user);\n        Assertions.assertEquals(\"{\\\"id\\\":10001,\\\"name\\\":\\\"张三\\\",\\\"age\\\":18}\", objectJson);\n\n        // test   Json -> Object\n        SysUser user2 = SaManager.getSaJsonTemplate().jsonToObject(objectJson, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n        testMap();\n    }\n\n    // 测试 Map 的转换\n    private void testMap() {\n\n        // test   Map -> Json\n        Map<String, Object> map = new HashMap<>();\n        map.put(\"id\", 10001);\n        map.put(\"name\", \"张三\");\n        map.put(\"age\", 18);\n        String mapJson = SaManager.getSaJsonTemplate().objectToJson(map);\n        Assertions.assertEquals(\"{\\\"name\\\":\\\"张三\\\",\\\"id\\\":10001,\\\"age\\\":18}\", mapJson);\n\n        // test   Json -> Map\n        Map<String, Object> map2 = SaManager.getSaJsonTemplate().jsonToMap(mapJson);\n        Assertions.assertEquals(map2.toString(), map.toString());\n\n    }\n\n    // 测试 Null 值\n    private void testNull() {\n        Assertions.assertNull(SaManager.getSaJsonTemplate().objectToJson(null));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null, SysUser.class));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToObject(null));\n        Assertions.assertNull(SaManager.getSaJsonTemplate().jsonToMap(null));\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysRole.java",
    "content": "package com.pj.test.model;\n\n/**\n * Role 实体类\n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysRole {\n//\n//\tpublic SysRole() {\n//\t}\n//\n//\tpublic SysRole(long id, String name) {\n//\t\tsuper();\n//\t\tthis.id = id;\n//\t\tthis.name = name;\n//\t}\n//\n//\n//\t/**\n//\t * 角色id\n//\t */\n//\tprivate long id;\n//\n//\t/**\n//\t * 角色名称\n//\t */\n//\tprivate String name;\n//\n//\t/**\n//\t * @return id\n//\t */\n//\tpublic long getId() {\n//\t\treturn id;\n//\t}\n//\n//\t/**\n//\t * @param id 要设置的 id\n//\t */\n//\tpublic void setId(long id) {\n//\t\tthis.id = id;\n//\t}\n//\n//\t/**\n//\t * @return name\n//\t */\n//\tpublic String getName() {\n//\t\treturn name;\n//\t}\n//\n//\t/**\n//\t * @param name 要设置的 name\n//\t */\n//\tpublic void setName(String name) {\n//\t\tthis.name = name;\n//\t}\n//\n//\t@Override\n//\tpublic String toString() {\n//\t\treturn \"SysRole [id=\" + id + \", name=\" + name + \"]\";\n//\t}\n//\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-json-test/src/test/java/com/pj/test/model/SysUser.java",
    "content": "package com.pj.test.model;\n\n/**\n * User 实体类 \n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysUser {\n\n\tpublic SysUser() {\n\t}\n\t\n\tpublic SysUser(long id, String name, int age) {\n\t\tsuper();\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.age = age;\n\t}\n\t\n\n\t/**\n\t * 用户id\n\t */\n\tprivate long id;\n\t\n\t/**\n\t * 用户名称\n\t */\n\tprivate String name;\n\t\n\t/**\n\t * 用户年龄\n\t */\n\tprivate int age;\n\n\t/**\n\t * 用户角色\n\t */\n\tprivate SysRole role;\n\n\t/**\n\t * @return id\n\t */\n\tpublic long getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id 要设置的 id\n\t */\n\tpublic void setId(long id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return name\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 要设置的 name\n\t */\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return age\n\t */\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\t/**\n\t * @param age 要设置的 age\n\t */\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\tpublic SysRole getRole() {\n\t\treturn role;\n\t}\n\n\tpublic SysUser setRole(SysRole role) {\n\t\tthis.role = role;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SysUser{\" +\n\t\t\t\t\"id=\" + id +\n\t\t\t\t\", name='\" + name + '\\'' +\n\t\t\t\t\", age=\" + age +\n\t\t\t\t\", role=\" + role +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jwt-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-test</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-jwt-test</name>\n    <artifactId>sa-token-jwt-test</artifactId>\n\t<description>sa-token-jwt-test</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-jwt</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForMixinTest.java",
    "content": "package com.pj.test;\n\nimport java.util.List;\n\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport org.junit.jupiter.api.*;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.ApiDisabledException;\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.jwt.SaJwtUtil;\nimport cn.dev33.satoken.jwt.StpLogicJwtForMixin;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.stp.SaLoginConfig;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.jwt.JWT;\n\n/**\n * Sa-Token 整合 jwt：mixin 模式 测试 \n * \n * @author click33 \n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class JwtForMixinTest {\n\n\t// 持久化Bean \n\t@Autowired(required = false)\n\tSaTokenDao dao = SaManager.getSaTokenDao();\n\t\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ JwtForMixinTest star ...\");\n    \tStpUtil.setStpLogic(new StpLogicJwtForMixin());\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ JwtForMixinTest end ... \\n\");\n    }\n\n\t@BeforeEach\n\tpublic void beforeEach() {\n\t\tSaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse());\n\t}\n\n\t@AfterEach\n\tpublic void afterEach() {\n\t\tSaTokenContextServletUtil.clearContext();\n\t}\n\n    // 测试：登录 \n    @Test\n    public void doLogin() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// API 验证 \n    \tAssertions.assertTrue(StpUtil.isLogin());\t\n    \tAssertions.assertNotNull(token);\t// token不为null\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);\t// 登录设备类型\n\n    \t// token 验证 \n    \tJWT jwt = JWT.of(token);\n    \tJSONObject payloads = jwt.getPayloads();\n    \tAssertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_ID), \"10001\"); // 账号 \n    \tAssertions.assertEquals(payloads.getStr(SaJwtUtil.DEVICE_TYPE), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);  // 登录设备类型\n    \tAssertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_TYPE), StpUtil.TYPE);  // 账号类型 \n    \t\n    \t// db数据 验证  \n    \t// token不存在 \n    \tAssertions.assertNull(dao.get(\"satoken:login:token:\" + token));\n    \t// Session 存在 \n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNotNull(session);\n    \tAssertions.assertEquals(session.getId(), \"satoken:login:session:\" + 10001);\n    \tAssertions.assertTrue(session.getTerminalList().size() >= 1);\n    }\n    \n    // 测试：注销 \n    @Test\n    public void logout() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tString token = StpUtil.getTokenValue();\n    \tAssertions.assertEquals(JWT.of(token).getPayloads().getStr(\"loginId\"), \"10001\");\n    \t\n    \t// 注销\n    \tStpUtil.logout();\n    \t// token 应该被清除\n    \tAssertions.assertNull(StpUtil.getTokenValue());\n    \tAssertions.assertFalse(StpUtil.isLogin());\n    }\n    \n    // 测试：Session会话 \n    @Test\n    public void testSession() {\n    \tStpUtil.login(10001);\n    \t\n    \t// API 应该可以获取 Session \n    \tAssertions.assertNotNull(StpUtil.getSession(false));\n    \t\n    \t// db中应该存在 Session\n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNotNull(session);\n    \t\n    \t// 存取值 \n    \tsession.set(\"name\", \"zhang\");\n    \tsession.set(\"age\", \"18\");\n    \tAssertions.assertEquals(session.get(\"name\"), \"zhang\");\n    \tAssertions.assertEquals(session.getInt(\"age\"), 18);\n    \tAssertions.assertEquals((int)session.getModel(\"age\", int.class), 18);\n    \tAssertions.assertEquals((int)session.get(\"age\", 20), 18);\n    \tAssertions.assertEquals((int)session.get(\"name2\", 20), 20);\n    \tAssertions.assertEquals((int)session.get(\"name2\", () -> 30), 30);\n    \tsession.clear();\n    \tAssertions.assertEquals(session.get(\"name\"), null);\n    }\n    \n    // 测试：权限认证 \n    @Test\n    public void testCheckPermission() {\n    \tStpUtil.login(10001);\n    \t\n    \t// 权限认证 \n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user-add\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user-list\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"art-add\"));\n    \tAssertions.assertFalse(StpUtil.hasPermission(\"get-user\"));\n    \t// and\n    \tAssertions.assertTrue(StpUtil.hasPermissionAnd(\"art-add\", \"art-get\"));\n    \tAssertions.assertFalse(StpUtil.hasPermissionAnd(\"art-add\", \"comment-add\"));\n    \t// or \n    \tAssertions.assertTrue(StpUtil.hasPermissionOr(\"art-add\", \"comment-add\"));\n    \tAssertions.assertFalse(StpUtil.hasPermissionOr(\"comment-add\", \"comment-delete\"));\n    }\n\n    // 测试：角色认证\n    @Test\n    public void testCheckRole() {\n    \tStpUtil.login(10001);\n    \t\n    \t// 角色认证 \n    \tAssertions.assertTrue(StpUtil.hasRole(\"admin\")); \n    \tAssertions.assertFalse(StpUtil.hasRole(\"teacher\")); \n    \t// and\n    \tAssertions.assertTrue(StpUtil.hasRoleAnd(\"admin\", \"super-admin\")); \n    \tAssertions.assertFalse(StpUtil.hasRoleAnd(\"admin\", \"ceo\")); \n    \t// or\n    \tAssertions.assertTrue(StpUtil.hasRoleOr(\"admin\", \"ceo\")); \n    \tAssertions.assertFalse(StpUtil.hasRoleOr(\"ceo\", \"cto\")); \n    }\n\t\n    // 测试：根据token强制注销 \n    @Test\n    public void testLogoutByToken() {\n\t\tAssertions.assertThrows(ApiDisabledException.class, () -> {\n\t    \t// 先登录上 \n\t    \tStpUtil.login(10001); \n\t    \tAssertions.assertTrue(StpUtil.isLogin());\t\n\t    \tString token = StpUtil.getTokenValue();\n\t    \t\n\t    \t// 根据token注销 \n\t    \tStpUtil.logoutByTokenValue(token); \n    \t});\n    }\n\n    // 测试：根据账号id强制注销 \n    @Test\n    public void testLogoutByLoginId() {\n\t\tAssertions.assertThrows(ApiDisabledException.class, () -> {\n\t    \t// 先登录上 \n\t    \tStpUtil.login(10001); \n\t    \tAssertions.assertTrue(StpUtil.isLogin());\t\n\t    \t\n\t    \t// 根据账号id注销 \n\t    \tStpUtil.logout(10001);\n    \t});\n    }\n\n    // 测试Token-Session \n    @Test\n    public void testTokenSession() {\n\n    \t// 先登录上 \n    \tStpUtil.login(10001); \n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// 刚开始不存在 \n    \tAssertions.assertNull(StpUtil.stpLogic.getTokenSession(false));\n    \tSaSession session = dao.getSession(\"satoken:login:token-session:\" + token);\n    \tAssertions.assertNull(session);\n    \t\n    \t// 调用一次就存在了\n    \tStpUtil.getTokenSession();\n    \tAssertions.assertNotNull(StpUtil.stpLogic.getTokenSession(false));\n    \tSaSession session2 = dao.getSession(\"satoken:login:token-session:\" + token);\n    \tAssertions.assertNotNull(session2);\n    }\n    \n    // 测试：账号封禁 \n    @Test\n    public void testDisable() {\n    \tAssertions.assertThrows(DisableServiceException.class, () -> {\n        \t// 封号 \n        \tStpUtil.disable(10007, 200);\n        \tAssertions.assertTrue(StpUtil.isDisable(10007));\n        \tAssertions.assertEquals(dao.get(\"satoken:login:disable:login:\" + 10007), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); \n        \t\n        \t// 解封  \n        \tStpUtil.untieDisable(10007);\n        \tAssertions.assertFalse(StpUtil.isDisable(10007));\n        \tAssertions.assertEquals(dao.get(\"satoken:login:disable:login:\" + 10007), null); \n        \t\n        \t// 封号后校验 (会抛出 DisableLoginException 异常)\n        \tStpUtil.disable(10007, 200); \n        \tStpUtil.checkDisable(10007);\n        \tStpUtil.login(10007);  \n    \t});\n    }\n\n    // 测试：身份切换 \n    @Test\n    public void testSwitch() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tAssertions.assertFalse(StpUtil.isSwitch());\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\n    \t\n    \t// 开始身份切换 \n    \tStpUtil.switchTo(10044);\n    \tAssertions.assertTrue(StpUtil.isSwitch());\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10044);\n    \t\n    \t// 结束切换 \n    \tStpUtil.endSwitch(); \n    \tAssertions.assertFalse(StpUtil.isSwitch());\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\n    }\n    \n    // 测试：会话管理\n    @Test\n    public void testSearchTokenValue() {\n    \tAssertions.assertThrows(ApiDisabledException.class, () -> {\n        \t// 登录\n        \tStpUtil.login(10001);\n        \tStpUtil.login(10002);\n        \tStpUtil.login(10003);\n        \tStpUtil.login(10004);\n        \tStpUtil.login(10005);\n        \t\n        \t// 查询 \n        \tList<String> list = StpUtil.searchTokenValue(\"\", 0, 10, true);\n        \tAssertions.assertTrue(list.size() >= 5);\n    \t});\n    }\n\n    // 测试：getExtra \n    @Test\n    public void getExtra() {\n    \t// 登录\n    \tStpUtil.login(10001, SaLoginConfig.setExtra(\"name\", \"zhangsan\"));\n    \tString tokenValue = StpUtil.getTokenValue();\n    \t\n    \t// 可以取到\n    \tAssertions.assertEquals(StpUtil.getExtra(\"name\"), \"zhangsan\");\n    \tAssertions.assertEquals(StpUtil.getExtra(tokenValue, \"name\"), \"zhangsan\");\n    \t\n    \t// 取不到 \n    \tAssertions.assertEquals(StpUtil.getExtra(\"name2\"), null);\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForSimpleTest.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport org.junit.jupiter.api.*;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.jwt.StpLogicJwtForSimple;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.stp.SaLoginConfig;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.jwt.JWT;\n\n/**\n * Sa-Token 整合 jwt：Simple 模式 测试\n * \n * @author click33 \n *\n */\n//@RunWith(SpringRunner.class)\n@SpringBootTest(classes = StartUpApplication.class)\npublic class JwtForSimpleTest {\n\n\t// 持久化Bean \n\tstatic SaTokenDao dao;\n\t\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ JwtForSimpleTest star ...\");\n    \tdao = SaManager.getSaTokenDao();\n    \tStpUtil.setStpLogic(new StpLogicJwtForSimple());\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ JwtForSimpleTest end ... \\n\");\n    }\n\n\t@BeforeEach\n\tpublic void beforeEach() {\n\t\tSaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse());\n\t}\n\n\t@AfterEach\n\tpublic void afterEach() {\n\t\tSaTokenContextServletUtil.clearContext();\n\t}\n\n    // 测试：登录 \n    @Test\n    public void doLogin() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// API 验证 \n    \tAssertions.assertTrue(StpUtil.isLogin());\t\n    \tAssertions.assertNotNull(token);\t// token不为null\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);\t// 登录设备类型\n\n    \t// token 验证 \n    \tJWT jwt = JWT.of(token);\n    \tJSONObject payloads = jwt.getPayloads();\n    \tAssertions.assertEquals(payloads.getStr(\"loginId\"), \"10001\");\n    \t\n    \t// db数据 验证  \n    \t// token存在 \n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token), \"10001\");\n    \t// Session 存在 \n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNotNull(session);\n    \tAssertions.assertEquals(session.getId(), \"satoken:login:session:\" + 10001);\n    \tAssertions.assertTrue(session.getTerminalList().size() >= 1);\n    }\n\n    // 测试：getExtra \n    @Test\n    public void getExtra() {\n    \t// 登录\n    \tStpUtil.login(10001, SaLoginConfig.setExtra(\"name\", \"zhangsan\"));\n    \tString tokenValue = StpUtil.getTokenValue();\n    \t\n    \t// 可以取到\n    \tAssertions.assertEquals(StpUtil.getExtra(\"name\"), \"zhangsan\");\n    \tAssertions.assertEquals(StpUtil.getExtra(tokenValue, \"name\"), \"zhangsan\");\n    \t// 取不到 \n    \tAssertions.assertEquals(StpUtil.getExtra(\"name2\"), null);\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/JwtForStatelessTest.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport org.junit.jupiter.api.*;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.ApiDisabledException;\nimport cn.dev33.satoken.jwt.SaJwtUtil;\nimport cn.dev33.satoken.jwt.StpLogicJwtForStateless;\nimport cn.dev33.satoken.stp.SaLoginConfig;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport cn.hutool.json.JSONObject;\nimport cn.hutool.jwt.JWT;\n\n/**\n * Sa-Token 整合 jwt：stateless 模式 测试 \n * \n * @author click33 \n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class JwtForStatelessTest {\n\n\t// 持久化Bean \n\t@Autowired(required = false)\n\tSaTokenDao dao = SaManager.getSaTokenDao();\n\t\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ JwtForStatelessTest star ...\");\n    \tStpUtil.setStpLogic(new StpLogicJwtForStateless());\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ JwtForStatelessTest end ... \\n\");\n    }\n\n\t@BeforeEach\n\tpublic void beforeEach() {\n\t\tSaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse());\n\t}\n\n\t@AfterEach\n\tpublic void afterEach() {\n\t\tSaTokenContextServletUtil.clearContext();\n\t}\n\n    // 测试：登录 \n    @Test\n    public void doLogin() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// API 验证 \n    \tAssertions.assertTrue(StpUtil.isLogin());\t\n    \tAssertions.assertNotNull(token);\t// token不为null\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);\t// 登录设备类型\n\n    \t// token 验证 \n    \tJWT jwt = JWT.of(token);\n    \tJSONObject payloads = jwt.getPayloads();\n    \tAssertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_ID), \"10001\"); // 账号 \n    \tAssertions.assertEquals(payloads.getStr(SaJwtUtil.DEVICE_TYPE), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);  // 登录设备类型\n    \tAssertions.assertEquals(payloads.getStr(SaJwtUtil.LOGIN_TYPE), StpUtil.TYPE);  // 账号类型 \n    \t\n    \t// 时间 \n    \tAssertions.assertTrue(StpUtil.getTokenTimeout() <= SaManager.getConfig().getTimeout());\n    \tAssertions.assertTrue(StpUtil.getTokenTimeout() > SaManager.getConfig().getTimeout() - 10000);\n    \t\n    \ttry {\n\t\t\t// 尝试获取Session会抛出异常 \n\t\t\tStpUtil.getSession();\n\t\t\tAssertions.assertTrue(false);\n\t\t} catch (Exception e) {\n\t\t}\n    }\n    \n    // 测试：注销 \n    @Test\n    public void logout() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tString token = StpUtil.getTokenValue();\n    \tAssertions.assertEquals(JWT.of(token).getPayloads().getStr(\"loginId\"), \"10001\");\n    \t\n    \t// 注销\n    \tStpUtil.logout();\n\n    \t// token 应该被清除 \n    \tAssertions.assertNull(StpUtil.getTokenValue());\n    \tAssertions.assertFalse(StpUtil.isLogin());\n    }\n    \n    // 测试：Session会话 \n    @Test\n    public void testSession() {\n    \tAssertions.assertThrows(ApiDisabledException.class, () -> {\n        \tStpUtil.login(10001);\n        \t\n        \t// 会抛异常 \n        \tStpUtil.getSession();\n    \t});\n    }\n    \n    // 测试：权限认证 \n    @Test\n    public void testCheckPermission() {\n    \tStpUtil.login(10001);\n    \t\n    \t// 权限认证 \n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user-add\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user-list\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"art-add\"));\n    \tAssertions.assertFalse(StpUtil.hasPermission(\"get-user\"));\n    \t// and\n    \tAssertions.assertTrue(StpUtil.hasPermissionAnd(\"art-add\", \"art-get\"));\n    \tAssertions.assertFalse(StpUtil.hasPermissionAnd(\"art-add\", \"comment-add\"));\n    \t// or \n    \tAssertions.assertTrue(StpUtil.hasPermissionOr(\"art-add\", \"comment-add\"));\n    \tAssertions.assertFalse(StpUtil.hasPermissionOr(\"comment-add\", \"comment-delete\"));\n    }\n\n    // 测试：角色认证\n    @Test\n    public void testCheckRole() {\n    \tStpUtil.login(10001);\n    \t\n    \t// 角色认证 \n    \tAssertions.assertTrue(StpUtil.hasRole(\"admin\")); \n    \tAssertions.assertFalse(StpUtil.hasRole(\"teacher\")); \n    \t// and\n    \tAssertions.assertTrue(StpUtil.hasRoleAnd(\"admin\", \"super-admin\")); \n    \tAssertions.assertFalse(StpUtil.hasRoleAnd(\"admin\", \"ceo\")); \n    \t// or\n    \tAssertions.assertTrue(StpUtil.hasRoleOr(\"admin\", \"ceo\")); \n    \tAssertions.assertFalse(StpUtil.hasRoleOr(\"ceo\", \"cto\")); \n    }\n\t\n    // 测试：根据token强制注销 \n    @Test\n    public void testLogoutByToken() {\n    \tAssertions.assertThrows(ApiDisabledException.class, () -> {\n        \t// 先登录上 \n        \tStpUtil.login(10001); \n        \tAssertions.assertTrue(StpUtil.isLogin());\t\n        \tString token = StpUtil.getTokenValue();\n        \t\n        \t// 根据token注销 \n        \tStpUtil.logoutByTokenValue(token); \n    \t});\n    }\n\n    // 测试：根据账号id强制注销 \n    @Test\n    public void testLogoutByLoginId() {\n    \tAssertions.assertThrows(ApiDisabledException.class, () -> {\n        \t// 先登录上 \n        \tStpUtil.login(10001); \n        \tAssertions.assertTrue(StpUtil.isLogin());\t\n        \t\n        \t// 根据账号id注销 \n        \tStpUtil.logout(10001);\n    \t});\n    }\n\n    // 测试：getExtra \n    @Test\n    public void getExtra() {\n    \t// 登录\n    \tStpUtil.login(10001, SaLoginConfig.setExtra(\"name\", \"zhangsan\"));\n    \tString tokenValue = StpUtil.getTokenValue();\n    \t\n    \t// 可以取到\n    \tAssertions.assertEquals(StpUtil.getExtra(\"name\"), \"zhangsan\");\n    \tAssertions.assertEquals(StpUtil.getExtra(tokenValue, \"name\"), \"zhangsan\");\n    \t\n    \t// 取不到 \n    \tAssertions.assertEquals(StpUtil.getExtra(\"name2\"), null);\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/StartUpApplication.java",
    "content": "package com.pj.test;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * 启动类 \n * @author Auster\n *\n */\n@SpringBootApplication\npublic class StartUpApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(StartUpApplication.class, args);\n\t}\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jwt-test/src/test/java/com/pj/test/satoken/StpInterfaceImpl.java",
    "content": "package com.pj.test.satoken;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\n\n/**\n * 自定义权限验证接口扩展 \n * \n * @author Auster\n *\n */\n@Component\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\treturn Arrays.asList(\"user*\", \"art-add\", \"art-delete\", \"art-update\", \"art-get\");\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\treturn Arrays.asList(\"admin\", \"super-admin\");\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-jwt-test/src/test/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # jwt秘钥 \n    jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间（毫秒）\n        timeout: 10000ms\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        "
  },
  {
    "path": "sa-token-test/sa-token-serializer-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-test</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-serializer-test</name>\n    <artifactId>sa-token-serializer-test</artifactId>\n\t<description>sa-token-serializer-test</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-serializer-features</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/SaSerializerTemplateTest.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.serializer.SaSerializerForBase64UseEmoji;\nimport cn.dev33.satoken.serializer.SaSerializerForBase64UsePeriodicTable;\nimport cn.dev33.satoken.serializer.SaSerializerForBase64UseSpecialSymbols;\nimport cn.dev33.satoken.serializer.SaSerializerForBase64UseTianGan;\nimport cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseBase64;\nimport cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseHex;\nimport cn.dev33.satoken.serializer.impl.SaSerializerTemplateForJdkUseISO_8859_1;\nimport com.pj.test.model.SysUser;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Sa-Token Serializer 序列化模块测试\n * \n * @author click33 \n *\n */\npublic class SaSerializerTemplateTest {\n\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaSerializerTemplateTest star ...\");\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaSerializerTemplateTest end ... \\n\");\n    }\n\n    // 测试：SaSerializerTemplateForJdkUseBase64\n    @Test\n    public void testSaSerializerTemplateForJdkUseBase64() {\n        SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseBase64());\n        Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseBase64.class);\n\n        // test   Object -> String\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectString = SaManager.getSaSerializerTemplate().objectToString(user);\n        Assertions.assertEquals(\"rO0ABXNyABljb20ucGoudGVzdC5tb2RlbC5TeXNVc2Vy0MeZoPBtVUwCAARJAANhZ2VKAAJpZEwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztMAARyb2xldAAbTGNvbS9wai90ZXN0L21vZGVsL1N5c1JvbGU7eHAAAAASAAAAAAAAJxF0AAblvKDkuIlw\", objectString);\n\n        // test   String -> Object\n        SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n    }\n\n    // 测试：SaSerializerTemplateForJdkUseHex\n    @Test\n    public void testSaSerializerTemplateForJdkUseHex() {\n        SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseHex());\n        Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseHex.class);\n\n        // test   Object -> String\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectString = SaManager.getSaSerializerTemplate().objectToString(user);\n        Assertions.assertEquals(\"ACED000573720019636F6D2E706A2E746573742E6D6F64656C2E53797355736572D0C799A0F06D554C0200044900036167654A000269644C00046E616D657400124C6A6176612F6C616E672F537472696E673B4C0004726F6C6574001B4C636F6D2F706A2F746573742F6D6F64656C2F537973526F6C653B7870000000120000000000002711740006E5BCA0E4B88970\", objectString);\n\n        // test   String -> Object\n        SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n    }\n\n    // 测试：SaSerializerTemplateForJdkUseISO_8859_1\n    @Test\n    public void testSaSerializerTemplateForJdkUseISO_8859_1() {\n        SaManager.setSaSerializerTemplate(new SaSerializerTemplateForJdkUseISO_8859_1());\n        Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerTemplateForJdkUseISO_8859_1.class);\n\n        // test   Object -> String\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectString = SaManager.getSaSerializerTemplate().objectToString(user);\n        // Assertions.assertEquals(\"xxxx\", objectString); // 太过奇形怪状，无法直接断言\n\n        // test   String -> Object\n        SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n    }\n\n    // 测试：SaSerializerForBase64UseTianGan\n    @Test\n    public void testSaSerializerForBase64UseTianGan() {\n        SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan());\n        Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseTianGan.class);\n\n        // test   Object -> String\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectString = SaManager.getSaSerializerTemplate().objectToString(user);\n        Assertions.assertEquals(\"雷辰中甲乙坤卯西甲乙日天离谷中雾艮庚石雾兑庚亥北兑丙宙霜离谷未日离丙宙酉金坤卯亥艮谷亥西中寅金巽石巳乙霜亥戌东丙甲甲未癸甲甲卯火巽谷亥子甲甲癸田巽戊东甲乙庚宙火离乾亥中甲乙癸寅坎月己谷震申安电震乾宙山丑信卯中艮月日雾巽北霜寅甲甲未西离谷南日兑甲甲离酉庚卯露离申安东坎土安中巽坤卯中丑谷信露巽庚亥电丑信卯宙艮信癸露离庚戌泰金辛甲甲甲甲甲申甲甲甲甲甲甲甲甲癸南己中甲甲离日露子丁地雾壬日东\", objectString);\n\n        // test   String -> Object\n        SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n    }\n\n    // 测试：SaSerializerForBase64UsePeriodicTable\n    @Test\n    public void testSaSerializerForBase64UsePeriodicTable() {\n        SaManager.setSaSerializerTemplate(new SaSerializerForBase64UsePeriodicTable());\n        Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UsePeriodicTable.class);\n\n        // test   Object -> String\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectString = SaManager.getSaSerializerTemplate().objectToString(user);\n        Assertions.assertEquals(\"钌磷碘氢氦铬硅锑氢氦锶氪镍铯碘银铜氮铌银锌氮钛碲锌锂铈钯镍铯氩锶镍锂铈钙镓铬硅钛铜铯钛锑碘铝镓铁铌硫氦钯钛钪铟锂氢氢氩氖氢氢硅硒铁铯钛钠氢氢氖钼铁硼铟氢氦氮铈硒镍钒钛碘氢氦氖铝钴钇碳铯锰钾钐铑锰钒铈锆镁氙硅碘铜钇锶银铁碲钯铝氢氢氩锑镍铯锡锶锌氢氢镍钙氮硅镉镍钾钐铟钴溴钐碘铁铬硅碘镁铯氙镉铁氮钛铑镁氙硅铈铜氙氖镉镍氮钪钕镓氧氢氢氢氢氢钾氢氢氢氢氢氢氢氢氖锡碳碘氢氢镍锶镉钠铍铷银氟锶铟\", objectString);\n\n        // test   String -> Object\n        SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n    }\n\n    // 测试：SaSerializerForBase64UseSpecialSymbols\n    @Test\n    public void testSaSerializerForBase64UseSpecialSymbols() {\n        SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseSpecialSymbols());\n        Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseSpecialSymbols.class);\n\n        // test   Object -> String\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectString = SaManager.getSaSerializerTemplate().objectToString(user);\n        Assertions.assertEquals(\"→▃☶▲▼▌▂☳▲▼§♫▬☰☶↘〓▶↑↘◤▶▎☱◤●☀↓▬☰▆§▬●☀█◥▌▂▎〓☰▎☳☶▁◥▊↑▄▼↓▎▏☲●▲▲▆♥▲▲▂♩▊☰▎♦▲▲♥↗▊■☲▲▼▶☀♩▬▍▎☶▲▼♥▁▉〼★☰▋▇‥↙▋▍☀↖♣☵▂☶〓〼§↘▊☱↓▁▲▲▆☳▬☰☷§◤▲▲▬█▶▂☴▬▇‥☲▉♪‥☶▊▌▂☶♣☰☵☴▊▶▎↙♣☵▂☀〓☵♥☴▬▶▏▪◥◀▲▲▲▲▲▇▲▲▲▲▲▲▲▲♥☷★☶▲▲▬§☴♦◆♬↘♠§☲\", objectString);\n\n        // test   String -> Object\n        SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n    }\n\n    // 测试：SaSerializerForBase64UseEmoji\n    @Test\n    public void testSaSerializerForBase64UseEmoji() {\n        SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseEmoji());\n        Assertions.assertEquals(SaManager.getSaSerializerTemplate().getClass(), SaSerializerForBase64UseEmoji.class);\n\n        // test   Object -> String\n        SysUser user = new SysUser(10001, \"张三\", 18);\n        String objectString = SaManager.getSaSerializerTemplate().objectToString(user);\n        Assertions.assertEquals(\"😫😎😴😀😁😗😍😲😀😁😥😣😛😶😴😮😜😆😨😮😝😆😕😳😝😂😹😭😛😶😑😥😛😂😹😓😞😗😍😕😜😶😕😲😴😌😞😙😨😏😁😭😕😔😰😂😀😀😑😉😀😀😍😡😙😶😕😊😀😀😉😩😙😄😰😀😁😆😹😡😛😖😕😴😀😁😉😌😚😦😅😶😘😒😽😬😘😖😹😧😋😵😍😴😜😦😥😮😙😳😭😌😀😀😑😲😛😶😱😥😝😀😀😛😓😆😍😯😛😒😽😰😚😢😽😴😙😗😍😴😋😶😵😯😙😆😕😬😋😵😍😹😜😵😉😯😛😆😔😻😞😇😀😀😀😀😀😒😀😀😀😀😀😀😀😀😉😱😅😴😀😀😛😥😯😊😃😤😮😈😥😰\", objectString);\n\n        // test   String -> Object\n        SysUser user2 = SaManager.getSaSerializerTemplate().stringToObject(objectString, SysUser.class);\n        Assertions.assertEquals(user2.toString(), user.toString());\n\n        // more\n        testNull();\n    }\n\n    // 测试 Null 值\n    private void testNull() {\n        Assertions.assertNull(SaManager.getSaSerializerTemplate().objectToString(null));\n        Assertions.assertNull(SaManager.getSaSerializerTemplate().stringToObject(null, SysUser.class));\n        Assertions.assertNull(SaManager.getSaSerializerTemplate().stringToObject(null));\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysRole.java",
    "content": "package com.pj.test.model;\n\nimport java.io.Serializable;\n\n/**\n * Role 实体类\n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysRole implements Serializable {\n//\n//\tpublic SysRole() {\n//\t}\n//\n//\tpublic SysRole(long id, String name) {\n//\t\tsuper();\n//\t\tthis.id = id;\n//\t\tthis.name = name;\n//\t}\n//\n//\n//\t/**\n//\t * 角色id\n//\t */\n//\tprivate long id;\n//\n//\t/**\n//\t * 角色名称\n//\t */\n//\tprivate String name;\n//\n//\t/**\n//\t * @return id\n//\t */\n//\tpublic long getId() {\n//\t\treturn id;\n//\t}\n//\n//\t/**\n//\t * @param id 要设置的 id\n//\t */\n//\tpublic void setId(long id) {\n//\t\tthis.id = id;\n//\t}\n//\n//\t/**\n//\t * @return name\n//\t */\n//\tpublic String getName() {\n//\t\treturn name;\n//\t}\n//\n//\t/**\n//\t * @param name 要设置的 name\n//\t */\n//\tpublic void setName(String name) {\n//\t\tthis.name = name;\n//\t}\n//\n//\t@Override\n//\tpublic String toString() {\n//\t\treturn \"SysRole [id=\" + id + \", name=\" + name + \"]\";\n//\t}\n//\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-serializer-test/src/test/java/com/pj/test/model/SysUser.java",
    "content": "package com.pj.test.model;\n\nimport java.io.Serializable;\n\n/**\n * User 实体类 \n * \n * @author click33\n * @since 2022-10-15\n */\npublic class SysUser implements Serializable {\n\n\tpublic SysUser() {\n\t}\n\t\n\tpublic SysUser(long id, String name, int age) {\n\t\tsuper();\n\t\tthis.id = id;\n\t\tthis.name = name;\n\t\tthis.age = age;\n\t}\n\t\n\n\t/**\n\t * 用户id\n\t */\n\tprivate long id;\n\t\n\t/**\n\t * 用户名称\n\t */\n\tprivate String name;\n\t\n\t/**\n\t * 用户年龄\n\t */\n\tprivate int age;\n\n\t/**\n\t * 用户角色\n\t */\n\tprivate SysRole role;\n\n\t/**\n\t * @return id\n\t */\n\tpublic long getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id 要设置的 id\n\t */\n\tpublic void setId(long id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return name\n\t */\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name 要设置的 name\n\t */\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return age\n\t */\n\tpublic int getAge() {\n\t\treturn age;\n\t}\n\n\t/**\n\t * @param age 要设置的 age\n\t */\n\tpublic void setAge(int age) {\n\t\tthis.age = age;\n\t}\n\n\tpublic SysRole getRole() {\n\t\treturn role;\n\t}\n\n\tpublic SysUser setRole(SysRole role) {\n\t\tthis.role = role;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"SysUser{\" +\n\t\t\t\t\"id=\" + id +\n\t\t\t\t\", name='\" + name + '\\'' +\n\t\t\t\t\", age=\" + age +\n\t\t\t\t\", role=\" + role +\n\t\t\t\t'}';\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-test</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-springboot-test</name>\n    <artifactId>sa-token-springboot-test</artifactId>\n\t<description>sa-token-springboot-test</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n        </dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-sso</artifactId>\n            <scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-oauth2</artifactId>\n            <scope>test</scope>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-sign</artifactId>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t\t<!-- 冗余（生成单元测试报告） -->\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-servlet</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-core</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/application/SaApplicationTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.application;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.application.SaApplication;\nimport cn.dev33.satoken.context.SaHolder;\n\n/**\n * SaApplication 存取值测试 \n * \n * @author click33\n * @since 2022-9-4\n */\npublic class SaApplicationTest {\n\n\t// 测试 \n\t@Test\n\tpublic void testSaApplication() {\n\t\tSaApplication application = SaHolder.getApplication();\n\t\t\n\t\t// 取值 \n\t\tapplication.set(\"age\", \"18\");\n\t\tAssertions.assertEquals(application.get(\"age\").toString(), \"18\");\n\t\tAssertions.assertEquals(application.getInt(\"age\"), 18);\n\t\tAssertions.assertEquals(application.getLong(\"age\"), 18L);\n\t\tAssertions.assertEquals(application.getFloat(\"age\"), 18f);\n\t\tAssertions.assertEquals(application.getDouble(\"age\"), 18.0);\n\t\tAssertions.assertEquals(application.getString(\"age\"), \"18\");\n\t\tAssertions.assertEquals(application.get(\"age\", 20), 18);\n\t\tAssertions.assertEquals(application.get(\"age2\", 20), 20);\n\t\tAssertions.assertEquals(application.getString(\"age2\"), null);\n\t\t// lambda 取值，有值时依然是原值 \n\t\tAssertions.assertEquals(application.get(\"age\", () -> \"23\"), \"18\");\n\t\tAssertions.assertEquals(application.getInt(\"age\"), 18);\n\t\t// lambda 取值，无值时被写入新值 \n\t\tAssertions.assertEquals(application.get(\"age2\", () -> \"23\"), \"23\");\n\t\tAssertions.assertEquals(application.getInt(\"age2\"), 23);\n\n\t\t// getModel取值 \n\t\tAssertions.assertEquals(application.getModel(\"age\", int.class), 18);\n\t\tAssertions.assertEquals(application.getModel(\"age\", int.class, 30), 18);\n\t\tAssertions.assertEquals(application.getModel(\"age3\", int.class, 30), 30);\n\t\t\n\t\t// 删除值 \n\t\tapplication.delete(\"age\");\n\t\tAssertions.assertNull(application.get(\"age\"));\n\t\t\n\t\t// 是否为空 \n\t\tAssertions.assertTrue(application.valueIsNull(null));\n\t\tAssertions.assertTrue(application.valueIsNull(\"\"));\n\t\tAssertions.assertFalse(application.valueIsNull(\"abc\"));\n\t\t\n\t\t// 为空时才能写入 \n\t\tapplication.setByNull(\"age4\", \"18\");\n\t\tAssertions.assertEquals(application.getInt(\"age4\"), 18);\n\t\tapplication.setByNull(\"age4\", \"20\");\n\t\tAssertions.assertEquals(application.getInt(\"age4\"), 18);\n\t\t\n\t\t// 清空 \n\t\tapplication.clear();\n\t\tAssertions.assertEquals(application.keys().size(), 0);\n\t\t\n\t\t// 获取所有值 \n\t\tapplication.set(\"key1\", \"value1\");\n\t\tapplication.set(\"key2\", \"value2\");\n\t\tapplication.set(\"key3\", \"value3\");\n\t\tAssertions.assertEquals(application.keys().size(), 3);\n\n\t\t// 空列表 \n\t\tapplication.clear();\n\t\tAssertions.assertEquals(application.keys().size(), 0);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/config/SaTokenConfigTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.config;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.config.SaCookieConfig;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.config.SaTokenConfigFactory;\n\n/**\n * 配置类测试 \n * \n * @author click33\n * @since 2022-9-4\n */\npublic class SaTokenConfigTest {\n\n\t// 基本 get set 测试\n\t@Test\n\tpublic void testProp() {\n\t\tSaTokenConfig config = new SaTokenConfig();\n\t\t\n\t\tconfig.setTokenName(\"nav-token\");\n\t\tAssertions.assertEquals(config.getTokenName(), \"nav-token\");\n\t\t\n\t\tconfig.setTimeout(100204);\n\t\tAssertions.assertEquals(config.getTimeout(), 100204);\n\t\t\n\t\tconfig.setActiveTimeout(1804);\n\t\tAssertions.assertEquals(config.getActiveTimeout(), 1804);\n\n\t\tconfig.setIsConcurrent(false);\n\t\tAssertions.assertEquals(config.getIsConcurrent(), false);\n\n\t\tconfig.setIsShare(false);\n\t\tAssertions.assertEquals(config.getIsShare(), false);\n\n\t\tconfig.setMaxLoginCount(11);\n\t\tAssertions.assertEquals(config.getMaxLoginCount(), 11);\n\n\t\tconfig.setIsReadBody(false);\n\t\tAssertions.assertEquals(config.getIsReadBody(), false);\n\n\t\tconfig.setIsReadHeader(false);\n\t\tAssertions.assertEquals(config.getIsReadHeader(), false);\n\n\t\tconfig.setIsReadCookie(false);\n\t\tAssertions.assertEquals(config.getIsReadCookie(), false);\n\n\t\tconfig.setTokenStyle(\"tik\");\n\t\tAssertions.assertEquals(config.getTokenStyle(), \"tik\");\n\n\t\tconfig.setDataRefreshPeriod(111);\n\t\tAssertions.assertEquals(config.getDataRefreshPeriod(), 111);\n\n\t\tconfig.setTokenSessionCheckLogin(false);\n\t\tAssertions.assertEquals(config.getTokenSessionCheckLogin(), false);\n\n\t\tconfig.setAutoRenew(false);\n\t\tAssertions.assertEquals(config.getAutoRenew(), false);\n\n\t\tconfig.setTokenPrefix(\"token\");\n\t\tAssertions.assertEquals(config.getTokenPrefix(), \"token\");\n\n\t\tconfig.setIsPrint(false);\n\t\tAssertions.assertEquals(config.getIsPrint(), false);\n\n\t\tconfig.setIsLog(false);\n\t\tAssertions.assertEquals(config.getIsLog(), false);\n\n\t\tconfig.setJwtSecretKey(\"NgdfaXasARggr\");\n\t\tAssertions.assertEquals(config.getJwtSecretKey(), \"NgdfaXasARggr\");\n\n\t\tconfig.setSameTokenTimeout(1004);\n\t\tAssertions.assertEquals(config.getSameTokenTimeout(), 1004);\n\n\t\tconfig.setHttpBasic(\"sa:123456\");\n\t\tAssertions.assertEquals(config.getHttpBasic(), \"sa:123456\");\n\n\t\tconfig.setCurrDomain(\"http://127.0.0.1:8084\");\n\t\tAssertions.assertEquals(config.getCurrDomain(), \"http://127.0.0.1:8084\");\n\n\t\tconfig.setCheckSameToken(false);\n\t\tAssertions.assertEquals(config.getCheckSameToken(), false);\n\n\t\tSaCookieConfig scc = new SaCookieConfig();\n\t\tconfig.setCookie(scc);\n\t\tAssertions.assertEquals(config.getCookie(), scc);\n\t\t\n\t\tconfig.toString();\n\t}\n\n\t// 从文件读取 \n\t@Test\n\tpublic void testSaTokenConfigFactory() {\n\t\tSaTokenConfig config = SaTokenConfigFactory.createConfig(\"sa-token2.properties\");\n\t\tAssertions.assertEquals(config.getTokenName(), \"use-token\");\n\t\tAssertions.assertEquals(config.getTimeout(), 9000);\n\t\tAssertions.assertEquals(config.getActiveTimeout(), 240);\n\t\tAssertions.assertEquals(config.getIsConcurrent(), false);\n\t\tAssertions.assertEquals(config.getIsShare(), false);\n\t\tAssertions.assertEquals(config.getIsLog(), true);\n\t}\n\n\t// 测试 SaCookieConfig \n\t@Test\n\tpublic void testSaCookieConfig() {\n\t\tSaCookieConfig config = new SaCookieConfig();\n\t\t\n\t\tconfig.setDomain(\"stp.cn\");\n\t\tAssertions.assertEquals(config.getDomain(), \"stp.cn\");\n\t\t\n\t\tconfig.setPath(\"/pro/\");\n\t\tAssertions.assertEquals(config.getPath(), \"/pro/\");\n\t\t\n\t\tconfig.setSecure(true);\n\t\tAssertions.assertEquals(config.getSecure(), true);\n\n\t\tconfig.setHttpOnly(false);\n\t\tAssertions.assertEquals(config.getHttpOnly(), false);\n\n\t\tconfig.setSameSite(\"lax\");\n\t\tAssertions.assertEquals(config.getSameSite(), \"lax\");\n\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaCookieTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.context.model;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.context.model.SaCookie;\n\n/**\n * SaFoxUtil 工具类测试 \n * \n * @author click33\n * @since 2022-2-8 22:14:25\n */\npublic class SaCookieTest {\n\n    @Test\n    public void test() {\n    \tSaCookie cookie = new SaCookie(\"satoken\", \"xxxx-xxxx-xxxx-xxxx\")\n    \t\t\t.setDomain(\"https://sa-token.cc/\")\n    \t\t\t.setMaxAge(-1)\n    \t\t\t.setPath(\"/\")\n    \t\t\t.setSameSite(\"Lax\")\n    \t\t\t.setHttpOnly(true)\n    \t\t\t.setSecure(true);\n\n    \tAssertions.assertEquals(cookie.getName(), \"satoken\");\n    \tAssertions.assertEquals(cookie.getValue(), \"xxxx-xxxx-xxxx-xxxx\");\n    \tAssertions.assertEquals(cookie.getDomain(), \"https://sa-token.cc/\");\n    \tAssertions.assertEquals(cookie.getMaxAge(), -1);\n    \tAssertions.assertEquals(cookie.getPath(), \"/\");\n    \tAssertions.assertEquals(cookie.getSameSite(), \"Lax\");\n    \tAssertions.assertEquals(cookie.getHttpOnly(), true);\n    \tAssertions.assertEquals(cookie.getSecure(), true);\n    \tAssertions.assertEquals(cookie.toHeaderValue(), \"satoken=xxxx-xxxx-xxxx-xxxx; Domain=https://sa-token.cc/; Path=/; Secure; HttpOnly; SameSite=Lax\");\n    \t\n    \tAssertions.assertNotNull(cookie.toString());\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/context/model/SaTokenContextDefaultImplTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.context.model;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.context.SaTokenContextDefaultImpl;\nimport cn.dev33.satoken.exception.SaTokenException;\n\n/**\n * 默认上下文测试 \n * \n * @author click33\n * @since 2022-9-5\n */\npublic class SaTokenContextDefaultImplTest {\n\n\t@Test\n\tpublic void testSaTokenContextDefaultImpl() {\n\t\tSaTokenContextDefaultImpl context = new SaTokenContextDefaultImpl();\n\t\tAssertions.assertThrows(SaTokenException.class, () -> context.getStorage());\n\t\tAssertions.assertThrows(SaTokenException.class, () -> context.getRequest());\n\t\tAssertions.assertThrows(SaTokenException.class, () -> context.getResponse());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/dao/SaTokenDaoTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.dao;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.dao.SaTokenDaoDefaultImpl;\nimport cn.dev33.satoken.session.SaSession;\n\n/**\n * SaTokenDao 持久层 测试 \n * \n * @author click33\n * @since 2022-2-9 15:39:38\n */\npublic class SaTokenDaoTest {\n\n\tSaTokenDao dao = new SaTokenDaoDefaultImpl();\n\n\t// 字符串存取 \n    @Test\n    public void get() {\n    \tdao.set(\"name\", \"zhangsan\", 60);\n    \tAssertions.assertEquals(dao.get(\"name\"), \"zhangsan\");\n    \tAssertions.assertTrue(dao.getTimeout(\"name\") <= 60);\n    \tAssertions.assertEquals(dao.getTimeout(\"name2\"), -2);\n    \t\n    \tdao.update(\"name\", \"lisi\");\n    \tAssertions.assertEquals(dao.get(\"name\"), \"lisi\");\n    \t\n    \tdao.updateTimeout(\"name\", 100);\n    \tAssertions.assertTrue(dao.getTimeout(\"name\") <= 100);\n    \t\n    \tdao.delete(\"name\");\n    \tAssertions.assertEquals(dao.get(\"name\"), null);\n    }\n\n\t// 对象存取 \n    @Test\n    public void getObject() {\n    \tdao.setObject(\"name\", \"zhangsan\", 60);\n    \tAssertions.assertEquals(dao.getObject(\"name\"), \"zhangsan\");\n    \tAssertions.assertTrue(dao.getObjectTimeout(\"name\") <= 60);\n    \t\n    \tdao.updateObject(\"name\", \"lisi\");\n    \tAssertions.assertEquals(dao.getObject(\"name\"), \"lisi\");\n    \t\n    \tdao.updateObjectTimeout(\"name\", 100);\n    \tAssertions.assertTrue(dao.getObjectTimeout(\"name\") <= 100);\n    \t\n    \tdao.deleteObject(\"name\");\n    \tAssertions.assertEquals(dao.getObject(\"name\"), null);\n    }\n\n\t// SaSession 存取 \n    @Test\n    public void getSession() {\n    \tSaSession session = new SaSession(\"session-1001\");\n    \t\n    \tdao.setSession(session, 60);\n    \tAssertions.assertEquals(dao.getSession(\"session-1001\").getId(), session.getId());\n    \tAssertions.assertTrue(dao.getSessionTimeout(\"session-1001\") <= 60);\n    \t\n    \tSaSession session2 = new SaSession(\"session-1001\");\n    \tdao.updateSession(session2);\n    \tAssertions.assertEquals(dao.getSession(\"session-1001\").getId(), session2.getId());\n    \t\n    \tdao.updateSessionTimeout(\"session-1001\", 100);\n    \tAssertions.assertTrue(dao.getSessionTimeout(\"session-1001\") <= 100);\n    \t\n    \tdao.deleteSession(\"session-1001\");\n    \tAssertions.assertEquals(dao.getSession(\"session-1001\"), null);\n    }\n\n    // 测试永久有效期的写值改值 \n    @Test\n    public void testUpdate() {\n\n    \t// ----------- 字符串 相关 \n    \t\n    \t// 永久有效 \n    \tdao.set(\"age\", \"20\", -1);\n    \tAssertions.assertEquals(dao.get(\"age\"), \"20\");\n    \tAssertions.assertEquals(dao.getTimeout(\"age\"), SaTokenDao.NEVER_EXPIRE);\n    \t\n    \t// 修改值 \n    \tdao.update(\"age\", \"22\");\n    \tAssertions.assertEquals(dao.get(\"age\"), \"22\");\n    \t// 有效期应该不变，还是永久 \n    \tAssertions.assertEquals(dao.getTimeout(\"age\"), SaTokenDao.NEVER_EXPIRE);\n    \t\n    \t\n    \t// ----------- Session 相关 \n    \t\n    \t// 永久有效 \n    \tSaSession session = new SaSession(\"session-1001\");\n    \tdao.setSession(session, -1);\n    \tAssertions.assertEquals(dao.getSession(\"session-1001\").getId(), session.getId());\n    \tAssertions.assertEquals(dao.getSessionTimeout(\"session-1001\"), SaTokenDao.NEVER_EXPIRE);\n    \t\n    \t// 修改值 \n    \tdao.updateSession(session);\n    \tAssertions.assertEquals(dao.getSession(\"session-1001\").getId(), session.getId());\n    \t// 有效期应该不变，还是永久 \n    \tAssertions.assertEquals(dao.getSessionTimeout(\"session-1001\"), SaTokenDao.NEVER_EXPIRE);\n    \t\n    \t\n    \t// ----------- 无效update \n    \tdao.update(\"mid\", \"zhang\");\n    \tAssertions.assertNull(dao.get(\"mid\"));\n    }\n\n    // timeout为0或者小于等于-2时，不写入\n    @Test\n    public void test0Timeout() {\n    \t\n    \t// ----------- 字符串 相关 \n    \t\n    \t// 字符串 0 和 <-2 \n    \tdao.set(\"avatar\", \"1.jpg\", 0);\n    \tAssertions.assertNull(dao.get(\"avatar\"));\n    \t\n    \tdao.set(\"avatar\", \"1.jpg\", -9);\n    \tAssertions.assertNull(dao.get(\"avatar\"));\n\n    \t// ----------- Session 相关 \n    \t\n    \t// Session 0 和 <-2 \n    \tSaSession session = new SaSession(\"session-1001\");\n    \tdao.setSession(session, 0);\n    \tAssertions.assertNull(dao.getSession(\"session-1001\"));\n\n    \tdao.setSession(session, -9);\n    \tAssertions.assertNull(dao.getSession(\"session-1001\"));\n    }\n\n    // TO-DO 和时间相关的测试 \n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/fun/IsRunFunctionTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.fun;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.fun.IsRunFunction;\n\n/**\n * IsRunFunction 测试 \n * \n * @author click33\n * @since 2022-2-9 16:11:10\n */\npublic class IsRunFunctionTest {\n\n    @Test\n    public void test() {\n    \t\n    \tclass TempClass{\n    \t\tint count = 1;\n    \t}\n    \tTempClass obj = new TempClass();\n\n    \tIsRunFunction fun = new IsRunFunction(true);\n    \tfun.exe(()->{\n    \t\tobj.count = 2;\n    \t}).noExe(()->{\n    \t\tobj.count = 3;\n    \t});\n    \t\n    \tAssertions.assertEquals(obj.count, 2);\n    }\n\n    @Test\n    public void test2() {\n    \t\n    \tclass TempClass{\n    \t\tint count = 1;\n    \t}\n    \tTempClass obj = new TempClass();\n\n    \tIsRunFunction fun = new IsRunFunction(false);\n    \tfun.exe(()->{\n    \t\tobj.count = 2;\n    \t}).noExe(()->{\n    \t\tobj.count = 3;\n    \t});\n    \t\n    \tAssertions.assertEquals(obj.count, 3);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/json/SaJsonTemplateDefaultImplTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.json;\n\nimport cn.dev33.satoken.exception.NotImplException;\nimport cn.dev33.satoken.json.SaJsonTemplateDefaultImpl;\nimport cn.dev33.satoken.util.SoMap;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * json默认实现类测试 \n * \n * @author click33\n * @since 2022-9-1\n */\npublic class SaJsonTemplateDefaultImplTest {\n\n    @Test\n    public void testSaJsonTemplateDefaultImpl() {\n    \tSaJsonTemplateDefaultImpl saJsonTemplate = new SaJsonTemplateDefaultImpl();\n    \t// 组件未实现\n    \tAssertions.assertThrows(NotImplException.class, () -> {\n    \t\tsaJsonTemplate.jsonToMap(\"{}\");\n    \t});\n    \t// 组件未实现\n    \tAssertions.assertThrows(NotImplException.class, () -> {\n    \t\tsaJsonTemplate.objectToJson(SoMap.getSoMap(\"name\", \"zhangsan\"));\n    \t});\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/package-info.java",
    "content": "/**\n * 核心包测试 \n */\n/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core;"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/BCryptTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.secure;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.secure.BCrypt;\n\n/**\n * BCrypt 加密测试\n * \n * @author dream.\n * @since 2022/1/20\n */\npublic class BCryptTest {\n\n\t@Test\n\tpublic void testCheckpw() {\n\t\tfinal String hashed = BCrypt.hashpw(\"12345\");\n//\t\tSystem.out.println(hashed);\n\t\tAssertions.assertTrue(BCrypt.checkpw(\"12345\", hashed));\n\t\tAssertions.assertFalse(BCrypt.checkpw(\"123456\", hashed));\n\t}\n\t\n}"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaBase64UtilTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.secure;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.secure.SaBase64Util;\n\n/**\n * SaBase64Util 测试 \n * \n * @author click33\n * @since 2022-2-9\n */\npublic class SaBase64UtilTest {\n\n    @Test\n    public void test() {\n    \t// 文本\n    \tString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n    \t// 使用Base64编码\n    \tString base64Text = SaBase64Util.encode(text);\n    \tAssertions.assertEquals(base64Text, \"U2EtVG9rZW4g5LiA5Liq6L276YeP57qnamF2Yeadg+mZkOiupOivgeahhuaetg==\");\n\n    \t// 使用Base64解码\n    \tString text2 = SaBase64Util.decode(base64Text);\n    \tAssertions.assertEquals(text2, \"Sa-Token 一个轻量级java权限认证框架\");\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/secure/SaSecureUtilTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.secure;\n\nimport java.util.HashMap;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.secure.SaSecureUtil;\n\n/**\n * SaSecureUtil 加密工具类 测试 \n * \n * @author click33\n * @since 2022-2-9\n */\npublic class SaSecureUtilTest {\n\t\n    @Test\n    public void test() {\n    \t\n    \t// md5加密 \n    \tAssertions.assertEquals(SaSecureUtil.md5(\"123456\"), \"e10adc3949ba59abbe56e057f20f883e\");\n\n    \t// sha1加密 \n    \tAssertions.assertEquals(SaSecureUtil.sha1(\"123456\"), \"7c4a8d09ca3762af61e59520943dc26494f8941b\");\n\n    \t// sha256加密 \n    \tAssertions.assertEquals(SaSecureUtil.sha256(\"123456\"), \"8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92\");\n\n    \t// md5加盐加密: md5(md5(str) + md5(salt)) \n    \tAssertions.assertEquals(SaSecureUtil.md5BySalt(\"123456\", \"salt\"), \"f52020dca765fd3943ed40a615dc2c5c\");\n    \t\n    }\n\n    @Test\n    public void aesEncrypt() {\n    \t// 定义秘钥和明文\n    \tString key = \"123456\";\n    \tString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n    \t// 加密 \n    \tString ciphertext = SaSecureUtil.aesEncrypt(key, text);\n    \tAssertions.assertEquals(ciphertext, \"KmSqfwxY5BRuWoHMWJqtebcOZ2lEEZaj2OSi1Ei8pRx4zdi24wsnwsTQVjbXRQ0M\");\n\n    \t// 解密 \n    \tString text2 = SaSecureUtil.aesDecrypt(key, ciphertext);\n    \tAssertions.assertEquals(text2, \"Sa-Token 一个轻量级java权限认证框架\");\n    }\n\n    @Test\n    public void rsaEncryptByPublic() {\n    \t// 定义私钥和公钥 \n    \tString 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==\";\n    \tString publicKey = \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB\";\n\n    \t// 文本\n    \tString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n    \t// 使用公钥加密\n    \tString ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);\n//    \tAssert.assertEquals(ciphertext, \"d9e01fd105b059e975c524a1f4dccbe10dfc3a23b931a9e168ecb0a5758a29c45532254679f86cf83a63e5cc21ef631802fe70ea47e7519f5d96e0d1fab38a6f6dbebdb34b106ce7f27c341838e4e88a8ff3298c519c29a3f0944cf8f668bfecd9394f16945d85d84c4d813d12ecadf34bfb21850c383977b5b2de848fa40995\");\n\n    \t// 使用私钥解密\n    \tString text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);\n    \tAssertions.assertEquals(text2, \"Sa-Token 一个轻量级java权限认证框架\");\n    }\n\n    @Test\n    public void rsaEncryptByPrivate() throws Exception {\n    \t\n    \t// 生成私钥和公钥 \n    \tHashMap<String, String> map = SaSecureUtil.rsaGenerateKeyPair();\n    \tString privateKey = map.get(\"private\"); \n    \tString publicKey = map.get(\"public\");\n\n    \t// 文本\n    \tString text = \"Sa-Token 一个轻量级java权限认证框架\";\n\n    \t// 使用公钥加密\n    \tString ciphertext = SaSecureUtil.rsaEncryptByPrivate(privateKey, text);\n    \t\n    \t// 使用私钥解密\n    \tString text2 = SaSecureUtil.rsaDecryptByPublic(publicKey, ciphertext);\n    \tAssertions.assertEquals(text2, \"Sa-Token 一个轻量级java权限认证框架\");\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionCustomUtilTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.session;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaSessionCustomUtil;\n\n/**\n * SaSession 测试 \n * \n * @author click33\n * @since 2022-2-9 \n */\npublic class SaSessionCustomUtilTest {\n\n    // 测试自定义Session \n    @Test\n    public void testCustomSession() {\n    \tSaTokenDao dao = SaManager.getSaTokenDao();\n    \t\n    \t// 刚开始不存在 \n    \tAssertions.assertFalse(SaSessionCustomUtil.isExists(\"art-1\"));\n    \tSaSession session = dao.getSession(\"satoken:custom:session:\" + \"art-1\");\n    \tAssertions.assertNull(session);\n    \t\n    \t// 调用一下\n    \tSaSessionCustomUtil.getSessionById(\"art-1\");\n    \tSaSessionCustomUtil.getSessionById(\"art-1\", false);\n    \t\n    \t// 就存在了 \n    \tAssertions.assertTrue(SaSessionCustomUtil.isExists(\"art-1\"));\n    \tSaSession session2 = dao.getSession(\"satoken:custom:session:\" + \"art-1\");\n    \tAssertions.assertNotNull(session2);\n    \t\n    \t// 给删除掉 \n    \tSaSessionCustomUtil.deleteSessionById(\"art-1\");\n    \t\n    \t// 就又不存在了 \n    \tAssertions.assertFalse(SaSessionCustomUtil.isExists(\"art-1\"));\n    \tSaSession session3 = dao.getSession(\"satoken:custom:session:\" + \"art-1\");\n    \tAssertions.assertNull(session3);\n    \t\n    \t// 调用了也不会存在 \n    \tSaSessionCustomUtil.getSessionById(\"art-4\", false);\n    \tSaSession session4 = dao.getSession(\"satoken:custom:session:\" + \"art-2\");\n    \tAssertions.assertNull(session4);\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaSessionTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.session;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * SaSession 测试 \n * \n * @author click33\n * @since 2022-2-9 \n */\npublic class SaSessionTest {\n\n\t// 基础属性 \n    @Test\n    public void testProp() {\n    \tSaSession session = new SaSession(\"session-1001\");\n    \tAssertions.assertEquals(session.getId(), \"session-1001\");\n\n    \t// 属性读取 \n    \tsession = new SaSession();\n    \tsession.setId(\"session-1009\");\n    \tAssertions.assertEquals(session.getId(), \"session-1009\");\n    \t\n    \tsession.setCreateTime(1662241013902L);\n    \tAssertions.assertEquals(session.getCreateTime(), 1662241013902L);\n    }\n\t\n\t// 基础存取值 \n    @Test\n    public void testSetGet() {\n    \t\n    \t// 基础取值 \n    \tSaSession session = new SaSession(\"session-1002\");\n    \tsession.set(\"name\", \"zhangsan\");\n    \tsession.set(\"age\", 18);\n    \tAssertions.assertEquals(session.get(\"name\"), \"zhangsan\");\n    \tAssertions.assertEquals((int)session.get(\"age\", 20), 18);\n    \tAssertions.assertEquals((int)session.get(\"age2\", 20), 20);\n    \tAssertions.assertEquals(session.getModel(\"age\", Double.class).getClass(), Double.class);\n    \t\n    \t// 原本无值时才会写入 \n    \tsession.setByNull(\"name\", \"lisi\");\n    \tAssertions.assertEquals(session.get(\"name\"), \"zhangsan\");\n    \tsession.setByNull(\"name2\", \"lisi\");\n    \tAssertions.assertEquals(session.get(\"name2\"), \"lisi\");\n    \t\n    \t// 复杂取值 \n    \tclass User {\n    \t\tString name;\n    \t\tint age;\n\t\t\tUser(String name, int age) {\n\t\t\t\tthis.name = name;\n\t\t\t\tthis.age = age;\n\t\t\t}\n    \t}\n    \tUser user = new User(\"zhangsan\", 18);\n    \tsession.set(\"user\", user);\n    \t\n    \tUser user2 = session.getModel(\"user\", User.class);\n    \tAssertions.assertNotNull(user2);\n    \tAssertions.assertEquals(user2.name, \"zhangsan\");\n    \tAssertions.assertEquals(user2.age, 18);\n    }\n    \n    // 测试有效期\n    @Test\n    public void testSessionTimeout() {\n    \t// 修改剩余有效期 \n    \tSaSession session = new SaSession(\"session-1005\");\n    \tSaManager.getSaTokenDao().setSession(session, 20000);\n    \tsession.updateMaxTimeout(100);\n    \tAssertions.assertTrue(session.timeout() <= 100);\n    \tSystem.out.println(session.timeout());\n    \t// 仍然是 <=100 \n    \tsession.updateMaxTimeout(1000);\n    \tAssertions.assertTrue(session.timeout() <= 100);\n    \tSystem.out.println(session.timeout());\n    \t// Min 修改 \n    \tsession.updateMinTimeout(-1);\n    \tSystem.out.println(session.timeout());\n    \tAssertions.assertTrue(session.timeout() == -1);\n    }\n    \n    // 测试token 签名 \n    @Test\n    public void testSaTerminalInfo() {\n    \tSaSession session = new SaSession(\"session-1002\");\n    \t\n    \t// 添加 Token 签名 \n    \tsession.addTerminal(new SaTerminalInfo(1, \"xxxx-xxxx-xxxx-xxxx-1\", \"PC\", null));\n    \tsession.addTerminal(new SaTerminalInfo(2, \"xxxx-xxxx-xxxx-xxxx-2\", \"APP\", null));\n\n    \t// 查询 \n    \tAssertions.assertEquals(session.getTerminalList().size(), 2);\n    \tAssertions.assertEquals(session.getTerminal(\"xxxx-xxxx-xxxx-xxxx-1\").getDeviceType(), \"PC\");\n    \tAssertions.assertEquals(session.getTerminal(\"xxxx-xxxx-xxxx-xxxx-2\").getDeviceType(), \"APP\");\n\n    \t// 删除一个 \n    \tsession.removeTerminal(\"xxxx-xxxx-xxxx-xxxx-1\");\n    \tAssertions.assertEquals(session.getTerminalList().size(), 1);\n\n    \t// 删除一个不存在的，则不影响 SaTerminalInfo 列表\n    \tsession.removeTerminal(\"xxxx-xxxx-xxxx-xxxx-999\");\n    \tAssertions.assertEquals(session.getTerminalList().size(), 1);\n    \t\n    \t// 重置整个签名列表 \n    \tList<SaTerminalInfo> list = Arrays.asList(\n    \t\t\tnew SaTerminalInfo(1, \"xxxx-xxxx-xxxx-xxxx-1\", \"WEB\", null),\n    \t\t\tnew SaTerminalInfo(2, \"xxxx-xxxx-xxxx-xxxx-2\", \"phone\", null),\n    \t\t\tnew SaTerminalInfo(3, \"xxxx-xxxx-xxxx-xxxx-3\", \"ipad\", null)\n    \t\t\t);\n    \tsession.setTerminalList(list);\n    \tAssertions.assertEquals(session.getTerminalList().size(), 3);\n    \tAssertions.assertEquals(session.getTerminal(\"xxxx-xxxx-xxxx-xxxx-1\").getDeviceType(), \"WEB\");\n    \tAssertions.assertEquals(session.getTerminal(\"xxxx-xxxx-xxxx-xxxx-2\").getDeviceType(), \"phone\");\n    \tAssertions.assertEquals(session.getTerminal(\"xxxx-xxxx-xxxx-xxxx-3\").getDeviceType(), \"ipad\");\n    }\n    \n    // 测试重置 DataMap\n    @Test\n    public void testDataMap() {\n    \tSaSession session = new SaSession(\"session-1003\");\n    \tsession.set(\"key1\", \"value1\");\n    \tsession.set(\"key2\", \"value2\");\n    \tsession.set(\"key3\", \"value3\");\n    \t\n    \t// 所有数据 \n    \tAssertions.assertEquals(session.keys().size(), 3);\n    \tAssertions.assertEquals(session.getDataMap().size(), 3);\n    \t\n    \t// 重置所有数据 \n    \tMap<String, Object> dataMap = new ConcurrentHashMap<>();\n    \tdataMap.put(\"aaa\", \"111\");\n    \tdataMap.put(\"bbb\", \"222\");\n    \tsession.refreshDataMap(dataMap);\n    \tAssertions.assertEquals(session.keys().size(), 2);\n    \t\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/session/SaTerminalInfoTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.session;\n\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * SaTerminalInfo 相关测试 \n * \n * @author click33\n * @since 2022-9-4\n */\npublic class SaTerminalInfoTest {\n\n\t// 测试 \n\t@Test\n\tpublic void testSaTerminalInfo() {\n\t\tSaTerminalInfo terminal = new SaTerminalInfo();\n\t\tterminal.setDeviceType(\"PC\");\n\t\tterminal.setTokenValue(\"ttt-value\");\n\t\t\n\t\tAssertions.assertEquals(terminal.getDeviceType(), \"PC\");\n\t\tAssertions.assertEquals(terminal.getTokenValue(), \"ttt-value\");\n\n\t\tAssertions.assertNotNull(terminal.toString());\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/sign/SaSignTemplateTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.sign;\n\nimport cn.dev33.satoken.sign.SaSignManager;\nimport cn.dev33.satoken.sign.config.SaSignConfig;\nimport cn.dev33.satoken.util.SoMap;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n/**\n * API 接口签名测试 \n * \n * @author click33\n * @since 2022-9-2\n */\npublic class SaSignTemplateTest {\n\n\tString key = \"SwqFmsKxcbq23\";\n\n\t// 连接参数列表 \n\t@Test\n\tpublic void testJoinParamsDictSort() {\n\t\tSoMap map = SoMap.getSoMap()\n\t\t\t\t.set(\"name\", \"zhang\")\n\t\t\t\t.set(\"age\", 18)\n\t\t\t\t.set(\"sex\", \"女\");\n\t\tString str = SaSignManager.getSaSignTemplate().joinParamsDictSort(map);\n\n\t\t// 按照音序排列 \n\t\tAssertions.assertEquals(str, \"age=18&name=zhang&sex=女\");\n\t}\n\t\n\t// 给参数签名 \n\t@Test\n\tpublic void testCreateSign() {\n\t\tSoMap map = SoMap.getSoMap()\n\t\t\t\t.set(\"name\", \"zhang\")\n\t\t\t\t.set(\"age\", 18)\n\t\t\t\t.set(\"sex\", \"女\");\n\t\tSaSignManager.getSaSignTemplate().setSignConfig(new SaSignConfig().setSecretKey(key));\n\t\tString sign = SaSignManager.getSaSignTemplate().createSign(map);\n\t\tAssertions.assertEquals(sign, \"6f5e844a53e74363c2f6b24f64c4f0ff\");\n\t\t\n\t\t// 多次签名，结果一致  \n\t\tString sign2 = SaSignManager.getSaSignTemplate().createSign(map);\n\t\tAssertions.assertEquals(sign, sign2);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/stp/TokenInfoTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.stp;\n\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.stp.SaTokenInfo;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Token 参数扩展 \n * \n * @author click33\n * @since 2022-9-5\n */\npublic class TokenInfoTest {\n\n\t@Test\n\tpublic void test() {\n\t\tSaTokenInfo info = new SaTokenInfo();\n\t\tinfo.setTokenName(\"satoken\");\n\t\tinfo.setTokenValue(\"xxxxx-xxxxx-xxxxx-xxxxx\");\n\t\tinfo.setIsLogin(true);\n\t\tinfo.setLoginId(10001);\n\t\tinfo.setLoginType(\"login\");\n\t\tinfo.setTokenTimeout(1800);\n\t\tinfo.setSessionTimeout(120);\n\t\tinfo.setTokenSessionTimeout(1800);\n\t\tinfo.setTokenActiveTimeout(120);\n\t\tinfo.setLoginDeviceType(\"PC\");\n\t\tinfo.setTag(\"xxx\");\n\n\t\tAssertions.assertEquals(info.getTokenName(), \"satoken\");\n\t\tAssertions.assertEquals(info.getTokenValue(), \"xxxxx-xxxxx-xxxxx-xxxxx\");\n\t\tAssertions.assertEquals(info.getIsLogin(), true);\n\t\tAssertions.assertEquals(info.getLoginId(), 10001);\n\t\tAssertions.assertEquals(info.getLoginType(), \"login\");\n\t\tAssertions.assertEquals(info.getTokenTimeout(), 1800);\n\t\tAssertions.assertEquals(info.getSessionTimeout(), 120);\n\t\tAssertions.assertEquals(info.getTokenSessionTimeout(), 1800);\n\t\tAssertions.assertEquals(info.getTokenActiveTimeout(), 120);\n\t\tAssertions.assertEquals(info.getLoginDeviceType(), \"PC\");\n\t\tAssertions.assertEquals(info.getTag(), \"xxx\");\n\t\t\n\t\tAssertions.assertNotNull(info.toString());\n\t}\n\n\t@Test\n\tpublic void testLoginParameter() {\n\t\tAssertions.assertEquals(new SaLoginParameter().setDeviceType(\"PC\").getDeviceType(), \"PC\");\n\t\tAssertions.assertEquals(new SaLoginParameter().setIsLastingCookie(false).getIsLastingCookie(), false);\n\t\tAssertions.assertEquals(new SaLoginParameter().setTimeout(1600).getTimeout(), 1600);\n\t\tAssertions.assertEquals(new SaLoginParameter().setToken(\"token-xxx\").getToken(), \"token-xxx\");\n\t\tAssertions.assertEquals(new SaLoginParameter().setExtra(\"age\", 18).getExtra(\"age\"), 18);\n\t\t\n\t\tMap<String, Object> extraData = new HashMap<>();\n\t\textraData.put(\"age\", 20);\n\t\tSaLoginParameter lm = new SaLoginParameter().setExtraData(extraData);\n\t\tAssertions.assertEquals(lm.getExtraData(), extraData);\n\t\tAssertions.assertEquals(lm.getExtra(\"age\"), 20);\n\t\tAssertions.assertTrue(lm.haveExtraData());\n\t\tAssertions.assertNotNull(lm.toString());\n\t\t\n\t\t// 计算 CookieTimeout \n\t\tSaLoginParameter loginParameter = SaLoginParameter\n\t\t\t\t.create()\n\t\t\t\t.setTimeout(-1);\n\t\tAssertions.assertEquals(loginParameter.getCookieTimeout(), Integer.MAX_VALUE);\n\t\tAssertions.assertEquals(loginParameter.getDeviceType(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.temp;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.temp.SaTempUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\n/**\n * 临时Token模块测试 \n * \n * @author click33\n * @since 2022-9-1\n */\npublic class SaTempTokenTest {\n\n    // 测试：临时Token认证模块\n    @Test\n    public void testSaTemp() {\n    \tSaTokenDao dao = SaManager.getSaTokenDao();\n    \t\n    \t// 生成token \n    \tString token = SaTempUtil.createToken(\"group-1014\", 200);\n//\t\t System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet());\n//\t\tSystem.out.println(\"satoken:temp-token:\" + \":\" + token);\n    \tAssertions.assertNotNull(token);\n\t\tAssertions.assertEquals(dao.getObject(\"satoken:temp-token:\" + token), \"group-1014\");\n    \t\n    \t// 解析token  \n    \tString value = SaTempUtil.parseToken(token, String.class);\n    \tAssertions.assertEquals(value, \"group-1014\");\n\n\t\t// 解析 token 并裁剪前缀\n\t\tlong value2 = SaTempUtil.parseToken(token, \"group-\", Long.class);\n\t\tAssertions.assertEquals(value2, 1014);\n\n\t\t// 默认类型\n    \tObject value3 = SaTempUtil.parseToken(token);\n    \tAssertions.assertEquals(value3, \"group-1014\"); \n    \t\n    \t// 转换类型 \n    \tString value4 = SaTempUtil.parseToken(token, String.class);\n    \tAssertions.assertEquals(value4, \"group-1014\"); \n    \t\n    \t// 过期时间 \n    \tlong timeout = SaTempUtil.getTimeout(token);\n    \tAssertions.assertTrue(timeout > 195);\n\t\tAssertions.assertTrue(timeout < 201);\n    \t\n    \t// 回收token \n    \tSaTempUtil.deleteToken(token);\n    \tString value5 = SaTempUtil.parseToken(token, String.class);\n        Assertions.assertNull(value5);\n        Assertions.assertNull(dao.getObject(\"satoken:temp-token:\" + \":\" + token));\n    }\n\n    // 测试：临时Token认证模块索引\n    @Test\n    public void testSaTempIndex() {\n    \tSaTokenDao dao = SaManager.getSaTokenDao();\n    \t\n    \t// 生成token \n\t\tString token1 = SaTempUtil.createToken(\"1001\", 200, true);\n\t\tString token2 = SaTempUtil.createToken(\"1001\", 300, true);\n\t\tString token3 = SaTempUtil.createToken(\"1001\", 400, true);\n\n\t\tAssertions.assertNotNull(token1);\n\t\tAssertions.assertNotNull(token2);\n\t\tAssertions.assertNotNull(token3);\n    \t// System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap);\n\n    \t// 解析token\n    \tAssertions.assertEquals(SaTempUtil.parseToken(token1, String.class), \"1001\");\n\t\tAssertions.assertEquals(SaTempUtil.parseToken(token2, String.class), \"1001\");\n\t\tAssertions.assertEquals(SaTempUtil.parseToken(token3, String.class), \"1001\");\n\n\t\t// 缓存数据比对\n\t\tAssertions.assertEquals(dao.getObject(\"satoken:temp-token:\" + token1), \"1001\");\n\t\tAssertions.assertEquals(dao.getObject(\"satoken:temp-token:\" + token2), \"1001\");\n\t\tAssertions.assertEquals(dao.getObject(\"satoken:temp-token:\" + token3), \"1001\");\n\n\t\t// 索引\n\t\tList<String> tempTokenList = SaTempUtil.getTempTokenList(\"1001\");\n\t\tAssertions.assertEquals(tempTokenList.size(), 3);\n\t\tAssertions.assertTrue(tempTokenList.contains(token1));\n\t\tAssertions.assertTrue(tempTokenList.contains(token2));\n\t\tAssertions.assertTrue(tempTokenList.contains(token3));\n\n\t\tlong sessionTimeout = dao.getSessionTimeout(\"satoken:raw-session:temp-token:\" + \"1001\");\n\t\tAssertions.assertTrue(sessionTimeout > 395);\n\t\tAssertions.assertTrue(sessionTimeout < 401);\n\n\t\t// 移除一个 token\n\t\tSaTempUtil.deleteToken(token3);\n        Assertions.assertNull(SaTempUtil.parseToken(token3, String.class));\n        Assertions.assertNull(dao.getObject(\"satoken:temp-token:\" + token3));\n\n\t\tList<String> tempTokenList2 = SaTempUtil.getTempTokenList(\"1001\");\n\t\tAssertions.assertEquals(tempTokenList2.size(), 2);\n\t\tAssertions.assertFalse(tempTokenList2.contains(token3));\n\n\t\tlong sessionTimeout2 = dao.getSessionTimeout(\"satoken:raw-session:temp-token:\" + \"1001\");\n\t\tAssertions.assertTrue(sessionTimeout2 > 295);\n\t\tAssertions.assertTrue(sessionTimeout2 < 301);\n\n\t\t// 新增一个 token\n\t\tString token4 = SaTempUtil.createToken(\"1001\", -1, true);\n\t\tAssertions.assertEquals(SaTempUtil.parseToken(token4, String.class), \"1001\");\n\n\t\tList<String> tempTokenList3 = SaTempUtil.getTempTokenList(\"1001\");\n\t\tAssertions.assertEquals(tempTokenList3.size(), 3);\n\t\tAssertions.assertTrue(tempTokenList3.contains(token4));\n\n\t\tlong sessionTimeout4 = dao.getSessionTimeout(\"satoken:raw-session:temp-token:\" + \"1001\");\n        Assertions.assertEquals(-1, sessionTimeout4);\n    }\n\n    @Test\n    public void testGetJwtSecretKey() {\n    \t// 秘钥默认为null\n    \tString jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey();\n        Assertions.assertNull(jwtSecretKey);\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaFoxUtilTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.util;\n\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * SaFoxUtil 工具类测试 \n * \n * @author click33\n * @since 2022-2-8 22:14:25\n */\npublic class SaFoxUtilTest {\n\n    @Test\n    public void getRandomString() {\n    \tString randomString = SaFoxUtil.getRandomString(8);\n    \tAssertions.assertEquals(randomString.length(), 8);\n    }\n\n    @Test\n    public void isEmpty() {\n    \tAssertions.assertTrue(SaFoxUtil.isEmpty(\"\"));\n    \tAssertions.assertTrue(SaFoxUtil.isEmpty(null));\n    \tAssertions.assertFalse(SaFoxUtil.isEmpty(\"abc\"));\n    \t\n    \tAssertions.assertTrue(SaFoxUtil.isNotEmpty(\"abc\"));\n    \tAssertions.assertFalse(SaFoxUtil.isNotEmpty(null));\n    \tAssertions.assertFalse(SaFoxUtil.isNotEmpty(\"\"));\n    }\n\n    @Test\n    public void equals() {\n    \tAssertions.assertTrue(SaFoxUtil.equals(null, null));\n    \tAssertions.assertTrue(SaFoxUtil.equals(\"a\", \"a\"));\n    \tAssertions.assertFalse(SaFoxUtil.equals(\"1\", 1));\n    \tAssertions.assertFalse(SaFoxUtil.equals(\"1\", null));\n    \tAssertions.assertFalse(SaFoxUtil.equals(null, \"1\"));\n    }\n\n    @Test\n    public void getMarking28() {\n    \tAssertions.assertNotEquals(SaFoxUtil.getMarking28(), SaFoxUtil.getMarking28());\n    }\n\n    @Test\n    public void formatDate() {\n\tInstant instant = Instant.ofEpochMilli(1644328600364L);\n\tZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of(\"Asia/Shanghai\"));\n\tString formatDate = SaFoxUtil.formatDate(zonedDateTime);\n\tAssertions.assertEquals(formatDate, \"2022-02-08 21:56:40\");\n    }\n\n    @Test\n    public void searchList() {\n    \t// 原始数据 \n    \tList<String> dataList = Arrays.asList(\"token1\", \"token2\", \"token3\", \"token4\", \"token5\", \"aaa1\");\n    \t\n    \t// 分页 \n    \tList<String> list1 = SaFoxUtil.searchList(dataList, 1, 2, true);\n    \tAssertions.assertEquals(list1.size(), 2);\n    \tAssertions.assertEquals(list1.get(0), \"token2\");\n    \tAssertions.assertEquals(list1.get(1), \"token3\");\n    \t\n    \t// 前缀筛选 \n    \tList<String> list2 = SaFoxUtil.searchList(dataList, \"token\", \"\", 0, 10, true);\n    \tAssertions.assertEquals(list2.size(), 5);\n\n    \t// 关键字筛选 \n    \tList<String> list3 = SaFoxUtil.searchList(dataList, \"\", \"1\", 0, 10, true);\n    \tAssertions.assertEquals(list3.size(), 2);\n\n    \t// 综合筛选 \n    \tList<String> list4 = SaFoxUtil.searchList(dataList, \"token\", \"1\", 0, 10, true);\n    \tAssertions.assertEquals(list4.size(), 1);\n\n    \t// 关键字为null时，效果和 \"\" 等同 \n    \tList<String> list4_2 = SaFoxUtil.searchList(dataList, null, null, 0, 10, true);\n    \tList<String> list4_3 = SaFoxUtil.searchList(dataList, \"\", \"\", 0, 10, true);\n    \tAssertions.assertEquals(list4_2.get(0), list4_3.get(0));\n    \t\n    \t\n    \t// 不做分页  \n    \tList<String> list5 = SaFoxUtil.searchList(dataList, \"\", \"\", 0, -1, true);\n    \tAssertions.assertEquals(list5.size(), dataList.size());\n    \t\n    \t// 反序排列 list6的第一个元素 == dataList最后一个元素 \n    \tList<String> list6 = SaFoxUtil.searchList(dataList, \"\", \"\", 0, -1, false);\n    \tAssertions.assertEquals(list6.get(0), dataList.get(dataList.size() - 1));\n    }\n\n\t@Test\n\tpublic void vagueMatch() {\n\t\t// 不模糊\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"hello\", \"hello\"));\n\n\t\t// 正常模糊\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"hello*\", \"hello\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"hello*\", \"hello world\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"hello*\", \"hello*\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"hello*\", \"he\"));\n\n\t\t// 带 -\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user-*\", \"user-\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user-*\", \"user-add\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user-*\", \"user-*\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user-*\", \"user\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user-*-add-*\", \"user-xx-add-1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user-*-add-*\", \"user-add-1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user-*\", \"usermgt-list\"));\n\n\t\t// 带 /\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user/*\", \"user/\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user/*\", \"user/add\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user/*\", \"user/*\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user/*\", \"user\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user/*/add/*\", \"user/xx/add/1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user/*/add/*\", \"user/add/1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user/*\", \"usermgt/list\"));\n\n\t\t// 带 :\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user:*\", \"user:\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user:*\", \"user:add\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user:*\", \"user:*\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user:*\", \"user\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user:*:add:*\", \"user:xx:add:1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user:*:add:*\", \"user:add:1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user:*\", \"usermgt:list\"));\n\n\t\t// 带 .\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user.*\", \"user.\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user.*\", \"user.add\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user.*\", \"user.*\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user.*\", \"user\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"user.*.add.*\", \"user.xx.add.1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user.*.add.*\", \"user.add.1\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"user.*\", \"usermgt.list\"));\n\n\t\t// 极端情况\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(null, null));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(null, \"hello\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"hello*\", null));\n\n\t\t// url 匹配\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"*\", \"http://sa-sso-client1.com:9001/sso/login\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"http://sa-sso-client1.com:9001/*\", \"http://sa-sso-client1.com:9001/sso/login\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"http://sa-sso-client1.com:9001/*\", \"http://sa-sso-client1.com:9001/sso/login?name=1\"));\n\t\tAssertions.assertTrue(SaFoxUtil.vagueMatch(\"http://sa-sso-client1.com:9001/*\", \"http://sa-sso-client1.com:9001/sso/login?name=1&age=2\"));\n\t\tAssertions.assertFalse(SaFoxUtil.vagueMatch(\"http://sa-sso-client1.com:9001/*\", \"http://sa-sso-client1.com:9002\"));\n\t}\n\n    @Test\n    public void isWrapperType() {\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Integer.class));\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Short.class));\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Long.class));\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Byte.class));\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Float.class));\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Double.class));\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Boolean.class));\n    \tAssertions.assertTrue(SaFoxUtil.isWrapperType(Character.class));\n    \t\n    \tAssertions.assertFalse(SaFoxUtil.isWrapperType(int.class));\n    \tAssertions.assertFalse(SaFoxUtil.isWrapperType(long.class));\n    \tAssertions.assertFalse(SaFoxUtil.isWrapperType(Object.class));\n\t}\n\n    @Test\n    public void isBasicType() {\n    \tAssertions.assertTrue(SaFoxUtil.isBasicType(int.class));\n    \tAssertions.assertTrue(SaFoxUtil.isBasicType(Integer.class));\n    \tAssertions.assertTrue(SaFoxUtil.isBasicType(long.class));\n    \tAssertions.assertTrue(SaFoxUtil.isBasicType(Long.class));\n    \tAssertions.assertTrue(SaFoxUtil.isBasicType(String.class));\n    \t\n    \tAssertions.assertFalse(SaFoxUtil.isBasicType(List.class));\n    \tAssertions.assertFalse(SaFoxUtil.isBasicType(Map.class));\n\t}\n\t\n    @Test\n    public void getValueByType() {\n    \t// 基础类型，转换 \n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", int.class), 1);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", long.class), 1L);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", Long.class), 1L);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", String.class), \"1\");\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", short.class), (short)1);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", Short.class), (short)1);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", byte.class), (byte)1);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", Byte.class), (byte)1);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", float.class), 1f);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", Float.class), 1f);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", double.class), 1.0);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", Double.class), 1.0);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", boolean.class), false);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", Boolean.class), false);\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", char.class), '1');\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(\"1\", Character.class), '1');\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(1, String.class), \"1\");\n\n    \t// 复杂类型，还原 \n    \tObject obj = new ArrayList<>();\n    \tAssertions.assertEquals(SaFoxUtil.getValueByType(obj, List.class).getClass(), ArrayList.class);\n    }\n\n    @Test\n    public void joinParam() {\n    \t// 参数为空时，返回原url\n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc\", null), \"https://sa-token.cc\");\n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc\", \"\"), \"https://sa-token.cc\");\n    \t// url为空时，视为空字符串 \n    \tAssertions.assertEquals(SaFoxUtil.joinParam(null, \"id=1\"), \"?id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"\", \"id=1\"), \"?id=1\");\n    \t\n    \t// 各种情况的测试 \n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc\", \"id=1\"), \"https://sa-token.cc?id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc?\", \"id=1\"), \"https://sa-token.cc?id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc?name=zhang\", \"id=1\"), \"https://sa-token.cc?name=zhang&id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc?name=zhang&\", \"id=1\"), \"https://sa-token.cc?name=zhang&id=1\");\n    \t\n    \t// 重载方法测试 \n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc?name=zhang&\", \"id\", 1), \"https://sa-token.cc?name=zhang&id=1\");\n    \t// url或key为null时，不拼接 \n    \tAssertions.assertEquals(SaFoxUtil.joinParam(null, \"id\", 1), null);\n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc\", null, 1), \"https://sa-token.cc\");\n    \t// value为null时，会拼接出一个null字符串 \n    \tAssertions.assertEquals(SaFoxUtil.joinParam(\"https://sa-token.cc\", \"id\", null), \"https://sa-token.cc?id=null\");\n    }\n\n    @Test\n    public void joinSharpParam() {\n    \t// 参数为空时，返回原url\n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc\", null), \"https://sa-token.cc\");\n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc\", \"\"), \"https://sa-token.cc\");\n    \t// url为空时，视为空字符串 \n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(null, \"id=1\"), \"#id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"\", \"id=1\"), \"#id=1\");\n    \t\n    \t// 各种情况的测试 \n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc\", \"id=1\"), \"https://sa-token.cc#id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc#\", \"id=1\"), \"https://sa-token.cc#id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc#name=zhang\", \"id=1\"), \"https://sa-token.cc#name=zhang&id=1\");\n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc#name=zhang&\", \"id=1\"), \"https://sa-token.cc#name=zhang&id=1\");\n\n    \t// 重载方法测试 \n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc#name=zhang&\", \"id\", 1), \"https://sa-token.cc#name=zhang&id=1\");\n    \t// url或key为null时，不拼接 \n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(null, \"id\", 1), null);\n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc\", null, 1), \"https://sa-token.cc\");\n    \t// value为null时，会拼接出一个null字符串 \n    \tAssertions.assertEquals(SaFoxUtil.joinSharpParam(\"https://sa-token.cc\", \"id\", null), \"https://sa-token.cc#id=null\");\n    }\n\n    @Test\n    public void spliceTwoUrl() {\n    \t// 其中一个为null时，直接返回另一个\n    \tAssertions.assertEquals(SaFoxUtil.spliceTwoUrl(\"https://sa-sso-server.com/sso/auth\", null), \"https://sa-sso-server.com/sso/auth\");\n    \tAssertions.assertEquals(SaFoxUtil.spliceTwoUrl(null, \"https://sa-sso-server.com/sso/auth\"), \"https://sa-sso-server.com/sso/auth\");\n    \t\n    \t// 正常情况，拼接\n    \tAssertions.assertEquals(SaFoxUtil.spliceTwoUrl(\"https://sa-sso-server.com\", \"/sso/auth\"), \"https://sa-sso-server.com/sso/auth\");\n    \t\n    \t// url2以http开头时，直接返回url2 \n    \tAssertions.assertEquals(SaFoxUtil.spliceTwoUrl(\"https://sa-sso-server2.com\", \"https://sa-sso-server.com/sso/auth2\"), \"https://sa-sso-server.com/sso/auth2\");\n    }\n    \n    @Test\n    public void arrayJoin() {\n    \tAssertions.assertEquals(SaFoxUtil.arrayJoin(new String[] {\"a\", \"b\", \"c\"}), \"a,b,c\");\n    \tAssertions.assertEquals(SaFoxUtil.arrayJoin(new String[] {}), \"\");\n    \tAssertions.assertEquals(SaFoxUtil.arrayJoin(null), \"\");\n    }\n\n    @Test\n    public void isUrl() {\n    \tAssertions.assertTrue(SaFoxUtil.isUrl(\"https://sa-token.cc\"));\n    \tAssertions.assertTrue(SaFoxUtil.isUrl(\"https://www.baidu.com/\"));\n\n    \tAssertions.assertFalse(SaFoxUtil.isUrl(null));\n    \tAssertions.assertFalse(SaFoxUtil.isUrl(\"\"));\n    \tAssertions.assertFalse(SaFoxUtil.isUrl(\"htt://www.baidu.com/\"));\n    \tAssertions.assertFalse(SaFoxUtil.isUrl(\"https:www.baidu.com/\"));\n    \tAssertions.assertFalse(SaFoxUtil.isUrl(\"httpswwwbaiducom/\"));\n    \tAssertions.assertFalse(SaFoxUtil.isUrl(\"https://www.baidu.com/,\"));\n    }\n\n    @Test\n    public void encodeUrl() {\n    \tAssertions.assertEquals(SaFoxUtil.encodeUrl(\"https://sa-token.cc\"), \"https%3A%2F%2Fsa-token.cc\");\n    \tAssertions.assertEquals(SaFoxUtil.decoderUrl(\"https%3A%2F%2Fsa-token.cc\"), \"https://sa-token.cc\");\n    }\n    \n    @Test\n    public void convertStringToList() {\n    \tList<String> list = SaFoxUtil.convertStringToList(\"a,b,,c\");\n    \tAssertions.assertEquals(list.size(), 3);\n    \tAssertions.assertEquals(list.get(0), \"a\");\n    \tAssertions.assertEquals(list.get(1), \"b\");\n    \tAssertions.assertEquals(list.get(2), \"c\");\n\n    \tList<String> list2 = SaFoxUtil.convertStringToList(\"a,\");\n    \tAssertions.assertEquals(list2.size(), 1);\n\n    \tList<String> list3 = SaFoxUtil.convertStringToList(\",\");\n    \tAssertions.assertEquals(list3.size(), 0);\n\n    \tList<String> list4 = SaFoxUtil.convertStringToList(\"\");\n    \tAssertions.assertEquals(list4.size(), 0);\n\n    \tList<String> list5 = SaFoxUtil.convertStringToList(null);\n    \tAssertions.assertEquals(list5.size(), 0);\n    }\n\n    @Test\n    public void convertListToString() {\n    \t// 正常\n    \tList<String> list = Arrays.asList(\"a\", \"b\", \"c\");\n    \tAssertions.assertEquals(SaFoxUtil.convertListToString(list), \"a,b,c\");\n\n    \t// 空数组 \n    \tList<String> list2 = Arrays.asList();\n    \tAssertions.assertEquals(SaFoxUtil.convertListToString(list2), \"\");\n    \t\n    \t// 空 \n    \tList<String> list3 = null;\n    \tAssertions.assertEquals(SaFoxUtil.convertListToString(list3), \"\");\n    }\n\n    @Test\n    public void convertStringToArray() {\n    \tString[] array = SaFoxUtil.convertStringToArray(\"a,b,c\");\n    \tAssertions.assertEquals(array.length, 3);\n    \tAssertions.assertEquals(array[0], \"a\");\n    \tAssertions.assertEquals(array[1], \"b\");\n    \tAssertions.assertEquals(array[2], \"c\");\n\n    \tString[] array2 = SaFoxUtil.convertStringToArray(\"a,\");\n    \tAssertions.assertEquals(array2.length, 1);\n\n    \tString[] array3 = SaFoxUtil.convertStringToArray(\",\");\n    \tAssertions.assertEquals(array3.length, 0);\n\n    \tString[] array4 = SaFoxUtil.convertStringToArray(\"\");\n    \tAssertions.assertEquals(array4.length, 0);\n\n    \tString[] array5 = SaFoxUtil.convertStringToArray(null);\n    \tAssertions.assertEquals(array5.length, 0);\n    }\n\n    @Test\n    public void convertArrayToString() {\n    \t// 正常 \n    \tString[] array = new String[] {\"a\", \"b\", \"c\"};\n    \tAssertions.assertEquals(SaFoxUtil.convertArrayToString(array), \"a,b,c\");\n\n    \t// null\n    \tString[] array2 = null;\n    \tAssertions.assertEquals(SaFoxUtil.convertArrayToString(array2), \"\");\n    \t\n    \t// 空数组 \n    \tString[] array3 = new String[] {};\n    \tAssertions.assertEquals(SaFoxUtil.convertArrayToString(array3), \"\");\n    }\n\n    @Test\n    public void emptyList() {\n    \tList<String> list = SaFoxUtil.emptyList();\n    \tAssertions.assertEquals(list.size(), 0);\n    }\n\n    @Test\n    public void toList() {\n    \tList<String> list = SaFoxUtil.toList(\"a\",\"b\", \"c\");\n    \tAssertions.assertEquals(list.size(), 3);\n    \tAssertions.assertEquals(list.get(0), \"a\");\n    \tAssertions.assertEquals(list.get(1), \"b\");\n    \tAssertions.assertEquals(list.get(2), \"c\");\n    }\n\n\t@Test\n\tpublic void hasNonPrintableASCII() {\n\t\tAssertions.assertFalse(SaFoxUtil.hasNonPrintableASCII(\"Hello World!\"));\n\t\tAssertions.assertTrue(SaFoxUtil.hasNonPrintableASCII(\"Hello\\u0007World\"));\n\t\tAssertions.assertTrue(SaFoxUtil.hasNonPrintableASCII(\"Hello\\tWorld\"));\n\t\tAssertions.assertTrue(SaFoxUtil.hasNonPrintableASCII(\"Hello\\nWorld\"));\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/util/SaResultTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.core.util;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * SaResult 结果集 测试 \n * \n * @author click33\n * @since 2022-2-8 22:14:25\n */\npublic class SaResultTest {\n\n\t// 构造函数构建 \n    @Test\n    public void test() {\n    \t// 无参构造时，默认所有参数为null \n    \tSaResult res = new SaResult();\n    \tAssertions.assertEquals(res.getCode(), null);\n    \tAssertions.assertEquals(res.getMsg(), null);\n    \tAssertions.assertEquals(res.getData(), null);\n    \t\n    \t// 全参数构造 \n    \tSaResult res2 = new SaResult(200, \"ok\", \"zhangsan\");\n    \tAssertions.assertEquals((int)res2.getCode(), 200);\n    \tAssertions.assertEquals(res2.getMsg(), \"ok\");\n    \tAssertions.assertEquals(res2.getData(), \"zhangsan\");\n    \t\n    \t// 自定义写值取值 \n    \tres.set(\"age\", 18);\n    \tAssertions.assertEquals(res.get(\"age\"), 18);\n    \tAssertions.assertEquals(res.get(\"age\", String.class), \"18\");\n    \tAssertions.assertEquals(res.getOrDefault(\"age\", 20), 18);\n    \tAssertions.assertEquals(res.getOrDefault(\"age2\", 20), 20);\n    }\n\n    // 静态函数快速构建 \n    @Test\n    public void test2() {\n    \t// ok 和 error\n    \tAssertions.assertEquals((int)SaResult.ok().getCode(), 200);\n    \tAssertions.assertEquals((int)SaResult.error().getCode(), 500);\n    \tAssertions.assertEquals(SaResult.error(\"错误\").getMsg(), \"错误\");\n\n    \t// 指定code\n    \tSaResult res = SaResult.code(201);\n    \tAssertions.assertEquals((int)res.getCode(), 201);\n    \t\n    \t// \n    \t// 全参数构造 \n    \tSaResult res2 = SaResult.get(200, \"ok\", \"zhangsan\");\n    \tAssertions.assertEquals((int)res2.getCode(), 200);\n    \tAssertions.assertEquals(res2.getMsg(), \"ok\");\n    \tAssertions.assertEquals(res2.getData(), \"zhangsan\");\n    \t// 序列化\n    \tAssertions.assertEquals(res2.toString(), \"{\\\"code\\\": 200, \\\"msg\\\": \\\"ok\\\", \\\"data\\\": \\\"zhangsan\\\"}\");\n    \t// data 为 int 时的序列化 \n    \tres2.setData(1);\n    \tAssertions.assertEquals(res2.toString(), \"{\\\"code\\\": 200, \\\"msg\\\": \\\"ok\\\", \\\"data\\\": 1}\");\n    \t\n    \t// Map 构造\n    \tMap<String, Object> map = new HashMap<>();\n    \tmap.put(\"key1\", \"value1\");\n    \tmap.put(\"key2\", \"value2\");\n    \tSaResult res4 = new SaResult(map);\n    \tAssertions.assertEquals(res4.get(\"key1\"), \"value1\");\n    \tAssertions.assertEquals(res4.get(\"key2\"), \"value2\");\n    }\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/StartUpApplication.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * 启动类 \n * @author Auster\n *\n */\n@SpringBootApplication\npublic class StartUpApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(StartUpApplication.class, args);\n\t}\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationController.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.annotation;\n\nimport cn.dev33.satoken.annotation.*;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * 测试注解用的Controller \n * \n * @author click33\n * @since 2022-9-2\n */\n@RestController\n@RequestMapping(\"/at/\")\npublic class SaAnnotationController {\n\n\t// 登录 \n\t@RequestMapping(\"login\")\n\tpublic SaResult login(long id) {\n\t\tStpUtil.login(id);\n\t\treturn SaResult.ok().set(\"token\", StpUtil.getTokenValue());\n\t}\n\n\t// 登录校验 \n\t@SaCheckLogin\n\t@RequestMapping(\"checkLogin\")\n\tpublic SaResult checkLogin() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 角色校验 \n\t@SaCheckRole(\"admin\")\n\t@RequestMapping(\"checkRole\")\n\tpublic SaResult checkRole() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限校验 \n\t@SaCheckPermission(\"art-add\")\n\t@RequestMapping(\"checkPermission\")\n\tpublic SaResult checkPermission() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 权限校验 or 角色校验 \n\t@SaCheckPermission(value = \"art-add2\", orRole = \"admin\")\n\t@RequestMapping(\"checkPermission2\")\n\tpublic SaResult checkPermission2() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 开启二级认证 \n\t@RequestMapping(\"openSafe\")\n\tpublic SaResult openSafe() {\n\t\tStpUtil.openSafe(120);\n\t\treturn SaResult.ok();\n\t}\n\n\t// 二级认证校验\n\t@SaCheckSafe\n\t@RequestMapping(\"checkSafe\")\n\tpublic SaResult checkSafe() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 封禁账号 \n\t@RequestMapping(\"disable\")\n\tpublic SaResult disable(long id) {\n\t\tStpUtil.disable(id, \"comment\", 200);\n\t\treturn SaResult.ok();\n\t}\n\n\t// 服务封禁校验 \n\t@SaCheckDisable(\"comment\")\n\t@RequestMapping(\"checkDisable\")\n\tpublic SaResult checkDisable() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 解封账号 \n\t@RequestMapping(\"untieDisable\")\n\tpublic SaResult untieDisable(long id) {\n\t\tStpUtil.untieDisable(id, \"comment\");\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationControllerTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.annotation;\n\nimport java.util.Map;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.servlet.MockMvc;\nimport org.springframework.test.web.servlet.MvcResult;\nimport org.springframework.test.web.servlet.request.MockMvcRequestBuilders;\nimport org.springframework.test.web.servlet.result.MockMvcResultMatchers;\nimport org.springframework.test.web.servlet.setup.MockMvcBuilders;\nimport org.springframework.web.context.WebApplicationContext;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.integrate.StartUpApplication;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 注解鉴权测试\n *\n * @author Auster\n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class SaAnnotationControllerTest {\n\n\t@Autowired\n\tprivate WebApplicationContext wac;\n\n\tprivate MockMvc mvc;\n\n\t// 每个方法前执行\n\t@BeforeEach\n\tpublic void before() {\n\t\tmvc = MockMvcBuilders.webAppContextSetup(wac).build();\n\t}\n\n\t// 校验通过的情况\n\t@Test\n\tpublic void testPassing() {\n\t\t// 登录拿到Token\n\t\tSaResult res = request(\"/at/login?id=10001\");\n\t\tString satoken = res.get(\"token\", String.class);\n\t\tAssertions.assertNotNull(satoken);\n\n\t\t// 登录校验，通过\n\t\tSaResult res2 = request(\"/at/checkLogin?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res2.getCode(), 200);\n\n\t\t// 角色校验，通过\n\t\tSaResult res3 = request(\"/at/checkRole?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res3.getCode(), 200);\n\n\t\t// 权限校验，通过\n\t\tSaResult res4 = request(\"/at/checkPermission?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res4.getCode(), 200);\n\n\t\t// 权限校验or角色校验，通过\n\t\tSaResult res5 = request(\"/at/checkPermission2?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res5.getCode(), 200);\n\n\t\t// 开启二级认证\n\t\tSaResult res6 = request(\"/at/openSafe?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res6.getCode(), 200);\n\n\t\t// 校验二级认证，通过\n\t\tSaResult res7 = request(\"/at/checkSafe?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res7.getCode(), 200);\n\n\t\t// 访问校验封禁的接口 ，通过\n\t\tSaResult res9 = request(\"/at/checkDisable?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res9.getCode(), 200);\n\t}\n\n\t// 校验不通过的情况\n\t@Test\n\tpublic void testNotPassing() {\n\t\t// 登录拿到Token\n\t\tSaResult res = request(\"/at/login?id=10002\");\n\t\tString satoken = res.get(\"token\", String.class);\n\t\tAssertions.assertNotNull(satoken);\n\n\t\t// 登录校验，不通过\n\t\tSaResult res2 = request(\"/at/checkLogin\");\n\t\tAssertions.assertEquals(res2.getCode(), 401);\n\n\t\t// 角色校验，不通过\n\t\tSaResult res3 = request(\"/at/checkRole?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res3.getCode(), 402);\n\n\t\t// 权限校验，不通过\n\t\tSaResult res4 = request(\"/at/checkPermission?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res4.getCode(), 403);\n\n\t\t// 权限校验or角色校验，不通过\n\t\tSaResult res5 = request(\"/at/checkPermission2?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res5.getCode(), 403);\n\n\t\t// 校验二级认证，不通过\n\t\tSaResult res7 = request(\"/at/checkSafe?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res7.getCode(), 901);\n\n\t\t// -------- 登录拿到Token\n\t\tString satoken10042 = request(\"/at/login?id=10042\").get(\"token\", String.class);\n\t\tAssertions.assertNotNull(satoken10042);\n\n\t\t// 校验账号封禁 ，通过\n\t\tSaResult res8 = request(\"/at/disable?id=10042\");\n\t\tAssertions.assertEquals(res8.getCode(), 200);\n\n\t\t// 访问校验封禁的接口 ，不通过\n\t\tSaResult res9 = request(\"/at/checkDisable?satoken=\" + satoken10042);\n\t\tAssertions.assertEquals(res9.getCode(), 904);\n\n\t\t// 解封后就能访问了\n\t\trequest(\"/at/untieDisable?id=10042\");\n\t\tSaResult res10 = request(\"/at/checkDisable?satoken=\" + satoken10042);\n\t\tAssertions.assertEquals(res10.getCode(), 200);\n\t}\n\n\t// 测试忽略认证\n\t@Test\n\tpublic void testIgnore() {\n\t\t// 必须登录才能访问的\n\t\tSaResult res1 = request(\"/ig/show1\");\n\t\tAssertions.assertEquals(res1.getCode(), 401);\n\n\t\t// 不登录也可以访问的\n\t\tSaResult res2 = request(\"/ig/show2\");\n\t\tAssertions.assertEquals(res2.getCode(), 200);\n\t}\n\n\t// 封装请求\n\tprivate SaResult request(String path) {\n\t\ttry {\n\t\t\t// 发请求\n\t\t\tMvcResult mvcResult = mvc.perform(\n\t\t\t\t\t\t\tMockMvcRequestBuilders.post(path)\n\t\t\t\t\t\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t\t\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t\t)\n\t\t\t\t\t.andExpect(MockMvcResultMatchers.status().isOk())\n\t\t\t\t\t.andReturn();\n\n\t\t\t// 转 Map\n\t\t\tString content = mvcResult.getResponse().getContentAsString();\n\t\t\tMap<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(content);\n\n\t\t\t// 转 SaResult 对象\n\t\t\treturn new SaResult().setMap(map);\n\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/annotation/SaAnnotationIgnoreController.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.annotation;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.annotation.SaCheckLogin;\nimport cn.dev33.satoken.annotation.SaIgnore;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 测试注解用的Controller \n * \n * @author click33\n * @since 2022-9-2\n */\n@SaCheckLogin\n@RestController\n@RequestMapping(\"/ig/\")\npublic class SaAnnotationIgnoreController {\n\n\t// 需要登录后访问  \n\t@RequestMapping(\"show1\")\n\tpublic SaResult show1() {\n\t\treturn SaResult.ok();\n\t}\n\n\t// 不登录也可访问  \n\t@SaIgnore\n\t@RequestMapping(\"show2\")\n\tpublic SaResult show2() {\n\t\treturn SaResult.ok();\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/HandlerException.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure;\n\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\nimport cn.dev33.satoken.exception.DisableServiceException;\nimport cn.dev33.satoken.exception.NotHttpBasicAuthException;\nimport cn.dev33.satoken.exception.NotLoginException;\nimport cn.dev33.satoken.exception.NotPermissionException;\nimport cn.dev33.satoken.exception.NotRoleException;\nimport cn.dev33.satoken.exception.NotSafeException;\nimport cn.dev33.satoken.exception.SameTokenInvalidException;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 全局异常处理 \n * @author click33\n *\n */\n@RestControllerAdvice\npublic class HandlerException {\n\n\t// 未登录异常，code=401\n\t@ExceptionHandler(NotLoginException.class)\n\tpublic SaResult handlerNotLoginException(NotLoginException e) {\n\t\treturn SaResult.error().setCode(401);\n\t}\n\n\t// 缺少角色异常，code=402\n\t@ExceptionHandler(NotRoleException.class)\n\tpublic SaResult handlerNotRoleException(NotRoleException e) {\n\t\treturn SaResult.error().setCode(402);\n\t}\n\n\t// 缺少权限异常，code=403\n\t@ExceptionHandler(NotPermissionException.class)\n\tpublic SaResult handlerNotPermissionException(NotPermissionException e) {\n\t\treturn SaResult.error().setCode(403);\n\t}\n\n\t// 二级认证失败，code=901\n\t@ExceptionHandler(NotSafeException.class)\n\tpublic SaResult handlerNotSafeException(NotSafeException e) {\n\t\treturn SaResult.error().setCode(901);\n\t}\n\n\t// same-token 校验失败，code=902\n\t@ExceptionHandler(SameTokenInvalidException.class)\n\tpublic SaResult handlerSameTokenInvalidException(SameTokenInvalidException e) {\n\t\treturn SaResult.error().setCode(902);\n\t}\n\n\t// Http Basic 校验失败，code=903\n\t@ExceptionHandler(NotHttpBasicAuthException.class)\n\tpublic SaResult handlerNotBasicAuthException(NotHttpBasicAuthException e) {\n\t\treturn SaResult.error().setCode(903);\n\t}\n\n\t// 服务被封禁 ，code=904\n\t@ExceptionHandler(DisableServiceException.class)\n\tpublic SaResult handlerDisableServiceException(DisableServiceException e) {\n\t\treturn SaResult.error().setCode(904);\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/SaTokenConfigure.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure;\n\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\nimport cn.dev33.satoken.interceptor.SaInterceptor;\n\n/**\n * Sa-Token 相关配置类\n * \n * @author click33\n * @since 2022-9-2\n */\n@Configuration\npublic class SaTokenConfigure implements WebMvcConfigurer {\n   \n\t// 注册 Sa-Token 拦截器，打开注解式鉴权功能 \n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n\n        // 测试环境下上下文过滤器不生效，所以此处从拦截器需要补充上下文\n        registry.addInterceptor(new SaInterceptor(handle -> {\n            SaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse());\n        }).isAnnotation(false)).addPathPatterns(\"/**\");\n\n        // 注册 Sa-Token 拦截器，打开注解式鉴权功能 \n        registry.addInterceptor(new SaInterceptor()).addPathPatterns(\"/**\");\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/StpInterfaceImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 自定义权限验证接口扩展 \n * \n * @author click33\n *\n */\n@Component\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\tint id = SaFoxUtil.getValueByType(loginId, int.class);\n\t\tif(id == 10001) {\n\t\t\treturn Arrays.asList(\"user*\", \"art-add\", \"art-delete\", \"art-update\", \"art-get\");\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\tint id = SaFoxUtil.getValueByType(loginId, int.class);\n\t\tif(id == 10001) {\n\t\t\treturn Arrays.asList(\"admin\", \"super-admin\");\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaBasicTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicTemplate;\n\n@Component\npublic class MySaBasicTemplate extends SaHttpBasicTemplate {\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaOAuth2Template.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.oauth2.template.SaOAuth2Template;\n\n/**\n * 自定义 Sa-OAuth2 模板方法 \n * \n * @author click33\n * @since 2022-9-5\n */\n@Component\npublic class MySaOAuth2Template extends SaOAuth2Template {\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSameTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.same.SaSameTemplate;\n\n@Component\npublic class MySaSameTemplate extends SaSameTemplate {\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSignTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport cn.dev33.satoken.sign.template.SaSignTemplate;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class MySaSignTemplate extends SaSignTemplate {\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaSsoTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.sso.template.SaSsoTemplate;\n\n/**\n * 自定义 Sa-SSO 模板方法 \n * \n * @author click33\n * @since 2022-9-5\n */\n@Component\npublic class MySaSsoTemplate extends SaSsoTemplate {\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport cn.dev33.satoken.temp.SaTempTemplate;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class MySaTempTemplate extends SaTempTemplate {\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenDao.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.dao.SaTokenDaoDefaultImpl;\n\n@Component\npublic class MySaTokenDao extends SaTokenDaoDefaultImpl {\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTokenListener.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.listener.SaTokenListenerForSimple;\n\n@Component\npublic class MySaTokenListener extends SaTokenListenerForSimple {\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MyStpLogic.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.configure.inject;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\n\n@Component\npublic class MyStpLogic extends StpLogic {\n\n\tpublic MyStpLogic() {\n\t\tsuper(StpUtil.TYPE);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginController.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.login;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 登录测试 \n * \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/acc/\")\npublic class LoginController {\n\n    // 测试登录  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456\n    @RequestMapping(\"doLogin\")\n    public SaResult doLogin(String name, String pwd) {\n        // 此处仅作模拟示例，真实项目需要从数据库中查询数据进行比对 \n        if(\"zhang\".equals(name) && \"123456\".equals(pwd)) {\n            StpUtil.login(10001);\n            return SaResult.ok(\"登录成功\").set(\"token\", StpUtil.getTokenValue());\n        }\n        return SaResult.error(\"登录失败\");\n    }\n\n    // 查询登录状态  ---- http://localhost:8081/acc/isLogin\n    @RequestMapping(\"isLogin\")\n    public SaResult isLogin() {\n        return SaResult.data(StpUtil.isLogin());\n    }\n\n    // 查询 Token 信息  ---- http://localhost:8081/acc/tokenInfo\n    @RequestMapping(\"tokenInfo\")\n    public SaResult tokenInfo() {\n        return SaResult.data(StpUtil.getTokenInfo());\n    }\n\n    // 测试注销  ---- http://localhost:8081/acc/logout\n    @RequestMapping(\"logout\")\n    public SaResult logout() {\n        StpUtil.logout();\n        return SaResult.ok();\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/login/LoginControllerTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.login;\n\nimport java.util.Map;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.servlet.MockMvc;\nimport org.springframework.test.web.servlet.MvcResult;\nimport org.springframework.test.web.servlet.request.MockMvcRequestBuilders;\nimport org.springframework.test.web.servlet.result.MockMvcResultMatchers;\nimport org.springframework.test.web.servlet.setup.MockMvcBuilders;\nimport org.springframework.web.context.WebApplicationContext;\n\nimport cn.dev33.satoken.integrate.StartUpApplication;\nimport cn.dev33.satoken.util.SoMap;\n\n/**\n * Sa-Token 登录API测试 \n * \n * @author click33 \n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\n@SuppressWarnings(\"deprecation\")\npublic class LoginControllerTest {\n\t\n\t@Autowired\n\tprivate WebApplicationContext wac;\n\t \n\tprivate MockMvc mvc;\n\t\n\t// 开始 \n\t@BeforeEach\n    public void before() {\n\t\tmvc = MockMvcBuilders.webAppContextSetup(wac).build();\n    }\n\t\n    @Test\n    public void testLogin() throws Exception{\n    \t// 请求 \n\t\tMvcResult mvcResult = mvc.perform(\n\t\t\tMockMvcRequestBuilders.post(\"/acc/doLogin\")\n\t\t\t\t.param(\"name\", \"zhang\")\n\t\t\t\t.param(\"pwd\", \"123456\")\n\t\t\t\t.contentType(MediaType.APPLICATION_JSON_UTF8)\n\t\t\t\t.accept(MediaType.APPLICATION_JSON_UTF8)\n\t\t\t)\n\t\t\t.andExpect(MockMvcResultMatchers.status().isOk())\n\t\t\t.andReturn();\n\t\t\n\t\t// 拿到结果 \n\t\tSoMap so = SoMap.getSoMap().setJsonString(\n\t\t\t\tmvcResult.getResponse().getContentAsString()\n\t\t\t\t);\n\t\tString token = so.getString(\"token\");\n\t\t\n\t\t// 断言 \n\t\tAssertions.assertTrue(mvcResult.getResponse().getHeader(\"Set-Cookie\") != null);\n\t\tAssertions.assertEquals(so.getInt(\"code\"), 200);\n\t\tAssertions.assertNotNull(token);\n    }\n\n\t@Test\n\t@SuppressWarnings(\"unchecked\")\n    public void testLogin2() throws Exception{\n    \t// 获取token \n    \tSoMap so = request(\"/acc/doLogin?name=zhang&pwd=123456\");\n    \tAssertions.assertNotNull(so.getString(\"token\"));\n\n    \tString token = so.getString(\"token\");\n\n    \t// 是否登录\n    \tSoMap so2 = request(\"/acc/isLogin?satoken=\" + token);\n    \tAssertions.assertTrue(so2.getBoolean(\"data\"));\n\n    \t// tokenInfo \n    \tSoMap so3 = request(\"/acc/tokenInfo?satoken=\" + token);\n    \tSoMap so4 = SoMap.getSoMap((Map<String, ?>)so3.get(\"data\"));\n    \tAssertions.assertEquals(so4.getString(\"tokenName\"), \"satoken\");\n\t\tAssertions.assertEquals(so4.getString(\"tokenValue\"), token);\n    \t\n\t\t// 注销\n\t\trequest(\"/acc/logout?satoken=\" + token);\n\n    \t// 是否登录 \n    \tSoMap so5 = request(\"/acc/isLogin?satoken=\" + token);\n    \tAssertions.assertFalse(so5.getBoolean(\"data\"));\n    }\n    \n    // 封装请求 \n    private SoMap request(String path) throws Exception {\n    \tMvcResult mvcResult = mvc.perform(\n    \t\t\tMockMvcRequestBuilders.post(path)\n\t\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n    \t\t\t)\n    \t\t\t.andExpect(MockMvcResultMatchers.status().isOk())\n    \t\t\t.andReturn();\n    \t\n\t\tSoMap so = SoMap.getSoMap().setJsonString(\n\t\t\t\tmvcResult.getResponse().getContentAsString()\n\t\t\t\t);\n\t\t\n\t\treturn so;\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreController.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.more;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.util.SaFoxUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 其它测试 \n * \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/more/\")\npublic class MoreController {\n\n\t// 一些基本的测试 \n    @RequestMapping(\"getInfo\")\n    public SaResult getInfo() {\n    \tSaRequest req = SaHolder.getRequest();\n    \tboolean flag = \n    \t\t\tSaFoxUtil.equals(req.getParam(\"name\"), \"zhang\")\n    \t\t\t&& SaFoxUtil.equals(req.getParam(\"name2\", \"li\"), \"li\")\n    \t\t\t&& SaFoxUtil.equals(req.getParamNotNull(\"name\"), \"zhang\")\n    \t\t\t&& req.isParam(\"name\", \"zhang\")\n    \t\t\t&& req.isPath(\"/more/getInfo\")\n    \t\t\t&& req.hasParam(\"name\")\n    \t\t\t&& SaFoxUtil.equals(req.getHeader(\"div\"), \"val\")\n    \t\t\t&& SaFoxUtil.equals(req.getHeader(\"div\", \"zhang\"), \"val\")\n    \t\t\t&& SaFoxUtil.equals(req.getHeader(\"div2\", \"zhang\"), \"zhang\")\n    \t\t\t;\n\n    \tSaHolder.getResponse().setServer(\"sa-server\");\n    \treturn SaResult.data(flag);\n    }\n\n\t// Http Basic 认证 \n    @RequestMapping(\"basicAuth\")\n    public SaResult basicAuth() {\n    \tSaHttpBasicUtil.check(\"sa:123456\");\n    \treturn SaResult.ok();\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/more/MoreControllerTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.more;\n\nimport java.util.Map;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.servlet.MockMvc;\nimport org.springframework.test.web.servlet.MvcResult;\nimport org.springframework.test.web.servlet.request.MockMvcRequestBuilders;\nimport org.springframework.test.web.servlet.result.MockMvcResultMatchers;\nimport org.springframework.test.web.servlet.setup.MockMvcBuilders;\nimport org.springframework.web.context.WebApplicationContext;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.integrate.StartUpApplication;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.spring.SaTokenContextForSpring;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n\n/**\n * 其它测试 \n * \n * @author click33\n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class MoreControllerTest {\n\n\t@Autowired\n\tprivate WebApplicationContext wac;\n\t \n\tprivate MockMvc mvc;\n\n\t// 开始 \n\t@BeforeEach\n    public void before() {\n\t\tmvc = MockMvcBuilders.webAppContextSetup(wac).build();\n\t\t\n\t\t// 在单元测试时，通过 request.getServletPath() 获取到的请求路径为空，导致路由拦截不正确 \n\t\t// 虽然不知道为什么会这样，但是暂时可以通过以下方式来解决 \n\t\tSaManager.setSaTokenContext(new SaTokenContextForSpring() {\n\t\t\t@Override\n\t\t\tpublic SaRequest getRequest() {\n\t\t\t\treturn new SaRequestForServlet(SpringMVCUtil.getRequest()) {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic String getRequestPath() {\n\t\t\t\t\t\treturn request.getRequestURI();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t});\n\t\t\n    }\n\n\t// 基础API测试 \n\t@Test\n\tpublic void testApi() {\n\t\tSaResult res = request(\"/more/getInfo?name=zhang\");\n\t\tAssertions.assertEquals(res.getData(), true);\n\t}\n\n\t// Http Basic 认证 \n\t@Test\n\tpublic void testBasic() throws Exception {\n\t\t\n\t\t// ---------------- 认证不通过\n\t\tMvcResult mvcResult = mvc.perform(\n\t\t\t\tMockMvcRequestBuilders.post(\"/more/basicAuth\")\n\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t)\n\t\t\t.andExpect(MockMvcResultMatchers.status().is(401))\n\t\t\t.andReturn();\n\t\n\t\t// 转 Map \n\t\tString content = mvcResult.getResponse().getContentAsString();\n\t\tMap<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(content);\n\t\t// 转 SaResult 对象 \n\t\tSaResult res = new SaResult().setMap(map);\n\t\tAssertions.assertEquals(res.getCode(), 903);\n\t\t// 会有一个特殊响应头\n\t\tString header = mvcResult.getResponse().getHeader(\"WWW-Authenticate\");\n\t\tAssertions.assertEquals(header, \"Basic Realm=Sa-Token\");\n\t\t\n\t\t\n\t\t// ---------------- 认证通过\n    \tMvcResult mvcResult2 = mvc.perform(\n    \t\t\t\tMockMvcRequestBuilders.post(\"/more/basicAuth\")\n\t\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t\t.header(\"Authorization\", \"Basic c2E6MTIzNDU2\")\n    \t\t\t)\n    \t\t\t.andExpect(MockMvcResultMatchers.status().isOk())\n    \t\t\t.andReturn();\n    \t\n\t\t// 转 Map \n\t\tString content2 = mvcResult2.getResponse().getContentAsString();\n\t\tMap<String, Object> map2 = SaManager.getSaJsonTemplate().jsonToMap(content2);\n\t\t// 转 SaResult 对象 \n\t\tSaResult res2 = new SaResult().setMap(map2);\n\t\tAssertions.assertEquals(res2.getCode(), 200);\n\t}\n\t\n\n    // 封装请求 \n    private SaResult request(String path) {\n    \ttry {\n    \t\t// 发请求 \n        \tMvcResult mvcResult = mvc.perform(\n        \t\t\t\tMockMvcRequestBuilders.post(path)\n    \t\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n    \t\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n    \t\t\t\t\t.header(\"div\", \"val\")\n        \t\t\t)\n        \t\t\t.andExpect(MockMvcResultMatchers.status().isOk())\n        \t\t\t.andReturn();\n        \t\n    \t\t// 转 Map \n    \t\tString content = mvcResult.getResponse().getContentAsString();\n    \t\tMap<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(content);\n    \t\t\n    \t\t// 转 SaResult 对象 \n    \t\treturn new SaResult().setMap(map);\n    \t\t\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterController.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.router;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * 路由鉴权测试 \n * \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/rt/\")\npublic class RouterController {\n\n    @RequestMapping(\"getInfo\")\n    public SaResult getInfo() {\n    \treturn SaResult.ok();\n    }\n\n    @RequestMapping(\"getInfo*\")\n    public SaResult getInfo2() {\n    \treturn SaResult.ok();\n    }\n\n    // 读url \n    @RequestMapping(\"getInfo_101\")\n    public SaResult getInfo_101() {\n    \treturn SaResult.data(SaHolder.getRequest().getUrl());\n    }\n\n    // 读Cookie \n    @RequestMapping(\"getInfo_102\")\n    public SaResult getInfo_102() {\n    \treturn SaResult.data(SaHolder.getRequest().getCookieValue(\"x-token\"));\n    }\n\n    // 测试转发 \n    @RequestMapping(\"getInfo_103\")\n    public SaResult getInfo_103() {\n    \tSaHolder.getRequest().forward(\"/rt/getInfo_102\");\n    \treturn SaResult.ok();\n    }\n\n    // 空接口 \n    @RequestMapping(\"getInfo_200\")\n    public SaResult getInfo_200() {\n    \treturn SaResult.ok();\n    }\n    @RequestMapping(\"getInfo_201\")\n    public SaResult getInfo_201() {\n    \treturn SaResult.ok();\n    }\n    @RequestMapping(\"getInfo_202\")\n    public SaResult getInfo_202() {\n    \treturn SaResult.ok();\n    }\n\t@RequestMapping(\"login\")\n\tpublic SaResult login(long id) {\n\t\tStpUtil.login(id);\n\t\treturn SaResult.ok().set(\"token\", StpUtil.getTokenValue());\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/RouterControllerTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.router;\n\nimport java.util.Arrays;\nimport java.util.Map;\n\nimport javax.servlet.http.Cookie;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.servlet.MockMvc;\nimport org.springframework.test.web.servlet.MvcResult;\nimport org.springframework.test.web.servlet.request.MockMvcRequestBuilders;\nimport org.springframework.test.web.servlet.result.MockMvcResultMatchers;\nimport org.springframework.test.web.servlet.setup.MockMvcBuilders;\nimport org.springframework.web.context.WebApplicationContext;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.model.SaRequest;\nimport cn.dev33.satoken.integrate.StartUpApplication;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.router.SaRouterStaff;\nimport cn.dev33.satoken.servlet.model.SaRequestForServlet;\nimport cn.dev33.satoken.spring.SaTokenContextForSpring;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n\n/**\n * C Controller 测试 \n * \n * @author click33\n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class RouterControllerTest {\n\n\t@Autowired\n\tprivate WebApplicationContext wac;\n\t \n\tprivate MockMvc mvc;\n\n\t// 开始 \n\t@BeforeEach\n    public void before() {\n\t\tmvc = MockMvcBuilders.webAppContextSetup(wac).build();\n\t\t\n\t\t// 在单元测试时，通过 request.getServletPath() 获取到的请求路径为空，导致路由拦截不正确 \n\t\t// 虽然不知道为什么会这样，但是暂时可以通过以下方式来解决 \n\t\tSaManager.setSaTokenContext(new SaTokenContextForSpring() {\n\t\t\t@Override\n\t\t\tpublic SaRequest getRequest() {\n\t\t\t\treturn new SaRequestForServlet(SpringMVCUtil.getRequest()) {\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic String getRequestPath() {\n\t\t\t\t\t\treturn request.getRequestURI();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t});\n\t\t\n    }\n\n\t// 基础API测试 \n\t@Test\n\tpublic void testApi() {\n\t\t// 是否命中 \n    \tSaRouterStaff staff = SaRouter.match(false);\n    \tAssertions.assertFalse(staff.isHit());\n\n    \t// 重置 \n    \tstaff.reset();\n    \tAssertions.assertTrue(staff.isHit());\n    \t\n    \t// lambda 形式 \n    \tSaRouterStaff staff2 = SaRouter.match(r -> false);\n    \tAssertions.assertFalse(staff2.isHit());\n    \t\n    \t// 匹配 \n    \tAssertions.assertTrue(SaRouter.isMatch(\"/user/**\", \"/user/add\"));\n    \tAssertions.assertTrue(SaRouter.isMatch(new String[] {\"/user/**\", \"/art/**\", \"/goods/**\"}, \"/art/delete\"));\n    \tAssertions.assertTrue(SaRouter.isMatch(Arrays.asList(\"/user/**\", \"/art/**\", \"/goods/**\"), \"/art/delete\"));\n    \tAssertions.assertTrue(SaRouter.isMatch(new String[] {\"POST\", \"GET\", \"PUT\"},  \"GET\"));\n    \t\n    \t// 不匹配的 \n    \tAssertions.assertTrue(SaRouter.notMatch(false).isHit());\n    \tAssertions.assertTrue(SaRouter.notMatch(r -> false).isHit());\n\t}\n\t\n\t// 各种路由测试 \n\t@Test\n\tpublic void testRouter() {\n\t\t// getInfo \n\t\tSaResult res = request(\"/rt/getInfo?name=zhang\");\n\t\tAssertions.assertEquals(res.getCode(), 201);\n\t\t\n\t\t// getInfo2 \n\t\tSaResult res2 = request(\"/rt/getInfo2\");\n\t\tAssertions.assertEquals(res2.getCode(), 202);\n\n\t\t// getInfo3 \n\t\tSaResult res3 = request(\"/rt/getInfo3\");\n\t\tAssertions.assertEquals(res3.getCode(), 203);\n\n\t\t// getInfo4 \n\t\tSaResult res4 = request(\"/rt/getInfo4\");\n\t\tAssertions.assertEquals(res4.getCode(), 204);\n\t\t\n\t\t// getInfo5 \n\t\tSaResult res5 = request(\"/rt/getInfo5\");\n\t\tAssertions.assertEquals(res5.getCode(), 205);\n\t\t\n\t\t// getInfo6 \n\t\tSaResult res6 = request(\"/rt/getInfo6\");\n\t\tAssertions.assertEquals(res6.getCode(), 206);\n\t\t\n\t\t// getInfo7 \n\t\tSaResult res7 = request(\"/rt/getInfo7\");\n\t\tAssertions.assertEquals(res7.getCode(), 200);\n\t\t\n\t\t// getInfo8 \n\t\tSaResult res8 = request(\"/rt/getInfo8\");\n\t\tAssertions.assertEquals(res8.getCode(), 200);\n\t\t\n\t\t// getInfo9 \n\t\tSaResult res9 = request(\"/rt/getInfo9\");\n\t\tAssertions.assertEquals(res9.getCode(), 209);\n\t\t\n\t\t// getInfo10 \n\t\tSaResult res10 = request(\"/rt/getInfo10\");\n\t\tAssertions.assertEquals(res10.getCode(), 200);\n\t\t\n\t\t// getInfo11 \n\t\tSaResult res11 = request(\"/rt/getInfo11\");\n\t\tAssertions.assertEquals(res11.getCode(), 211);\n\t\t\n\t\t// getInfo12\n\t\tSaResult res12 = request(\"/rt/getInfo12\");\n\t\tAssertions.assertEquals(res12.getCode(), 212);\n\t\t\n\t\t// getInfo13\n\t\tSaResult res13 = request(\"/rt/getInfo13\");\n\t\tAssertions.assertEquals(res13.getCode(), 213);\n\t\t\n\t\t// getInfo14\n\t\tSaResult res14 = request(\"/rt/getInfo14\");\n\t\tAssertions.assertEquals(res14.getCode(), 214);\n\t\t\n\t\t// getInfo15\n\t\tSaResult res15 = request(\"/rt/getInfo15\");\n\t\tAssertions.assertEquals(res15.getCode(), 215);\n\t\t\n\t}\n\n\t// 测试 getUrl() \n\t@Test\n\tpublic void testGetUrl() {\n\t\t// getInfo_101 \n\t\tSaResult res = request(\"/rt/getInfo_101\");\n\t\tAssertions.assertTrue(res.getData().toString().endsWith(\"/rt/getInfo_101\"));\n\t\t\n\t\t// getInfo_101，不包括后面的参数 \n\t\tSaResult res2 = request(\"/rt/getInfo_101?id=1\");\n\t\tAssertions.assertTrue(res2.getData().toString().endsWith(\"/rt/getInfo_101\"));\n\t\t\n\t\t// 自定义当前域名 \n\t\tSaManager.getConfig().setCurrDomain(\"http://xxx.com\");\n\t\tSaResult res3 = request(\"/rt/getInfo_101?id=1\");\n\t\tAssertions.assertEquals(res3.getData().toString(), \"http://xxx.com/rt/getInfo_101\");\n\t\tSaManager.getConfig().setCurrDomain(null);\n\t}\n\n\t// 测试读取Cookie \n\t@Test\n\tpublic void testGetCookie() throws Exception {\n\t\tMvcResult mvcResult = mvc.perform(\n\t\t\t\tMockMvcRequestBuilders.post(\"/rt/getInfo_102\")\n\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t.cookie(new Cookie(\"x-token\", \"token-111\"))\n\t\t\t)\n\t\t\t.andExpect(MockMvcResultMatchers.status().is(200))\n\t\t\t.andReturn();\n\t\t\n\t\t// 转 Map \n\t\tString content = mvcResult.getResponse().getContentAsString();\n\t\tMap<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(content);\n\t\t\n\t\t// 转 SaResult 对象 \n\t\tSaResult res = new SaResult().setMap(map);\n\t\tAssertions.assertEquals(res.getData(), \"token-111\");\n\t}\n\t\n\t// 测试重定向 \n\t@Test\n\tpublic void testRedirect() throws Exception {\n\t\tMvcResult mvcResult = mvc.perform(\n\t\t\t\tMockMvcRequestBuilders.post(\"/rt/getInfo16\")\n\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n\t\t\t)\n\t\t\t.andExpect(MockMvcResultMatchers.status().is(302))\n\t\t\t.andReturn();\n\t\n\t\tAssertions.assertEquals(mvcResult.getResponse().getHeader(\"Location\"), \"/rt/getInfo3\");\n\t}\n\n\t// 空接口 \n\t@Test\n\tpublic void testGetInfo200() {\n//\t\tSaResult res = request(\"/rt/getInfo_200\");\n//\t\tAssertions.assertEquals(res.getCode(), 200);\n//\t\tSaResult res1 = request(\"/rt/getInfo_201\");\n//\t\tAssertions.assertEquals(res1.getCode(), 201);\n//\t\tSaResult res2 = request(\"/rt/getInfo_202\");\n//\t\tAssertions.assertEquals(res2.getCode(), 401);\n\t\t\n\t\t// 登录拿到Token \n    \tSaResult resLogin = request(\"/rt/login?id=10001\");\n    \tString satoken = resLogin.get(\"token\", String.class);\n\t\tSaResult res3 = request(\"/rt/getInfo_202?satoken=\" + satoken);\n\t\tAssertions.assertEquals(res3.getCode(), 200);\n\t}\n\n\t// 测试转发 \n\t@Test\n\tpublic void testForward() {\n\t\tSaResult res = request(\"/rt/getInfo_103\");\n\t\tAssertions.assertEquals(res.getCode(), 200);\n\t}\n\t\n    // 封装请求 \n    private SaResult request(String path) {\n    \ttry {\n    \t\t// 发请求 \n        \tMvcResult mvcResult = mvc.perform(\n        \t\t\t\tMockMvcRequestBuilders.post(path)\n    \t\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n    \t\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n        \t\t\t)\n        \t\t\t.andExpect(MockMvcResultMatchers.status().isOk())\n        \t\t\t.andReturn();\n        \t\n    \t\t// 转 Map \n    \t\tString content = mvcResult.getResponse().getContentAsString();\n    \t\tMap<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(content);\n    \t\t\n    \t\t// 转 SaResult 对象 \n    \t\treturn new SaResult().setMap(map);\n    \t\t\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/router/SaTokenConfigure2.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.router;\n\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.interceptor.SaInterceptor;\nimport cn.dev33.satoken.router.SaHttpMethod;\nimport cn.dev33.satoken.router.SaRouter;\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.util.SaResult;\nimport org.junit.jupiter.api.Assertions;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\nimport java.util.Arrays;\n\n/**\n * Sa-Token 相关配置类\n * \n * @author click33\n * @since 2022-9-2\n */\n@Configuration\npublic class SaTokenConfigure2 implements WebMvcConfigurer {\n\t\n    // 路由鉴权\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n\n\t\t// 测试环境下上下文过滤器不生效，所以此处从拦截器需要补充上下文\n\t\tregistry.addInterceptor(new SaInterceptor(handle -> {\n\t\t\tSaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse());\n\t\t}).isAnnotation(false)).addPathPatterns(\"/**\");\n\n        // 路由鉴权\n        registry.addInterceptor(new SaInterceptor(handle -> {})\n        \t\t.isAnnotation(true)\n        \t\t.setAuth(handle -> {\n\n        \t// 匹配 getInfo ，返回code=201 \n        \tSaRouter.match(\"/**\")\n        \t\t.match(SaHttpMethod.POST)\n        \t\t.matchMethod(\"POST\")\n        \t\t.match(SaHolder.getRequest().getMethod().equals(\"POST\"))\n        \t\t.match(r -> SaHolder.getRequest().isPath(\"/rt/getInfo\"))\n        \t\t.match(r -> SaHolder.getRequest().isParam(\"name\", \"zhang\"))\n        \t\t.back(SaResult.code(201));\n\n        \t// 匹配 getInfo2 ，返回code=202 \n        \tSaRouter.match(\"/rt/getInfo2\")\n        \t\t.match(Arrays.asList(\"/rt/getInfo2\", \"/rt/*\"))\n        \t\t.notMatch(\"/rt/getInfo3\")\n        \t\t.notMatch(false)\n        \t\t.notMatch(r -> false)\n        \t\t.notMatch(SaHttpMethod.GET)\n        \t\t.notMatchMethod(\"PUT\")\n        \t\t.notMatch(Arrays.asList(\"/rt/getInfo4\", \"/rt/getInfo5\"))\n        \t\t.back(SaResult.code(202));\n\n        \t// 匹配 getInfo3 ，返回code=203 \n        \tSaRouter.match(\"/rt/getInfo3\", \"/rt/getInfo4\", () -> SaRouter.back(SaResult.code(203)));\n        \tSaRouter.match(\"/rt/getInfo4\", \"/rt/getInfo5\", r -> SaRouter.back(SaResult.code(204)));\n        \tSaRouter.match(\"/rt/getInfo5\", () -> SaRouter.back(SaResult.code(205)));\n        \tSaRouter.match(\"/rt/getInfo6\", r -> SaRouter.back(SaResult.code(206)));\n        \t\n        \t// 通往 Controller  \n        \tSaRouter.match(Arrays.asList(\"/rt/getInfo7\")).stop();\n\n        \t// 通往 Controller  \n        \tSaRouter.match(\"/rt/getInfo8\", () -> SaRouter.stop());\n        \t\n        \tSaRouter.matchMethod(\"POST\").match(\"/rt/getInfo9\").free(r -> SaRouter.back(SaResult.code(209)));\n        \tSaRouter.match(SaHttpMethod.POST).match(\"/rt/getInfo10\").setHit(false).back();\n        \t\n        \t// 11\n        \tSaRouter.notMatch(\"/rt/getInfo11\").reset().match(\"/rt/getInfo11\").back(SaResult.code(211));\n        \tSaRouter.notMatch(SaHttpMethod.GET).match(\"/rt/getInfo12\").back(SaResult.code(212));\n        \tSaRouter.notMatch(Arrays.asList(\"/rt/getInfo12\", \"/rt/getInfo14\")).match(\"/rt/getInfo13\").back(SaResult.code(213));\n        \tSaRouter.notMatchMethod(\"GET\", \"PUT\").match(\"/rt/getInfo14\").back(SaResult.code(214));\n        \t\n//        \tSaRouter.match(Arrays.asList(\"/rt/getInfo15\", \"/rt/getInfo16\"))\n        \tif(SaRouter.isMatchCurrURI(\"/rt/getInfo15\")) {\n    \t\t\tif(SaHolder.getRequest().getCookieValue(\"ddd\") == null\n    \t\t\t\t\t&& SaHolder.getStorage().getSource() == SpringMVCUtil.getRequest()\n    \t\t\t\t\t&& SaHolder.getRequest().getSource() == SpringMVCUtil.getRequest()\n    \t\t\t\t\t&& SaHolder.getResponse().getSource() == SpringMVCUtil.getResponse()\n    \t\t\t\t\t) {\n    \t\t\t\tSaRouter.newMatch().free(r -> SaRouter.back(SaResult.code(215)));\n    \t\t\t}\n        \t}\n        \t\n        \tSaRouter.match(\"/rt/getInfo16\", () -> {\n        \t\tAssertions.assertThrows(Exception.class, () -> SaHolder.getResponse().redirect(null));\n        \t\tSaHolder.getResponse().redirect(\"/rt/getInfo3\");\n        \t});\n        \t\n        })).addPathPatterns(\"/**\");\n    }\n\n}\n\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenController.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.same;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * same-token Controller \n * \n * @author click33\n *\n */\n@RestController\n@RequestMapping(\"/same/\")\npublic class SaSameTokenController {\n\n\t// 获取信息 \n\t@RequestMapping(\"getInfo\")\n\tpublic SaResult getInfo() {\n\t\t// 获取并校验same-token \n\t\tString sameToken = SpringMVCUtil.getRequest().getHeader(SaSameUtil.SAME_TOKEN);\n\t\tSaSameUtil.checkToken(sameToken);\n\t\t// 返回信息 \n\t\treturn SaResult.data(\"info=zhangsan\");\n\t}\n\n\t// 获取信息2 \n\t@RequestMapping(\"getInfo2\")\n\tpublic SaResult getInfo2() {\n\t\t// 获取并校验same-token \n\t\tSaSameUtil.checkCurrentRequestToken();\n\t\t// 返回信息 \n\t\treturn SaResult.data(\"info=zhangsan2\");\n\t}\n\t\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/same/SaSameTokenControllerTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.integrate.same;\n\nimport java.util.Map;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.servlet.MockMvc;\nimport org.springframework.test.web.servlet.MvcResult;\nimport org.springframework.test.web.servlet.request.MockMvcRequestBuilders;\nimport org.springframework.test.web.servlet.result.MockMvcResultMatchers;\nimport org.springframework.test.web.servlet.setup.MockMvcBuilders;\nimport org.springframework.web.context.WebApplicationContext;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.exception.SameTokenInvalidException;\nimport cn.dev33.satoken.integrate.StartUpApplication;\nimport cn.dev33.satoken.same.SaSameUtil;\nimport cn.dev33.satoken.util.SaResult;\n\n/**\n * same-token Controller 测试 \n * \n * @author click33\n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class SaSameTokenControllerTest {\n\n\t@Autowired\n\tprivate WebApplicationContext wac;\n\t \n\tprivate MockMvc mvc;\n\t\n\t// 开始 \n\t@BeforeEach\n    public void before() {\n\t\tmvc = MockMvcBuilders.webAppContextSetup(wac).build();\n    }\n\t\n\t// 获取信息 \n\t@Test\n\tpublic void testGetInfo() {\n\t\tString token = SaSameUtil.getToken();\n\t\t// 加token，能调通 \n\t\tSaResult res = request(\"/same/getInfo\", token);\n\t\tAssertions.assertEquals(res.getCode(), 200);\n\t\t// 不加token，不能调通 \n\t\tSaResult res2 = request(\"/same/getInfo\", \"xxx\");\n\t\tAssertions.assertEquals(res2.getCode(), 902);\n\n\t\t// 获取信息2  \n\t\ttoken = SaSameUtil.getTokenNh();\n\t\t// 加token，能调通 \n\t\tSaResult res3 = request(\"/same/getInfo2\", token);\n\t\tAssertions.assertEquals(res3.getCode(), 200);\n\t\t// 不加token，不能调通 \n\t\tSaResult res4 = request(\"/same/getInfo2\", \"xxx\");\n\t\tAssertions.assertEquals(res4.getCode(), 902);\n\t}\n\n\t// 基础测试 \n\t@Test\n\tpublic void testApi() {\n\t\tString token = SaSameUtil.getToken();\n\t\t\n\t\t// 刷新一下，会有变化 \n\t\tSaSameUtil.refreshToken();\n\t\tString token2 = SaSameUtil.getToken();\n\t\tAssertions.assertNotEquals(token, token2);\n\t\t\n\t\t// 旧token，变为次级token\n\t\tString pastToken = SaSameUtil.getPastTokenNh();\n\t\tAssertions.assertEquals(token, pastToken);\n\t\t\n\t\t// dao中应该有值 \n\t\tString daoToken = SaManager.getSaTokenDao().get(\"satoken:var:same-token\");\n\t\tString daoToken2 = SaManager.getSaTokenDao().get(\"satoken:var:past-same-token\");\n\t\tAssertions.assertEquals(token2, daoToken);\n\t\tAssertions.assertEquals(token, daoToken2);\n\t\t\n\t\t// 新旧都有效 \n\t\tAssertions.assertTrue(SaSameUtil.isValid(token));\n\t\tAssertions.assertTrue(SaSameUtil.isValid(token2));\n\t\t\n\t\t// 空的不行 \n\t\tAssertions.assertFalse(SaSameUtil.isValid(null));\n\t\tAssertions.assertFalse(SaSameUtil.isValid(\"\"));\n\t\t\n\t\t// 不抛出异常 \n\t\tAssertions.assertDoesNotThrow(() -> SaSameUtil.checkToken(token));\n\t\tAssertions.assertDoesNotThrow(() -> SaSameUtil.checkToken(token2));\n\t\t\n\t\t// 抛出异常\n\t\tAssertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken(null));\n\t\tAssertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken(\"\"));\n\t\tAssertions.assertThrows(SameTokenInvalidException.class, () -> SaSameUtil.checkToken(\"aaa\"));\n\t}\n\n\t\n\t\n    // 封装请求 \n    private SaResult request(String path, String sameToken) {\n    \ttry {\n    \t\t// 发请求 \n        \tMvcResult mvcResult = mvc.perform(\n        \t\t\t\tMockMvcRequestBuilders.post(path)\n    \t\t\t\t\t.contentType(MediaType.APPLICATION_PROBLEM_JSON)\n    \t\t\t\t\t.accept(MediaType.APPLICATION_PROBLEM_JSON)\n    \t\t\t\t\t.header(SaSameUtil.SAME_TOKEN, sameToken)\n        \t\t\t)\n        \t\t\t.andExpect(MockMvcResultMatchers.status().isOk())\n        \t\t\t.andReturn();\n        \t\n    \t\t// 转 Map \n    \t\tString content = mvcResult.getResponse().getContentAsString();\n    \t\tMap<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(content);\n    \t\t\n    \t\t// 转 SaResult 对象 \n    \t\treturn new SaResult().setMap(map);\n    \t\t\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n    }\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/BasicsTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.springboot;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.context.SaHolder;\nimport cn.dev33.satoken.context.SaTokenContext;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.*;\nimport cn.dev33.satoken.filter.SaServletFilter;\nimport cn.dev33.satoken.json.SaJsonTemplate;\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.session.SaSession;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder;\nimport cn.dev33.satoken.stp.SaLoginConfig;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport cn.dev33.satoken.stp.parameter.SaLoginParameter;\nimport cn.dev33.satoken.util.SaTokenConsts;\nimport cn.dev33.satoken.util.SoMap;\nimport org.junit.jupiter.api.*;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.mock.web.MockFilterChain;\nimport org.springframework.util.PathMatcher;\n\nimport javax.servlet.ServletException;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Sa-Token 基础API测试 \n * \n * <p> 注解详解参考： https://www.cnblogs.com/flypig666/p/11505277.html\n * @author Auster \n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class BasicsTest {\n\n\t// 持久化Bean \n\t@Autowired(required = false)\n\tSaTokenDao dao = SaManager.getSaTokenDao();\n\t\n\t@Autowired\n\tPathMatcher pathMatcher;\n\t\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ 基础测试 start ...\");\n    \tSaManager.getConfig().setActiveTimeout(180);\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ 基础测试 end ... \\n\");\n    }\n\n\t@BeforeEach\n\tpublic void beforeEach() {\n\t\tSaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse());\n\t}\n\n\t// 结束\n\t@AfterEach\n\tpublic void afterEach() {\n\t\tSaTokenContextServletUtil.clearContext();\n\t}\n\n    // 测试：基础API\n    @Test\n    public void testBasicsApi() {\n    \t// 基本API \n    \tAssertions.assertEquals(StpUtil.getLoginType(), \"login\");\n    \tAssertions.assertEquals(StpUtil.getStpLogic(), SaManager.getStpLogic(\"login\"));\n    \tAssertions.assertEquals(StpUtil.getTokenName(), \"satoken\");\n    \t\n    \t// 安全的更新 StpUtil 的 StpLogic 对象 \n    \tStpLogic loginStpLogic = new StpLogic(\"login\");\n    \tStpUtil.setStpLogic(loginStpLogic);\n    \tAssertions.assertEquals(StpUtil.getStpLogic(), loginStpLogic);\n    \tAssertions.assertEquals(SaManager.getStpLogic(\"login\"), loginStpLogic);\n    }\n    \n    // 测试：登录 \n    @Test\n    public void testDoLogin() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// token 存在 \n    \tAssertions.assertNotNull(token);\n    \tAssertions.assertEquals(token, StpUtil.getTokenValueNotCut());\n    \tAssertions.assertEquals(token, StpUtil.getTokenValueByLoginId(10001));\n    \tAssertions.assertEquals(token, StpUtil.getTokenValueByLoginId(10001, SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE));\n    \t\n    \t// token 队列 \n    \tList<String> tokenList = StpUtil.getTokenValueListByLoginId(10001);\n    \tList<String> tokenList2 = StpUtil.getTokenValueListByLoginId(10001, SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);\n    \tAssertions.assertEquals(token, tokenList.get(tokenList.size() - 1));\n    \tAssertions.assertEquals(token, tokenList2.get(tokenList.size() - 1));\n    \t\n    \t// API 验证 \n    \tAssertions.assertTrue(StpUtil.isLogin());\t\n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkLogin());\n    \tAssertions.assertNotNull(token);\t// token不为null\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginIdAsInt(), 10001);\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginIdAsString(), \"10001\");\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginId(), \"10001\");\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginIdDefaultNull(), \"10001\");\t// loginId=10001 \n    \tAssertions.assertEquals(StpUtil.getLoginDevice(), SaTokenConsts.DEFAULT_LOGIN_DEVICE_TYPE);\t// 登录设备类型\n    \t\n    \t// db数据 验证  \n    \t// token存在 \n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token), \"10001\");\n    \t// Session 存在 \n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNotNull(session);\n    \tAssertions.assertEquals(session.getId(), \"satoken:login:session:\" + 10001);\n    \tAssertions.assertTrue(session.getTerminalList().size() >= 1);\n    }\n    \n    // 测试：注销 \n    @Test\n    public void testLogout() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tString token = StpUtil.getTokenValue();\n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token), \"10001\");\n    \t\n    \t// 注销\n    \tStpUtil.logout();\n    \t// token 应该被清除\n    \tAssertions.assertNull(StpUtil.getTokenValue());\n    \tAssertions.assertFalse(StpUtil.isLogin());\n    \tAssertions.assertNull(dao.get(\"satoken:login:token:\" + token));\n    \t\n    \t// 全部客户端注销掉 \n    \tStpUtil.logout(10001);\n    \t// Session 应该被清除 \n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNull(session);\n    \t\n    \t// 在调用 getLoginId() 就会抛出异常 \n    \tAssertions.assertEquals(StpUtil.getLoginId(\"无值\"), \"无值\");\n    \tAssertions.assertThrows(NotLoginException.class, () -> StpUtil.getLoginId());\n    }\n    \n    // 测试：Session会话 \n    @Test\n    public void testSession() {\n    \tStpUtil.login(10001);\n    \t\n    \t// API 应该可以获取 Session \n    \tAssertions.assertNotNull(StpUtil.getSession());\n    \tAssertions.assertNotNull(StpUtil.getSession(false));\n    \t\n    \t// db中应该存在 Session\n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNotNull(session);\n    \t\n    \t// 存取值 \n    \tsession.set(\"name\", \"zhang\");\n    \tsession.set(\"age\", \"18\");\n    \tAssertions.assertEquals(session.get(\"name\"), \"zhang\");\n    \tAssertions.assertEquals(session.getInt(\"age\"), 18);\n    \tAssertions.assertEquals((int)session.getModel(\"age\", int.class), 18);\n    \tAssertions.assertEquals((int)session.get(\"age\", 20), 18);\n    \tAssertions.assertEquals((int)session.get(\"name2\", 20), 20);\n    \tAssertions.assertEquals((int)session.get(\"name2\", () -> 30), 30);\n    \tsession.clear();\n    \tAssertions.assertEquals(session.get(\"name\"), null);\n    }\n    \n    // 测试：权限认证 \n    @Test\n    public void testCheckPermission() {\n    \tStpUtil.login(10001);\n    \t\n    \t// 获取权限 \n    \tList<String> permissionList = StpUtil.getPermissionList();\n    \tList<String> permissionList2 = StpUtil.getPermissionList(10001);\n    \tAssertions.assertEquals(permissionList.size(), permissionList2.size());\n    \t\n    \t// 权限校验  \n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user-add\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user-list\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"user\"));\n    \tAssertions.assertTrue(StpUtil.hasPermission(\"art-add\"));\n    \tAssertions.assertFalse(StpUtil.hasPermission(\"get-user\"));\n    \t// and\n    \tAssertions.assertTrue(StpUtil.hasPermissionAnd(\"art-add\", \"art-get\"));\n    \tAssertions.assertFalse(StpUtil.hasPermissionAnd(\"art-add\", \"comment-add\"));\n    \t// or \n    \tAssertions.assertTrue(StpUtil.hasPermissionOr(\"art-add\", \"comment-add\"));\n    \tAssertions.assertFalse(StpUtil.hasPermissionOr(\"comment-add\", \"comment-delete\"));\n    \t// more\n    \tAssertions.assertTrue(StpUtil.hasPermission(10001, \"user-add\"));\n    \tAssertions.assertFalse(StpUtil.hasPermission(10002, \"user-add\"));\n    \t\n    \t// 抛异常 \n    \tAssertions.assertThrows(NotPermissionException.class, () -> StpUtil.checkPermission(\"goods-add\"));\n    \tAssertions.assertThrows(NotPermissionException.class, () -> StpUtil.checkPermissionAnd(\"goods-add\", \"art-add\"));\n    \t// 不抛异常 \n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkPermission(\"user-add\"));\n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkPermissionAnd(\"art-get\", \"art-add\"));\n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkPermissionOr(\"goods-add\", \"art-add\"));\n    }\n\n    // 测试：角色认证\n    @Test\n    public void testCheckRole() {\n    \tStpUtil.login(10001);\n    \t\n    \t// 获取角色 \n    \tList<String> roleList = StpUtil.getRoleList();\n    \tList<String> roleList2 = StpUtil.getRoleList(10001);\n    \tAssertions.assertEquals(roleList.size(), roleList2.size());\n    \t\n    \t// 角色校验  \n    \tAssertions.assertTrue(StpUtil.hasRole(\"admin\")); \n    \tAssertions.assertFalse(StpUtil.hasRole(\"teacher\")); \n    \t// and\n    \tAssertions.assertTrue(StpUtil.hasRoleAnd(\"admin\", \"super-admin\")); \n    \tAssertions.assertFalse(StpUtil.hasRoleAnd(\"admin\", \"ceo\")); \n    \t// or\n    \tAssertions.assertTrue(StpUtil.hasRoleOr(\"admin\", \"ceo\")); \n    \tAssertions.assertFalse(StpUtil.hasRoleOr(\"ceo\", \"cto\")); \n    \t// more\n    \tAssertions.assertTrue(StpUtil.hasRole(10001, \"admin\"));\n    \tAssertions.assertFalse(StpUtil.hasRole(10002, \"admin2\"));\n    \t\n    \t// 抛异常 \n    \tAssertions.assertThrows(NotRoleException.class, () -> StpUtil.checkRole(\"ceo\"));\n    \tAssertions.assertThrows(NotRoleException.class, () -> StpUtil.checkRoleAnd(\"ceo\", \"admin\"));\n    \t// 不抛异常 \n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkRole(\"admin\"));\n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkRoleAnd(\"admin\", \"super-admin\"));\n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkRoleOr(\"ceo\", \"admin\"));\n    }\n\t\n    // 测试：根据token强制注销 \n    @Test\n    public void testLogoutByToken() {\n\t\tStpUtil.logout(10001);\n    \t\n    \t// 先登录上\n\t\tStpUtil.login(10001);\n    \tAssertions.assertTrue(StpUtil.isLogin());\t\n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// 根据token注销 \n    \tStpUtil.logoutByTokenValue(token);\n    \tAssertions.assertFalse(StpUtil.isLogin()); \n    \t\n    \t// token 应该被清除\n    \tAssertions.assertNull(dao.get(\"satoken:login:token:\" + token));\n    \t// Session 应该被清除 \n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNull(session);\n\n\t\t// 场景值应该是token无效 \n    \ttry {\n    \t\tStpUtil.checkLogin();\n\t\t} catch (NotLoginException e) {\n\t\t\tAssertions.assertEquals(e.getType(), NotLoginException.INVALID_TOKEN);\n\t\t}\n\n    \t// 根据token踢下线 \n    \tStpUtil.login(10001); \n    \tStpUtil.kickoutByTokenValue(StpUtil.getTokenValue());\n    \t\n\t\t// 场景值应该是被踢下线 \n    \ttry {\n    \t\tStpUtil.checkLogin();\n\t\t} catch (NotLoginException e) {\n\t\t\tAssertions.assertEquals(e.getType(), NotLoginException.KICK_OUT);\n\t\t}\n    }\n\n    // 测试：根据账号id强制注销 \n    @Test\n    public void testLogoutByLoginId() {\n\n    \t// 先登录上 \n    \tStpUtil.login(10001); \n    \tAssertions.assertTrue(StpUtil.isLogin());\t\n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// 根据账号id注销 \n    \tStpUtil.logout(10001);\n    \tAssertions.assertFalse(StpUtil.isLogin()); \n    \t\n    \t// token 应该被清除\n    \tAssertions.assertNull(dao.get(\"satoken:login:token:\" + token));\n    \t// Session 应该被清除 \n    \tSaSession session = dao.getSession(\"satoken:login:session:\" + 10001);\n    \tAssertions.assertNull(session);\n\n\t\t// 场景值应该是token无效 \n    \ttry {\n    \t\tStpUtil.checkLogin();\n\t\t} catch (NotLoginException e) {\n\t\t\tAssertions.assertEquals(e.getType(), NotLoginException.INVALID_TOKEN);\n\t\t}\n    }\n\n    // 测试Token-Session \n    @Test\n    public void testTokenSession() {\n\n    \t// 先登录上 \n    \tStpUtil.login(10001); \n    \tString token = StpUtil.getTokenValue();\n    \t\n    \t// 刚开始不存在 \n    \tAssertions.assertNull(StpUtil.stpLogic.getTokenSession(false));\n    \tSaSession session = dao.getSession(\"satoken:login:token-session:\" + token);\n    \tAssertions.assertNull(session);\n    \t\n    \t// 调用一次就存在了 \n    \tStpUtil.getTokenSession();\n    \tAssertions.assertNotNull(StpUtil.stpLogic.getTokenSession(false));\n    \tSaSession session2 = dao.getSession(\"satoken:login:token-session:\" + token);\n    \tAssertions.assertNotNull(session2);\n    \t\n    \t// \n    \tSaSession tokenSession = StpUtil.getTokenSession();\n    \tSaSession tokenSession2 = StpUtil.getTokenSessionByToken(token);\n    \tAssertions.assertEquals(tokenSession.getId(), tokenSession2.getId());\n    }\n    \n    // 测试：根据账号id踢人\n    @Test\n    public void kickoutByLoginId() {\n\n    \t// 踢人下线 \n    \tStpUtil.login(10001); \n    \tString token = StpUtil.getTokenValue();\n    \tStpUtil.kickout(10001);\n    \t\n    \t// token 应该被打标记 \n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token), NotLoginException.KICK_OUT);\n\n\t\t// 场景值应该是token已被踢下线 \n    \ttry {\n    \t\tStpUtil.checkLogin();\n\t\t} catch (NotLoginException e) {\n\t\t\tAssertions.assertEquals(e.getType(), NotLoginException.KICK_OUT);\n\t\t}\n    }\n    \n    // 测试：账号封禁 \n    @Test\n    public void testDisable() {\n    \t// 封号 \n    \tStpUtil.disable(10007, 200);\n    \tAssertions.assertTrue(StpUtil.isDisable(10007));\n    \tAssertions.assertEquals(dao.get(\"satoken:login:disable:login:\" + 10007), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); \n\n    \t// 封号后检测一下 (会抛出 DisableLoginException 异常) \n\t\tAssertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10007));\n    \t\n    \t// 封号时间 \n    \tlong disableTime = StpUtil.getDisableTime(10007);\n    \tAssertions.assertTrue(disableTime <= 200 && disableTime >= 199);\n    \t\n    \t// 解封  \n    \tStpUtil.untieDisable(10007);\n    \tAssertions.assertFalse(StpUtil.isDisable(10007));\n    \tAssertions.assertEquals(dao.get(\"satoken:login:disable:login:\" + 10007), null); \n\t\tAssertions.assertDoesNotThrow(() -> StpUtil.checkDisable(10007));\n    }\n\n    // 测试：分类封禁 \n    @Test\n    public void testDisableService() {\n    \t// 封掉评论功能 \n    \tStpUtil.disable(10008, \"comment\", 200);\n    \tAssertions.assertTrue(StpUtil.isDisable(10008, \"comment\"));\n    \tAssertions.assertEquals(dao.get(\"satoken:login:disable:comment:\" + 10008), String.valueOf(SaTokenConsts.DEFAULT_DISABLE_LEVEL)); \n    \tAssertions.assertNull(dao.get(\"satoken:login:disable:login:\" + 10008)); \n\n    \t// 封号后检测一下 \n\t\tAssertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10008, \"comment\"));\n\t\t// 检查多个，有一个不通过就报异常 \n\t\tAssertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisable(10008, \"comment\", \"login\"));\n\t\t\n    \t// 封号时间 \n    \tlong disableTime = StpUtil.getDisableTime(10008, \"comment\");\n    \tAssertions.assertTrue(disableTime <= 200 && disableTime >= 199);\n    \t\n    \t// 解封 (不加服务名不会成功)\n    \tStpUtil.untieDisable(10008);\n    \tAssertions.assertTrue(StpUtil.isDisable(10008, \"comment\"));\n    \tAssertions.assertNotNull(dao.get(\"satoken:login:disable:comment:\" + 10008)); \n    \t\n    \t// 解封 (加服务名才会成功)\n    \tStpUtil.untieDisable(10008, \"comment\");\n    \tAssertions.assertFalse(StpUtil.isDisable(10008, \"comment\"));\n    \tAssertions.assertEquals(dao.get(\"satoken:login:disable:comment:\" + 10008), null); \n\t\tAssertions.assertDoesNotThrow(() -> StpUtil.checkDisable(10007, \"comment\"));\n    }\n\n    // 测试：阶梯封禁 \n    @Test\n    public void testDisableLevel() {\n    \t// 封禁等级5\n    \tStpUtil.disableLevel(10009, 5, 200);\n    \tAssertions.assertTrue(StpUtil.isDisableLevel(10009, 3));\n    \tAssertions.assertTrue(StpUtil.isDisableLevel(10009, 5));\n    \t// 未达到7级 \n    \tAssertions.assertFalse(StpUtil.isDisableLevel(10009, 7));\n    \t// 账号未封禁 \n    \tAssertions.assertFalse(StpUtil.isDisableLevel(20009, 3));\n    \t\n    \t// dao中应该有值 \n    \tAssertions.assertEquals(dao.get(\"satoken:login:disable:login:\" + 10009), String.valueOf(5)); \n\n    \t// 封号后检测一下 \n\t\tAssertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10009, 3));\n\t\tAssertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10009, 5));\n\t\t// 未达到等级，不抛出异常\n\t\tAssertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(10009, 7));\n\t\t// 账号未被封禁，不抛出异常 \n\t\tAssertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(20009, 3));\n\t\t\n    \t// 封号等级 \n    \tAssertions.assertEquals(StpUtil.getDisableLevel(10009), 5);\n    \tAssertions.assertEquals(StpUtil.getDisableLevel(20009), -2);\n    \t\n    \t// 解封\n    \tStpUtil.untieDisable(10009);\n    \tAssertions.assertFalse(StpUtil.isDisable(10009));\n    \tAssertions.assertFalse(StpUtil.isDisableLevel(10009, 5));\n    \tAssertions.assertNull(dao.get(\"satoken:login:disable:login:\" + 10009)); \n    }\n\n    // 测试：分类封禁 + 阶梯封禁 \n    @Test\n    public void testDisableServiceLevel() {\n    \t// 封禁服务 shop，等级5\n    \tStpUtil.disableLevel(10010, \"shop\", 5, 200);\n    \tAssertions.assertTrue(StpUtil.isDisableLevel(10010, \"shop\", 3));\n    \tAssertions.assertTrue(StpUtil.isDisableLevel(10010, \"shop\", 5));\n    \t// 未达到7级 \n    \tAssertions.assertFalse(StpUtil.isDisableLevel(10010, \"shop\", 7));\n    \t// 账号未封禁 \n    \tAssertions.assertFalse(StpUtil.isDisableLevel(20010, \"shop\", 3));\n    \t// 服务名不对 \n    \tAssertions.assertFalse(StpUtil.isDisableLevel(10010, \"shop2\", 5));\n    \t\n    \t// dao中应该有值 \n    \tAssertions.assertEquals(dao.get(\"satoken:login:disable:shop:\" + 10010), String.valueOf(5)); \n\n    \t// 封号后检测一下 \n\t\tAssertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10010, \"shop\", 3));\n\t\tAssertions.assertThrows(DisableServiceException.class, () -> StpUtil.checkDisableLevel(10010, \"shop\", 5));\n\t\t// 未达到等级，不抛出异常\n\t\tAssertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(10010, \"shop\", 7));\n\t\t// 账号未被封禁，不抛出异常 \n\t\tAssertions.assertDoesNotThrow(() -> StpUtil.checkDisableLevel(20010, \"shop\", 3));\n\t\t\n    \t// 封号等级 \n    \tAssertions.assertEquals(StpUtil.getDisableLevel(10010, \"shop\"), 5);\n    \tAssertions.assertEquals(StpUtil.getDisableLevel(10010, \"shop2\"), -2);\n    \tAssertions.assertEquals(StpUtil.getDisableLevel(20010, \"shop\"), -2);\n    \t\n    \t// 解封\n    \tStpUtil.untieDisable(10010, \"shop\");\n    \tAssertions.assertFalse(StpUtil.isDisable(10010, \"shop\"));\n    \tAssertions.assertFalse(StpUtil.isDisableLevel(10010, \"shop\", 5));\n    \tAssertions.assertNull(dao.get(\"satoken:login:disable:shop:\" + 10010)); \n    }\n\n    // 测试：身份切换 \n    @Test\n    public void testSwitch() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tAssertions.assertFalse(StpUtil.isSwitch());\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\n    \t\n    \t// 开始身份切换 \n    \tStpUtil.switchTo(10044);\n    \tAssertions.assertTrue(StpUtil.isSwitch());\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10044);\n\n    \t// 开始身份切换 Lambda 方式\n    \tStpUtil.switchTo(10045, () -> {\n    \t\tAssertions.assertTrue(StpUtil.isSwitch());\n        \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10045);\n    \t});\n    \t\n    \t// 结束切换 \n    \tStpUtil.endSwitch(); \n    \tAssertions.assertFalse(StpUtil.isSwitch());\n    \tAssertions.assertEquals(StpUtil.getLoginIdAsLong(), 10001);\n    }\n    \n    // 测试：会话管理\n    @Test\n    public void testSearchTokenValue() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tStpUtil.login(10002);\n    \tStpUtil.login(10003);\n    \tStpUtil.login(10004);\n    \tStpUtil.login(10005);\n\n    \t// 查询 Token 列表 \n    \tList<String> list = StpUtil.searchTokenValue(\"\", 0, 10, true);\n    \tAssertions.assertTrue(list.size() >= 5);\n    \t\n    \t// 查询 Session 列表 \n    \tList<String> list2 = StpUtil.searchSessionId(\"\", 0, 10, true);\n    \tAssertions.assertTrue(list2.size() >= 5);\n    \tlist2.stream().forEach(sessionId -> {\n    \t\tAssertions.assertNotNull(StpUtil.getSessionBySessionId(sessionId));\n    \t});\n    }\n\n    // 测试：会话管理(Token-Session)\n    @Test\n    public void testSearchTokenSession() {\n    \t// 登录\n    \tStpUtil.login(10001);\n    \tStpUtil.getTokenSession();\n    \tStpUtil.login(10002);\n    \tStpUtil.getTokenSession();\n    \tStpUtil.login(10003);\n    \tStpUtil.getTokenSession();\n    \tStpUtil.login(10004);\n    \tStpUtil.getTokenSession();\n    \tStpUtil.login(10005);\n    \tStpUtil.getTokenSession();\n\n    \t// 查询 Token-Session 列表 \n    \tList<String> list2 = StpUtil.searchTokenSessionId(\"\", 0, 10, true);\n    \tAssertions.assertTrue(list2.size() >= 5);\n    \tlist2.stream().forEach(sessionId -> {\n    \t\tAssertions.assertNotNull(StpUtil.getSessionBySessionId(sessionId));\n    \t});\n    }\n    \n    // 测试：二级认证\n    @Test\n    public void testSafe() {\n    \t// 登录 \n    \tStpUtil.login(10001);\n    \tAssertions.assertFalse(StpUtil.isSafe());\n    \t\n    \t// 开启二级认证 \n    \tStpUtil.openSafe(2);\n    \tAssertions.assertTrue(StpUtil.isSafe()); \n    \tAssertions.assertTrue(StpUtil.getSafeTime() > 0); \n    \tStpUtil.checkSafe();\n    \t\n    \t// 自然结束 \n//    \tThread.sleep(2500);\n//    \tAssertions.assertFalse(StpUtil.isSafe());\n    \t\n    \t// 手动结束\n//    \tStpUtil.openSafe(2);\n    \tStpUtil.closeSafe();\n    \tAssertions.assertFalse(StpUtil.isSafe());\n    \t\n    \t// 抛异常 \n    \tAssertions.assertThrows(NotSafeException.class, () -> StpUtil.checkSafe());\n    }\n\n    \n    // ------------- 复杂点的 \n\n    // 测试：指定设备登录 \n    @Test\n    public void testDoLoginByDevice() {\n    \tStpUtil.login(10001, \"PC\");\n    \tAssertions.assertEquals(StpUtil.getLoginDevice(), \"PC\");\n\n    \t// 指定一个其它的设备注销，应该注销不掉 \n    \tStpUtil.logout(10001, \"APP\");\n    \tAssertions.assertTrue(StpUtil.isLogin());\n    \t\n    \t// 指定当前设备踢掉，则能够踢掉 \n    \tStpUtil.kickout(10001, \"PC\");\n    \tAssertions.assertFalse(StpUtil.isLogin());\n    \t\n    \t// 顶掉\n    \tStpUtil.login(10001, \"PC\");\n    \tStpUtil.replaced(10001, \"PC\");\n    \tAssertions.assertFalse(StpUtil.isLogin());\n    \ttry {\n\t\t\tStpUtil.checkLogin();\n\t\t} catch (NotLoginException e) {\n\t\t\t// 场景值应该为-4  \n\t\t\tAssertions.assertEquals(e.getType(), NotLoginException.BE_REPLACED);\n\t\t}\n    }\n\n    // 测试：指定 timeout 登录 \n    @Test\n    public void testDoLoginByTimeout() {\n    \t\n    \t// 指定timeout 登录 \n    \tStpUtil.login(10001, 100);\n    \tlong timeout = StpUtil.getTokenTimeout();\n    \tAssertions.assertTrue(timeout <= 100 && timeout >= 99);\n    \t\n    \t// 续期一下\n    \tStpUtil.renewTimeout(200);\n    \ttimeout = StpUtil.getTokenTimeout();\n    \tAssertions.assertTrue(timeout <= 200 && timeout >= 199);\n    \t\n    \t// 续期一下\n    \tStpUtil.renewTimeout(StpUtil.getTokenValue(), 300);\n    \ttimeout = StpUtil.getTokenTimeout();\n    \tAssertions.assertTrue(timeout <= 300 && timeout >= 299);\n    \t\n    \t// Session 也会续期\n    \ttimeout = StpUtil.getSessionTimeout();\n    \tAssertions.assertTrue(timeout >= 299);\n    \t\n    \tStpUtil.getTokenSession();\n    \ttimeout = StpUtil.getTokenSessionTimeout();\n    \tAssertions.assertTrue(timeout >= 299);\n    \t\n    \t\n    \t// 注销后，就是-2\n    \tStpUtil.logout();\n    \ttimeout = StpUtil.getTokenTimeout();\n    \tAssertions.assertTrue(timeout == SaTokenDao.NOT_VALUE_EXPIRE);\n    }\n\n    // 测试：预定 Token 登录 \n    @Test\n    public void testDoLoginBySetToken() {\n    \t// 预定 Token 登录 \n    \tStpUtil.login(10001, new SaLoginParameter().setToken(\"qwer-qwer-qwer-qwer\"));\n    \tAssertions.assertEquals(StpUtil.getTokenValue(), \"qwer-qwer-qwer-qwer\");\n\n    \t// 注销后，应该清除Token \n    \tStpUtil.logout();\n    \tAssertions.assertNull(StpUtil.getTokenValue());\n    }\n\n    // 测试：无上下文注入的登录 \n    @Test\n    public void testCreateLoginSession() {\n    \t\n    \t// 无上下文注入的登录\n    \tStpUtil.createLoginSession(10001);\n    \tAssertions.assertNull(StpUtil.getTokenValue());\n\n    \t// 无上下文注入的登录\n    \tString token = StpUtil.createLoginSession(10001, new SaLoginParameter());\n    \tAssertions.assertNull(StpUtil.getTokenValue());\n    \t\n    \t// 手动写入\n    \tStpUtil.setTokenValue(token);\n    \tAssertions.assertNotNull(StpUtil.getTokenValue());\n    \t\n    \t// 手动写入到Cookie \n    \tStpUtil.setTokenValue(token, 10);\n    \tAssertions.assertNotNull(StpUtil.getTokenValue());\n    }\n\n    // 测试，匿名 Token-Session \n    @Test\n    public void testAnonTokenSession() {\n    \t// token 不存在 \n    \tStpUtil.logout();\n    \tAssertions.assertNull(StpUtil.getTokenValue());\n    \t\n    \t// token 存在 \n    \tSaSession anonTokenSession = StpUtil.getAnonTokenSession();\n    \tString token = StpUtil.getTokenValue();\n    \tAssertions.assertNotNull(token);\n    \t// 写个值 \n    \tanonTokenSession.set(\"code\", \"123456\");\n    \t\n    \t// 登录时，预定上 \n    \tStpUtil.login(10001, SaLoginConfig.setToken(token));\n    \t// token不变 \n    \tAssertions.assertEquals(token, StpUtil.getTokenValue());\n    \t\n    \t// Token-Session 存在，且不变 \n    \tSaSession tokenSession = StpUtil.getTokenSession();\n    \tAssertions.assertEquals(anonTokenSession.getId(), tokenSession.getId());\n    \t\n    \t// 刚才写的值，仍然在\n    \tAssertions.assertEquals(tokenSession.get(\"code\"), \"123456\");\n    }\n\n    // 测试，token 最低活跃频率  \n    @Test\n    public void testActiveTimeout() {\n    \t// 登录 \n    \tStpUtil.login(10001);\n    \tAssertions.assertNotNull(StpUtil.getTokenValue());\n    \t\n    \t// 默认跟随全局 timeout \n    \tStpUtil.updateLastActiveToNow();\n    \tlong activeTimeout = StpUtil.getTokenActiveTimeout();\n    \tAssertions.assertTrue(activeTimeout <=180 || activeTimeout >=179);\n    \t\n    \t// 不会抛出异常 \n    \tAssertions.assertDoesNotThrow(() -> StpUtil.checkActiveTimeout());\n    }\n\n    // 测试，上下文 API \n    @Test\n    public void testSaTokenContext() {\n    \tSaTokenContext context = SaHolder.getContext();\n    \t// path 匹配 \n    \t// Assertions.assertTrue(context.matchPath(\"/user/**\", \"/user/add\"));\n    \t// context 是否有效 \n    \tAssertions.assertTrue(context.isValid());\n    \t// 是否为web环境 \n    \tAssertions.assertTrue(SpringMVCUtil.isWeb());\n    \t// pathMatcher\n    \t// Assertions.assertEquals(pathMatcher, SaPathMatcherHolder.getPathMatcher());\n    \t// 自创建 \n    \tSaPathMatcherHolder.pathMatcher = null;\n    \tAssertions.assertNotNull(SaPathMatcherHolder.getPathMatcher());\n    \tSaPathMatcherHolder.pathMatcher = pathMatcher;\n    }\n\n    // 测试json转换 \n    @Test\n    public void testSaJsonTemplate() {\n    \tSaJsonTemplate saJsonTemplate = SaManager.getSaJsonTemplate();\n    \t\n    \t// map 转 json \n    \tSoMap map = SoMap.getSoMap(\"name\", \"zhangsan\");\n    \tString jsonString = saJsonTemplate.objectToJson(map);\n    \tAssertions.assertEquals(jsonString, \"{\\\"name\\\":\\\"zhangsan\\\"}\");\n    \t\n    \t// 抛异常 \n    \t// Assertions.assertThrows(SaJsonConvertException.class, () -> saJsonTemplate.objectToJson(new Object()));\n    \t\n    \t// json 转 map \n    \tMap<String, Object> map2 = saJsonTemplate.jsonToMap(\"{\\\"name\\\":\\\"zhangsan\\\"}\");\n    \tAssertions.assertEquals(map2.get(\"name\"), \"zhangsan\");\n    \t\n    \t// 抛异常 \n    \tAssertions.assertThrows(SaJsonConvertException.class, () -> saJsonTemplate.jsonToMap(\"x\"));\n    }\n\n    // 测试过滤器、拦截器 基础API \n    @Test\n    public void testFilter() throws IOException, ServletException {\n    \t// 过滤器 \n    \tSaServletFilter filter = new SaServletFilter()\n    \t\t\t.addInclude(\"/**\")\n    \t\t\t.setIncludeList(Arrays.asList(\"/**\"))\n    \t\t\t.addExclude(\"/favicon.ico\")\n    \t\t\t.setExcludeList(Arrays.asList(\"/favicon.ico\"))\n    \t\t\t.setAuth(obj -> {})\n    \t\t\t.setBeforeAuth(obj -> {})\n    \t\t\t;\n    \tAssertions.assertEquals(filter.includeList.get(0), \"/**\");\n    \tAssertions.assertEquals(filter.excludeList.get(0), \"/favicon.ico\");\n    \t// 以下功能无法测试\n    \tfilter.init(null);\n    \tfilter.doFilter(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse(), new MockFilterChain());\n    \tfilter.destroy();\n    \tAssertions.assertThrows(SaTokenException.class, () -> filter.error.run(new SaTokenException(\"xxx\")));\n    \t\n    \tfilter.setError(e -> e.getMessage());\n    \tAssertions.assertEquals(filter.error.run(new SaTokenException(\"msg\")), \"msg\");\n    }\n\n    \n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/ManyLoginTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.springboot;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.config.SaTokenConfig;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.servlet.util.SaTokenContextServletUtil;\nimport cn.dev33.satoken.session.SaTerminalInfo;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\nimport cn.dev33.satoken.stp.StpLogic;\nimport cn.dev33.satoken.stp.StpUtil;\nimport org.junit.jupiter.api.*;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\nimport java.util.List;\n\n/**\n * Sa-Token 多端登录测试 \n * \n * @author click33 \n *\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class ManyLoginTest {\n\n\t// 持久化Bean \n\t@Autowired(required = false)\n\tSaTokenDao dao = SaManager.getSaTokenDao();\n\t\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n------------ 多端登录测试 star ...\");\n    }\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n---------- 多端登录测试 end ... \\n\");\n    }\n\n\t@BeforeEach\n\tpublic void beforeEach() {\n\t\tSaTokenContextServletUtil.setContext(SpringMVCUtil.getRequest(), SpringMVCUtil.getResponse());\n\t}\n\n\t@AfterEach\n\tpublic void afterEach() {\n\t\tSaTokenContextServletUtil.clearContext();\n\t}\n\n    // 测试：并发登录、共享token、同端 \n    @Test\n    public void login() {\n    \tSaManager.setConfig(new SaTokenConfig().setIsShare(true));\n    \t\n    \tStpUtil.login(10001);\n    \tString token1 = StpUtil.getTokenValue();\n\n    \tStpUtil.login(10001);\n    \tString token2 = StpUtil.getTokenValue();\n    \t\n    \tAssertions.assertEquals(token1, token2);\n    }\n\n    // 测试：并发登录、共享token、不同端 \n    @Test\n    public void login2() {\n    \tSaManager.setConfig(new SaTokenConfig());\n    \t\n    \tStpUtil.login(10001, \"APP\");\n    \tString token1 = StpUtil.getTokenValue();\n\n    \tStpUtil.login(10001, \"PC\");\n    \tString token2 = StpUtil.getTokenValue();\n    \t\n    \tAssertions.assertNotEquals(token1, token2);\n    }\n\n    // 测试：并发登录、不共享token\n    @Test\n    public void login3() {\n    \tSaManager.setConfig(new SaTokenConfig().setIsShare(false));\n    \t\n    \tStpUtil.login(10001);\n    \tString token1 = StpUtil.getTokenValue();\n\n    \tStpUtil.login(10001);\n    \tString token2 = StpUtil.getTokenValue();\n    \t\n    \tAssertions.assertNotEquals(token1, token2);\n    }\n\n    // 测试：禁并发登录，后者顶出前者 \n    @Test\n    public void login4() {\n    \tSaManager.setConfig(new SaTokenConfig().setIsConcurrent(false));\n    \t\n    \tStpUtil.login(10001);\n    \tString token1 = StpUtil.getTokenValue();\n\n    \tStpUtil.login(10001);\n    \tString token2 = StpUtil.getTokenValue();\n    \t\n    \t// token不同 \n    \tAssertions.assertNotEquals(token1, token2);\n    \t\n    \t// token1会被标记为：已被顶下线 \n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token1), \"-4\");\n    \t\n    \t// Account-Session里的 token1 签名会被移除 \n    \tList<SaTerminalInfo> terminalList = StpUtil.getSessionByLoginId(10001).getTerminalList();\n    \tfor (SaTerminalInfo terminal : terminalList) {\n    \t\tAssertions.assertNotEquals(terminal.getTokenValue(), token1);\n\t\t}\n    }\n    \n    // 测试：多端登录，一起强制注销 \n    @Test\n    public void login5() {\n    \tSaManager.setConfig(new SaTokenConfig());\n    \t\n    \tStpUtil.login(10001, \"APP\");\n    \tString token1 = StpUtil.getTokenValue();\n    \t\n    \tStpUtil.login(10001, \"PC\");\n    \tString token2 = StpUtil.getTokenValue();\n    \t\n    \tStpUtil.login(10001, \"h5\");\n    \tString token3 = StpUtil.getTokenValue();\n    \t\n    \t// 注销 \n    \tStpUtil.logout(10001);\n\n    \t// 三个Token应该全部无效 \n    \tAssertions.assertNull(dao.get(\"satoken:login:token:\" + token1));\n    \tAssertions.assertNull(dao.get(\"satoken:login:token:\" + token2));\n    \tAssertions.assertNull(dao.get(\"satoken:login:token:\" + token3));\n    \t\n    \t// Account-Session也应该被清除掉 \n    \tAssertions.assertNull(StpUtil.getSessionByLoginId(10001, false));\n    \tAssertions.assertNull(dao.getSession(\"satoken:login:session:\" + 10001));\n    }\n\n    // 测试：多端登录，一起强制踢下线 \n    @Test\n    public void login6() {\n    \tSaManager.setConfig(new SaTokenConfig());\n    \t\n    \tStpUtil.login(10001, \"APP\");\n    \tString token1 = StpUtil.getTokenValue();\n    \t\n    \tStpUtil.login(10001, \"PC\");\n    \tString token2 = StpUtil.getTokenValue();\n    \t\n    \tStpUtil.login(10001, \"h5\");\n    \tString token3 = StpUtil.getTokenValue();\n    \t\n    \t// 注销 \n    \tStpUtil.kickout(10001);\n\n    \t// 三个Token应该全部无效 \n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token1), \"-5\");\n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token2), \"-5\");\n    \tAssertions.assertEquals(dao.get(\"satoken:login:token:\" + token3), \"-5\");\n    \t\n    \t// Account-Session也应该被清除掉 \n    \tAssertions.assertNull(StpUtil.getSessionByLoginId(10001, false));\n    \tAssertions.assertNull(dao.getSession(\"satoken:login:session:\" + 10001));\n    }\n\n    // 测试：多账号模式，在一个账号体系里登录成功，在另一个账号体系不会校验通过 \n    @Test\n    public void login7() {\n    \tSaManager.setConfig(new SaTokenConfig());\n    \t\n    \tStpUtil.login(10001);\n    \tString token1 = StpUtil.getTokenValue();\n    \t\n    \tStpLogic stp = new StpLogic(\"user\");\n    \t\n    \tAssertions.assertNotNull(StpUtil.getLoginIdByToken(token1));\n    \tAssertions.assertNull(stp.getLoginIdByToken(token1));\n    }\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SaPathMatcherTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.springboot;\n\nimport cn.dev33.satoken.spring.pathmatch.SaPathMatcherHolder;\nimport cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil;\nimport cn.dev33.satoken.spring.pathmatch.SaPatternsRequestConditionHolder;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.SpringBootVersion;\n\n/**\n * SaPathMatcher 路由匹配测试\n * \n * @author click33  \n *\n */\npublic class SaPathMatcherTest {\n\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \t\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \t\n    }\n\n    // 测试，SaPathMatcherHolder\n    @Test\n    public void testSaPathMatcherHolder() {\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/user/get\", \"/user/get\"));\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/user/*\", \"/user/get\"));\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/user/**\", \"/user/get/list\"));\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/user/**/page\", \"/user/get/list/page\"));\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/user/get/{id}\", \"/user/get/123\"));\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/user/get/{id}/page\", \"/user/get/123/page\"));\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/*.js\", \"/sa.js\"));\n        Assertions.assertTrue(SaPathMatcherHolder.getPathMatcher().match(\"/user/**/*.js\", \"/user/sa.js\"));\n\n        // SaPathMatcherHolder 无法匹配斜杠后缀\n        Assertions.assertFalse(SaPathMatcherHolder.getPathMatcher().match(\"/user/get\", \"/user/get/\"));\n    }\n\n    // 测试，SaPatternsRequestConditionHolder\n    @Test\n    public void testSaPatternsRequestConditionHolder() {\n\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/get\", \"/user/get\"));\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/*\", \"/user/get\"));\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/**\", \"/user/get/list\"));\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/**/page\", \"/user/get/list/page\"));\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/get/{id}\", \"/user/get/123\"));\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/get/{id}/page\", \"/user/get/123/page\"));\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/*.js\", \"/sa.js\"));\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/**/*.js\", \"/user/sa.js\"));\n\n        // SaPatternsRequestConditionHolder 可匹配斜杠后缀\n        Assertions.assertTrue(SaPatternsRequestConditionHolder.match(\"/user/get\", \"/user/get/\"));\n    }\n\n    // 测试，testSaPathPatternParserUtil\n    @Test\n    public void testSaPathPatternParserUtil() {\n\n        Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/get\", \"/user/get\"));\n        Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/*\", \"/user/get\"));\n        Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/**\", \"/user/get/list\"));\n        // PathPatternParser 不允许 ** 后面还有内容\n        // Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/**/page\", \"/user/get/list/page\"));\n        Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/get/{id}\", \"/user/get/123\"));\n        Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/get/{id}/page\", \"/user/get/123/page\"));\n        Assertions.assertTrue(SaPathPatternParserUtil.match(\"/*.js\", \"/sa.js\"));\n        // Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/**/*.js\", \"/user/sa.js\"));\n\n        // SaPathPatternParserUtil\n        //      在 springboot2.x 版本下 可匹配斜杠后缀\n        //      在 springboot3.x 版本下 不可匹配斜杠后缀\n        if(SpringBootVersion.getVersion().startsWith(\"2.\")) {\n            Assertions.assertTrue(SaPathPatternParserUtil.match(\"/user/get\", \"/user/get/\"));\n        }\n        if(SpringBootVersion.getVersion().startsWith(\"3.\")) {\n            Assertions.assertFalse(SaPathPatternParserUtil.match(\"/user/get\", \"/user/get/\"));\n        }\n    }\n\n\n\n\n\n\n\n\n\n\n\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/SpringMVCUtilTest.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.springboot;\n\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport cn.dev33.satoken.exception.SaTokenException;\nimport cn.dev33.satoken.spring.SpringMVCUtil;\n\n/**\n * SpringMVCUtil 测试 \n * \n * @author click33  \n *\n */\npublic class SpringMVCUtilTest {\n\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \t\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \t\n    }\n\n    // 测试，上下文 API \n    @Test\n    public void testSaTokenContext() {\n    \tAssertions.assertThrows(SaTokenException.class, () -> SpringMVCUtil.getRequest());\n    \tAssertions.assertThrows(SaTokenException.class, () -> SpringMVCUtil.getResponse());\n    \tAssertions.assertFalse(SpringMVCUtil.isWeb());\n    }\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/StartUpApplication.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.springboot;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * 启动类 \n * @author Auster\n *\n */\n@SpringBootApplication\npublic class StartUpApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(StartUpApplication.class, args);\n\t}\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/springboot/satoken/StpInterfaceImpl.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.springboot.satoken;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.springframework.stereotype.Component;\n\nimport cn.dev33.satoken.stp.StpInterface;\nimport cn.dev33.satoken.util.SaFoxUtil;\n\n/**\n * 自定义权限验证接口扩展 \n * \n * @author Auster\n *\n */\n@Component\npublic class StpInterfaceImpl implements StpInterface {\n\n\t/**\n\t * 返回一个账号所拥有的权限码集合 \n\t */\n\t@Override\n\tpublic List<String> getPermissionList(Object loginId, String loginType) {\n\t\tint id = SaFoxUtil.getValueByType(loginId, int.class);\n\t\tif(id == 10001) {\n\t\t\treturn Arrays.asList(\"user*\", \"art-add\", \"art-delete\", \"art-update\", \"art-get\");\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * 返回一个账号所拥有的角色标识集合 \n\t */\n\t@Override\n\tpublic List<String> getRoleList(Object loginId, String loginType) {\n\t\tint id = SaFoxUtil.getValueByType(loginId, int.class);\n\t\tif(id == 10001) {\n\t\t\treturn Arrays.asList(\"admin\", \"super-admin\");\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/util/SoMap.java",
    "content": "/*\n * Copyright 2020-2099 sa-token.cc\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage cn.dev33.satoken.util;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Date;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport javax.servlet.http.HttpServletRequest;\n\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\n/**\n * Map< String, Object> 是最常用的一种Map类型，但是它写着麻烦 \n * <p>所以特封装此类，继承Map，进行一些扩展，可以让Map更灵活使用 \n * <p>最新：2020-12-10 新增部分构造方法\n * @author click33\n */\npublic class SoMap extends LinkedHashMap<String, Object> {\n\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic SoMap() {\n\t}\n\t\n\n\t/** 以下元素会在isNull函数中被判定为Null， */\n\tpublic static final Object[] NULL_ELEMENT_ARRAY = {null, \"\"};\n\tpublic static final List<Object> NULL_ELEMENT_LIST;\n\n\t\n\tstatic {\n\t\tNULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);\n\t}\n\n\t// ============================= 读值 =============================\n\n\t/** 获取一个值 */\n\t@Override\n\tpublic Object get(Object key) {\n\t\tif(\"this\".equals(key)) {\n\t\t\treturn this;\n\t\t}\n\t\treturn super.get(key);\n\t}\n\n\t/** 如果为空，则返回默认值 */\n\tpublic Object get(Object key, Object defaultValue) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn value;\n\t}\n\t\n\t/** 转为String并返回 */\n\tpublic String getString(String key) {\n\t\tObject value = get(key);\n\t\tif(value == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn String.valueOf(value);\n\t}\n\n\t/** 如果为空，则返回默认值 */\n\tpublic String getString(String key, String defaultValue) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn String.valueOf(value);\n\t}\n\n\t/** 转为int并返回 */\n\tpublic int getInt(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Integer.valueOf(String.valueOf(value));\n\t}\n\t/** 转为int并返回，同时指定默认值 */\n\tpublic int getInt(String key, int defaultValue) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\treturn Integer.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为long并返回 */\n\tpublic long getLong(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn 0;\n\t\t}\n\t\treturn Long.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为double并返回 */\n\tpublic double getDouble(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn 0.0;\n\t\t}\n\t\treturn Double.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为boolean并返回 */\n\tpublic boolean getBoolean(String key) {\n\t\tObject value = get(key);\n\t\tif(valueIsNull(value)) {\n\t\t\treturn false;\n\t\t}\n\t\treturn Boolean.valueOf(String.valueOf(value));\n\t}\n\n\t/** 转为Date并返回，根据自定义格式 */\n\tpublic Date getDateByFormat(String key, String format) {\n\t\ttry {\n\t\t\treturn new SimpleDateFormat(format).parse(getString(key));\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\t/** 转为Date并返回，根据格式： yyyy-MM-dd */\n\tpublic Date getDate(String key) {\n\t\treturn getDateByFormat(key, \"yyyy-MM-dd\");\n\t}\n\n\t/** 转为Date并返回，根据格式： yyyy-MM-dd HH:mm:ss */\n\tpublic Date getDateTime(String key) {\n\t\treturn getDateByFormat(key, \"yyyy-MM-dd HH:mm:ss\");\n\t}\n\n\t/** 获取集合(必须原先就是个集合，否则会创建个新集合并返回) */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic List<Object> getList(String key) {\n\t\tObject value = get(key);\n\t\tList<Object> list = null;\n\t\tif(value == null || value.equals(\"\")) {\n\t\t\tlist = new ArrayList<Object>();\n\t\t}\n\t\telse if(value instanceof List) {\n\t\t\tlist = (List<Object>)value;\n\t\t} else {\n\t\t\tlist = new ArrayList<Object>();\n\t\t\tlist.add(value);\n\t\t}\n\t\treturn list;\n\t}\n\n\t/** 获取集合 (指定泛型类型) */\n\tpublic <T> List<T> getList(String key, Class<T> cs) {\n\t\tList<Object> list = getList(key);\n\t\tList<T> list2 = new ArrayList<T>();\n\t\tfor (Object obj : list) {\n\t\t\tT objC = getValueByClass(obj, cs);\n\t\t\tlist2.add(objC);\n\t\t}\n\t\treturn list2;\n\t}\n\n\t/** 获取集合(逗号分隔式)，(指定类型) */\n\tpublic <T> List<T> getListByComma(String key, Class<T> cs) {\n\t\tString listStr = getString(key);\n\t\tif(listStr == null || listStr.equals(\"\")) {\n\t\t\treturn new ArrayList<>();\n\t\t}\n\t\t// 开始转化\n\t\tString [] arr = listStr.split(\",\");\n\t\tList<T> list = new ArrayList<T>();\n\t\tfor (String str : arr) {\n\t\t\tif(cs == int.class || cs == Integer.class || cs == long.class || cs == Long.class) {\n\t\t\t\tstr = str.trim();\n\t\t\t}\n\t\t\tT objC = getValueByClass(str, cs);\n\t\t\tlist.add(objC);\n\t\t}\n\t\treturn list;\n\t}\n\n\n\t/** 根据指定类型从map中取值，返回实体对象 */\n\tpublic <T> T getModel(Class<T> cs) {\n\t\ttry {\n\t\t\treturn getModelByObject(cs.newInstance());\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\t\n\t/** 从map中取值，塞到一个对象中 */\n\tpublic <T> T getModelByObject(T obj) {\n\t\t// 获取类型 \n\t\tClass<?> cs = obj.getClass();\n\t\t// 循环复制  \n\t\tfor (Field field : cs.getDeclaredFields()) {\n\t\t\ttry {\n\t\t\t\t// 获取对象 \n\t\t\t\tObject value = this.get(field.getName());\t\n\t\t\t\tif(value == null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfield.setAccessible(true);\t\n\t\t\t\tObject valueConvert = getValueByClass(value, field.getType());\n\t\t\t\tfield.set(obj, valueConvert);\n\t\t\t} catch (IllegalArgumentException | IllegalAccessException e) {\n\t\t\t\tthrow new RuntimeException(\"属性取值出错：\" + field.getName(), e);\n\t\t\t}\n\t\t}\n\t\treturn obj;\n\t}\n\n\t\n\n\t/**\n\t * 将指定值转化为指定类型并返回\n\t * @param obj\n\t * @param cs\n\t * @param <T>\n\t * @return\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getValueByClass(Object obj, Class<T> cs) {\n\t\tString obj2 = String.valueOf(obj);\n\t\tObject obj3 = null;\n\t\tif (cs.equals(String.class)) {\n\t\t\tobj3 = obj2;\n\t\t} else if (cs.equals(int.class) || cs.equals(Integer.class)) {\n\t\t\tobj3 = Integer.valueOf(obj2);\n\t\t} else if (cs.equals(long.class) || cs.equals(Long.class)) {\n\t\t\tobj3 = Long.valueOf(obj2);\n\t\t} else if (cs.equals(short.class) || cs.equals(Short.class)) {\n\t\t\tobj3 = Short.valueOf(obj2);\n\t\t} else if (cs.equals(byte.class) || cs.equals(Byte.class)) {\n\t\t\tobj3 = Byte.valueOf(obj2);\n\t\t} else if (cs.equals(float.class) || cs.equals(Float.class)) {\n\t\t\tobj3 = Float.valueOf(obj2);\n\t\t} else if (cs.equals(double.class) || cs.equals(Double.class)) {\n\t\t\tobj3 = Double.valueOf(obj2);\n\t\t} else if (cs.equals(boolean.class) || cs.equals(Boolean.class)) {\n\t\t\tobj3 = Boolean.valueOf(obj2);\n\t\t} else {\n\t\t\tobj3 = (T)obj;\n\t\t}\n\t\treturn (T)obj3;\n\t}\n\n\t\n\t// ============================= 写值 =============================\n\n\t/**\n\t * 给指定key添加一个默认值（只有在这个key原来无值的情况先才会set进去）\n\t */\n\tpublic void setDefaultValue(String key, Object defaultValue) {\n\t\tif(isNull(key)) {\n\t\t\tset(key, defaultValue);\n\t\t}\n\t}\n\n\t/** set一个值，连缀风格 */\n\tpublic SoMap set(String key, Object value) {\n\t\t// 防止敏感key \n\t\tif(key.toLowerCase().equals(\"this\")) {\t\t\n\t\t\treturn this;\n\t\t}\n\t\tput(key, value);\n\t\treturn this;\n\t}\n\n\t/** 将一个Map塞进SoMap */\n\tpublic SoMap setMap(Map<String, ?> map) {\n\t\tif(map != null) {\n\t\t\tfor (String key : map.keySet()) {\n\t\t\t\tthis.set(key, map.get(key));\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/** 将一个对象解析塞进SoMap */\n\tpublic SoMap setModel(Object model) {\n\t\tif(model == null) {\n\t\t\treturn this;\n\t\t}\n\t\tField[] fields = model.getClass().getDeclaredFields();\n\t    for (Field field : fields) {\n\t        try{\n\t            field.setAccessible(true);\n\t            boolean isStatic = Modifier.isStatic(field.getModifiers());\n\t            if(!isStatic) {\n\t\t            this.set(field.getName(), field.get(model));\n\t            }\n\t        }catch (Exception e){\n\t        \tthrow new RuntimeException(e);\n\t        }\n\t    }\n\t\treturn this;\n\t}\n\n\t/** 将json字符串解析后塞进SoMap */\n\tpublic SoMap setJsonString(String jsonString) {\n\t\ttry {\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tMap<String, Object> map = new ObjectMapper().readValue(jsonString, Map.class);\n\t\t\treturn this.setMap(map);\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\t\n\t// ============================= 删值 =============================\n\n\t/** delete一个值，连缀风格 */\n\tpublic SoMap delete(String key) {\n\t\tremove(key);\n\t\treturn this;\n\t}\n\n\t/** 清理所有value为null的字段 */\n\tpublic SoMap clearNull() {\n\t\tIterator<String> iterator = this.keySet().iterator();\n\t\twhile(iterator.hasNext()) {\n\t\t\tString key = iterator.next();\n\t\t\tif(this.isNull(key)) {\n\t\t\t\titerator.remove();\n\t\t\t\tthis.remove(key);\n\t\t\t}\n\n\t\t}\n\t\treturn this;\n\t}\n\t/** 清理指定key */\n\tpublic SoMap clearIn(String ...keys) {\n\t\tList<String> keys2 = Arrays.asList(keys);\n\t\tIterator<String> iterator = this.keySet().iterator();\n\t\twhile(iterator.hasNext()) {\n\t\t\tString key = iterator.next();\n\t\t\tif(keys2.contains(key) == true) {\n\t\t\t\titerator.remove();\n\t\t\t\tthis.remove(key);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\t/** 清理掉不在列表中的key */\n\tpublic SoMap clearNotIn(String ...keys) {\n\t\tList<String> keys2 = Arrays.asList(keys);\n\t\tIterator<String> iterator = this.keySet().iterator();\n\t\twhile(iterator.hasNext()) {\n\t\t\tString key = iterator.next();\n\t\t\tif(keys2.contains(key) == false) {\n\t\t\t\titerator.remove();\n\t\t\t\tthis.remove(key);\n\t\t\t}\n\n\t\t}\n\t\treturn this;\n\t}\n\t/** 清理掉所有key */\n\tpublic SoMap clearAll() {\n\t\tclear();\n\t\treturn this;\n\t}\n\t\n\n\t// ============================= 快速构建 ============================= \n\n\t/** 构建一个SoMap并返回 */\n\tpublic static SoMap getSoMap() {\n\t\treturn new SoMap();\n\t}\n\t/** 构建一个SoMap并返回 */\n\tpublic static SoMap getSoMap(String key, Object value) {\n\t\treturn new SoMap().set(key, value);\n\t}\n\t/** 构建一个SoMap并返回 */\n\tpublic static SoMap getSoMap(Map<String, ?> map) {\n\t\treturn new SoMap().setMap(map);\n\t}\n\n\t/** 将一个对象集合解析成为SoMap */\n\tpublic static SoMap getSoMapByModel(Object model) {\n\t\treturn SoMap.getSoMap().setModel(model);\n\t}\n\t\n\t/** 将一个对象集合解析成为SoMap集合 */\n\tpublic static List<SoMap> getSoMapByList(List<?> list) {\n\t\tList<SoMap> listMap = new ArrayList<SoMap>();\n\t\tfor (Object model : list) {\n\t\t\tlistMap.add(getSoMapByModel(model));\n\t\t}\n\t\treturn listMap;\n\t}\n\t\n\t/** 克隆指定key，返回一个新的SoMap */\n\tpublic SoMap cloneKeys(String... keys) {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : keys) {\n\t\t\tso.set(key, this.get(key));\n\t\t}\n\t\treturn so;\n\t}\n\t/** 克隆所有key，返回一个新的SoMap */\n\tpublic SoMap cloneSoMap() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(key, this.get(key));\n\t\t}\n\t\treturn so;\n\t}\n\n\t/** 将所有key转为大写 */\n\tpublic SoMap toUpperCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(key.toUpperCase(), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key转为小写 */\n\tpublic SoMap toLowerCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(key.toLowerCase(), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key中下划线转为中划线模式 (kebab-case风格) */\n\tpublic SoMap toKebabCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(wordEachKebabCase(key), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key中下划线转为小驼峰模式 */\n\tpublic SoMap toHumpCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(wordEachBigFs(key), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t/** 将所有key中小驼峰转为下划线模式 */\n\tpublic SoMap humpToLineCase() {\n\t\tSoMap so = new SoMap();\n\t\tfor (String key : this.keySet()) {\n\t\t\tso.set(wordHumpToLine(key), this.get(key));\n\t\t}\n\t\tthis.clearAll().setMap(so);\n\t\treturn this;\n\t}\n\t\n\t\n\t\n\t\n\t// ============================= 辅助方法 =============================\n\n\n\t/** 指定key是否为null，判定标准为 NULL_ELEMENT_ARRAY 中的元素  */\n\tpublic boolean isNull(String key) {\n\t\treturn valueIsNull(get(key));\n\t}\n\n\t/** 指定key列表中是否包含value为null的元素，只要有一个为null，就会返回true */\n\tpublic boolean isContainNull(String ...keys) {\n\t\tfor (String key : keys) {\n\t\t\tif(this.isNull(key)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t/** 与isNull()相反 */\n\tpublic boolean isNotNull(String key) {\n\t\treturn !isNull(key);\n\t}\n\t/** 指定key的value是否为null，作用同isNotNull() */\n\tpublic boolean has(String key) {\n\t\treturn !isNull(key);\n\t}\n\t\n\t/** 指定value在此SoMap的判断标准中是否为null */\n\tpublic boolean valueIsNull(Object value) {\n\t\treturn NULL_ELEMENT_LIST.contains(value);\n\t}\n\t\n\t/** 验证指定key不为空，为空则抛出异常 */\n\tpublic SoMap checkNull(String ...keys) {\n\t\tfor (String key : keys) {\n\t\t\tif(this.isNull(key)) {\n\t\t\t\tthrow new RuntimeException(\"参数\" + key + \"不能为空\");\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\tstatic Pattern patternNumber = Pattern.compile(\"[0-9]*\");\n\t/** 指定key是否为数字 */\n\tpublic boolean isNumber(String key) {\n\t\tString value = getString(key);\n\t\tif(value == null) {\n\t\t\treturn false;\n\t\t}\n\t    return patternNumber.matcher(value).matches();   \n\t}\n\n\t\n\t\n\t\n\t/**\n\t * 转为JSON字符串\n\t */\n\tpublic String toJsonString() {\n\t\ttry {\n//\t\t\tSoMap so = SoMap.getSoMap(this);\n\t\t\treturn new ObjectMapper().writeValueAsString(this);\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n//\n//\t/**\n//\t * 转为JSON字符串, 带格式的 \n//\t */\n//\tpublic String toJsonFormatString() {\n//\t\ttry {\n//\t\t\treturn JSON.toJSONString(this, true); \n//\t\t} catch (Exception e) {\n//\t\t\tthrow new RuntimeException(e);\n//\t\t}\n//\t}\n\n\t// ============================= web辅助 =============================\n\n\n\t/**\n\t * 返回当前request请求的的所有参数 \n\t * @return\n\t */\n\tpublic static SoMap getRequestSoMap() {\n\t\t// 大善人SpringMVC提供的封装 \n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n\t\tif(servletRequestAttributes == null) {\n\t\t\tthrow new RuntimeException(\"当前线程非JavaWeb环境\");\n\t\t}\n\t\t// 当前request\n\t\tHttpServletRequest request = servletRequestAttributes.getRequest(); \n\t\tif (request.getAttribute(\"currentSoMap\") == null || request.getAttribute(\"currentSoMap\") instanceof SoMap == false ) {\n\t\t\tinitRequestSoMap(request);\n\t\t}\n\t\treturn (SoMap)request.getAttribute(\"currentSoMap\");\n\t}\n\n\t/** 初始化当前request的 SoMap */\n\tprivate static void initRequestSoMap(HttpServletRequest request) {\n\t\tSoMap soMap = new SoMap();\n\t\tMap<String, String[]> parameterMap = request.getParameterMap();\t// 获取所有参数 \n\t\tfor (String key : parameterMap.keySet()) {\n\t\t\ttry {\n\t\t\t\tString[] values = parameterMap.get(key); // 获得values \n\t\t\t\tif(values.length == 1) {\n\t\t\t\t\tsoMap.set(key, values[0]);\n\t\t\t\t} else {\n\t\t\t\t\tList<String> list = new ArrayList<String>();\n\t\t\t\t\tfor (String v : values) {\n\t\t\t\t\t\tlist.add(v);\n\t\t\t\t\t}\n\t\t\t\t\tsoMap.set(key, list);\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new RuntimeException(e);\n\t\t\t}\n\t\t}\n\t\trequest.setAttribute(\"currentSoMap\", soMap);\n\t}\n\t\n\t/**\n\t * 验证返回当前线程是否为JavaWeb环境 \n\t * @return\n\t */\n\tpublic static boolean isJavaWeb() {\n\t\tServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 大善人SpringMVC提供的封装 \n\t\tif(servletRequestAttributes == null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\t\n\n\n\t// ============================= 常见key （以下key经常用，所以封装以下，方便写代码） =============================\n\n\t/** get 当前页  */\n\tpublic int getKeyPageNo() {\n\t\tint pageNo = getInt(\"pageNo\", 1);\n\t\tif(pageNo <= 0) {\n\t\t\tpageNo = 1;\n\t\t}\n\t\treturn pageNo;\n\t}\n\t/** get 页大小  */\n\tpublic int getKeyPageSize() {\n\t\tint pageSize = getInt(\"pageSize\", 10);\n\t\tif(pageSize <= 0 || pageSize > 1000) {\n\t\t\tpageSize = 10;\n\t\t}\n\t\treturn pageSize;\n\t}\n\n\t/** get 排序方式 */\n\tpublic int getKeySortType() {\n\t\treturn getInt(\"sortType\");\n\t}\n\n\n\n\n\n\t// ============================= 分页相关(封装mybatis的page-help插件 ) =============================\n\n//\t/** 分页插件 */\n//\tprivate com.github.pagehelper.Page<?> pagePlug;\n//\t/** 分页插件 - 开始分页 */\n//\tpublic SoMap startPage() {\n//\t\tthis.pagePlug= com.github.pagehelper.PageHelper.startPage(getKeyPageNo(), getKeyPageSize());\n//\t\treturn this;\n//\t}\n//\t/** 获取上次分页的记录总数 */\n//\tpublic long getDataCount() {\n//\t\tif(pagePlug == null) {\n//\t\t\treturn -1;\n//\t\t}\n//\t\treturn pagePlug.getTotal();\n//\t}\n//\t/** 分页插件 - 结束分页, 返回总条数 （该方法已过时，请调用更加符合语义化的getDataCount() ） */\n//\t@Deprecated\n//\tpublic long endPage() {\n//\t\treturn getDataCount();\n//\t}\n\n\t\n\t\n\t\n\n\t// ============================= 工具方法 =============================\n\t\n\n\t/**\n\t * 将一个一维集合转换为树形集合 \n\t * @param list         集合\n\t * @param idKey        id标识key\n\t * @param parentIdKey  父id标识key\n\t * @param childListKey 子节点标识key\n\t * @return 转换后的tree集合 \n\t */\n\tpublic static List<SoMap> listToTree(List<SoMap> list, String idKey, String parentIdKey, String childListKey) {\n\t\t// 声明新的集合，存储tree形数据 \n\t\tList<SoMap> newTreeList = new ArrayList<SoMap>();\n\t\t// 声明hash-Map，方便查找数据 \n\t\tSoMap hash = new SoMap();\n\t\t// 将数组转为Object的形式，key为数组中的id \n\t\tfor (int i = 0; i < list.size(); i++) {\n\t\t\tSoMap json = (SoMap) list.get(i);\n\t\t\thash.put(json.getString(idKey), json);\n\t\t}\n\t\t// 遍历结果集\n\t\tfor (int j = 0; j < list.size(); j++) {\n\t\t\t// 单条记录\n\t\t\tSoMap aVal = (SoMap) list.get(j);\n\t\t\t// 在hash中取出key为单条记录中pid的值\n\t\t\tSoMap hashVp = (SoMap) hash.get(aVal.get(parentIdKey, \"\").toString());\n\t\t\t// 如果记录的pid存在，则说明它有父节点，将她添加到孩子节点的集合中\n\t\t\tif (hashVp != null) {\n\t\t\t\t// 检查是否有child属性，有则添加，没有则新建 \n\t\t\t\tif (hashVp.get(childListKey) != null) {\n\t\t\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\t\t\tList<SoMap> ch = (List<SoMap>) hashVp.get(childListKey);\n\t\t\t\t\tch.add(aVal);\n\t\t\t\t\thashVp.put(childListKey, ch);\n\t\t\t\t} else {\n\t\t\t\t\tList<SoMap> ch = new ArrayList<SoMap>();\n\t\t\t\t\tch.add(aVal);\n\t\t\t\t\thashVp.put(childListKey, ch);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnewTreeList.add(aVal);\n\t\t\t}\n\t\t}\n\t\treturn newTreeList;\n\t}\n\t\n\t\n\n\t/** 指定字符串的字符串下划线转大写模式 */\n\tprivate static String wordEachBig(String str){\n\t\tString newStr = \"\";\n\t\tfor (String s : str.split(\"_\")) {\n\t\t\tnewStr += wordFirstBig(s);\n\t\t}\n\t\treturn newStr;\n\t}\n\t/** 返回下划线转小驼峰形式 */\n\tprivate static String wordEachBigFs(String str){\n\t\treturn wordFirstSmall(wordEachBig(str));\n\t}\n\n\t/** 将指定单词首字母大写 */\n\tprivate static String wordFirstBig(String str) {\n\t\treturn str.substring(0, 1).toUpperCase() + str.substring(1, str.length());\n\t}\n\n\t/** 将指定单词首字母小写 */\n\tprivate static String wordFirstSmall(String str) {\n\t\treturn str.substring(0, 1).toLowerCase() + str.substring(1, str.length());\n\t}\n\n\t/** 下划线转中划线 */\n\tprivate static String wordEachKebabCase(String str) {\n\t\treturn str.replaceAll(\"_\", \"-\");\n\t}\n\n\t/** 驼峰转下划线  */\n\tprivate static String wordHumpToLine(String str) {\n\t\treturn str.replaceAll(\"[A-Z]\", \"_$0\").toLowerCase();\n\t}\n\t\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# Sa-Token配置\nsa-token: \n    # Token名称 (同时也是cookie名称)\n    token-name: satoken\n        \n        "
  },
  {
    "path": "sa-token-test/sa-token-springboot-test/src/test/resources/sa-token2.properties",
    "content": "# token 名称 (同时也是 cookie 名称)\ntokenName=use-token\n# token 有效期（单位：秒） 默认30天，-1 代表永久有效\ntimeout=9000\n# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\nactiveTimeout=240\n# 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\nisConcurrent=false\n# 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\nisShare=false\n# 是否输出操作日志 \nisLog=true"
  },
  {
    "path": "sa-token-test/sa-token-temp-jwt-test/pom.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t\n\t<parent>\n        <groupId>cn.dev33</groupId>\n        <artifactId>sa-token-test</artifactId>\n        <version>${revision}</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n    <packaging>jar</packaging>\n\n\t<name>sa-token-temp-jwt-test</name>\n    <artifactId>sa-token-temp-jwt-test</artifactId>\n\t<description>sa-token-temp-jwt-test</description>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-spring-boot-starter</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>cn.dev33</groupId>\n\t\t\t<artifactId>sa-token-temp-jwt</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java",
    "content": "package com.pj.test;\n\nimport cn.dev33.satoken.SaManager;\nimport cn.dev33.satoken.dao.SaTokenDao;\nimport cn.dev33.satoken.exception.ApiDisabledException;\nimport cn.dev33.satoken.temp.SaTempUtil;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/**\n * Sa-Token 整合 temp jwt\n *\n * @author click33\n * @since 1.42.0\n */\n@SpringBootTest(classes = StartUpApplication.class)\npublic class SaTempTemplateForJwtTest {\n\n\t// 开始 \n\t@BeforeAll\n    public static void beforeClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaTempTemplateForJwtTest star ...\");\n    }\n\n\t// 结束 \n    @AfterAll\n    public static void afterClass() {\n    \tSystem.out.println(\"\\n\\n------------------------ SaTempTemplateForJwtTest end ... \\n\");\n    }\n\n\t// 测试：临时Token认证模块\n\t@Test\n\tpublic void testSaTemp() {\n\n\t\t// 生成token\n\t\tString token = SaTempUtil.createToken(\"group-1014\", 200);\n\t\t//\t\t System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet());\n\t\t//\t\tSystem.out.println(\"satoken:temp-token:\" + \":\" + token);\n\t\tAssertions.assertNotNull(token);\n\n\t\t// 解析token\n\t\tString value = SaTempUtil.parseToken(token, String.class);\n\t\tAssertions.assertEquals(value, \"group-1014\");\n\n\t\t// 解析 token 并裁剪前缀\n\t\tlong value2 = SaTempUtil.parseToken(token, \"group-\", Long.class);\n\t\tAssertions.assertEquals(value2, 1014);\n\n\t\t// 默认类型\n\t\tObject value3 = SaTempUtil.parseToken(token);\n\t\tAssertions.assertEquals(value3, \"group-1014\");\n\n\t\t// 转换类型\n\t\tString value4 = SaTempUtil.parseToken(token, String.class);\n\t\tAssertions.assertEquals(value4, \"group-1014\");\n\n\t\t// 过期时间\n\t\tlong timeout = SaTempUtil.getTimeout(token);\n\t\tAssertions.assertTrue(timeout > 195);\n\t\tAssertions.assertTrue(timeout < 201);\n\n\t\t// 回收token\n\t\tAssertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.deleteToken(token) );\n\t}\n\n\t// 测试：临时Token认证模块索引\n\t@Test\n\tpublic void testSaTempIndex() {\n\t\tSaTokenDao dao = SaManager.getSaTokenDao();\n\n\t\t// 生成token\n\t\tString token1 = SaTempUtil.createToken(\"1001\", 200, true);\n\t\tString token2 = SaTempUtil.createToken(\"1001\", 300, true);\n\t\tString token3 = SaTempUtil.createToken(\"1001\", 400, true);\n\n\t\tAssertions.assertNotNull(token1);\n\t\tAssertions.assertNotNull(token2);\n\t\tAssertions.assertNotNull(token3);\n\t\t// System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap);\n\n\t\t// 解析token\n\t\tAssertions.assertEquals(SaTempUtil.parseToken(token1, String.class), \"1001\");\n\t\tAssertions.assertEquals(SaTempUtil.parseToken(token2, String.class), \"1001\");\n\t\tAssertions.assertEquals(SaTempUtil.parseToken(token3, String.class), \"1001\");\n\n\t\t// 缓存数据比对\n        Assertions.assertNull(dao.getObject(\"satoken:temp-token:\" + token1));\n        Assertions.assertNull(dao.getObject(\"satoken:temp-token:\" + token2));\n        Assertions.assertNull(dao.getObject(\"satoken:temp-token:\" + token3));\n\n\t\t// 索引\n\t\tAssertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.getTempTokenList(\"1001\") );\n\t}\n\n\t@Test\n\tpublic void testGetJwtSecretKey() {\n\t\t// 秘钥默认为null\n\t\tString jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey();\n\t\tAssertions.assertNotNull(jwtSecretKey);\n\t}\n\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java",
    "content": "package com.pj.test;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * 启动类 \n * @author Auster\n *\n */\n@SpringBootApplication\npublic class StartUpApplication {\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(StartUpApplication.class, args);\n\t}\n}\n"
  },
  {
    "path": "sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml",
    "content": "# 端口\nserver:\n    port: 8081\n\n# sa-token 配置\nsa-token: \n    # token 名称 (同时也是 cookie 名称)\n    token-name: satoken\n    # token 有效期（单位：秒） 默认30天，-1 代表永久有效\n    timeout: 2592000\n    # token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结\n    active-timeout: -1\n    # 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）\n    is-concurrent: true\n    # 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）\n    is-share: false\n    # token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）\n    token-style: uuid\n    # jwt秘钥 \n    jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk\n    \nspring: \n    # redis配置 \n    redis:\n        # Redis数据库索引（默认为0）\n        database: 0\n        # Redis服务器地址\n        host: 127.0.0.1\n        # Redis服务器连接端口\n        port: 6379\n        # Redis服务器连接密码（默认为空）\n        password: \n        # 连接超时时间（毫秒）\n        timeout: 10000ms\n        lettuce:\n            pool:\n                # 连接池最大连接数\n                max-active: 200\n                # 连接池最大阻塞等待时间（使用负值表示没有限制）\n                max-wait: -1ms\n                # 连接池中的最大空闲连接\n                max-idle: 10\n                # 连接池中的最小空闲连接\n                min-idle: 0\n        \n        "
  }
]