# Technical Documentation: dromara/Sa-Token

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/dromara/Sa-Token)

### Overview (`README.md`)

简体中文 | [繁體中文](./README_zh_TW.md) | [English](./README_en.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md)

<p align="center">
	<img alt="logo" src="https://sa-token.com/logo.png" width="150" height="150">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.46.0</h1>
<h4 align="center">✨ 开源、免费、一站式 java 权限认证框架，让鉴权变得简单、优雅！ </h4>
<p align="center">
	<a href="https://gitee.com/dromara/sa-token/stargazers"><img src="https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp"></a>
	<a href="https://gitee.com/dromara/sa-token/members"><img src="https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp"></a>
	<a href="https://atomgit.com/dromara/sa-token/stargazers"><img src="https://atomgit.com/dromara/Sa-Token/star/badge.svg"></a>
	<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>
	<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>
	
	
	<img src="https://img.shields.io/badge/JDK-8~25-green.svg?style=flat-square" alt="JDK 8~25">
	<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>
</p>

<p align="center">
	<a href="https://sa-token.com?way=readme" target="_blank">在线文档：https://sa-token.com</a>
	&nbsp;|&nbsp;
	<a href="https://sa-token.com/doc.html#/more/demand-commit" target="_blank">需求提交</a>
</p>


---

### 📝 前言：

回望 2020 年初，我为 Sa-Token 提交第一行代码之际，**彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架，更是一整套清晰、自洽的权限架构设计思想**。

因此，这几年间我将大量时间倾注在 Sa-Token 的文档编写，几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨，以求做到最清晰、干练、易懂的表述。**用心阅读文档，你学习到的将不止是 Sa-Token 框架本身，更是绝大多数场景下权限认证设计的最佳实践**。


### 🛠️ Sa-Token 介绍

Sa-Token 是一个轻量级 Java 权限认证框架，目前拥有五大核心模块：登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。

你还在手搓以下功能？**Stop，让 Sa-Token 来！**



要在 SpringBoot 项目中使用 Sa-Token，你只需要在 pom.xml 中引入依赖：

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-boot-starter</artifactId>
	<version>1.46.0</version>
</dependency>
```

除了支持 SpringBoot2、Sa-Token 还为 SpringBoot3/4、Solon、JFinal 等常见 Web 框架提供集成包，做到真正的开箱即用。


<details>
<summary><b>简单示例展示：</b>（点击展开 / 折叠）</summary>

Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分，以登录认证为例，你只需要：

``` java
// 会话登录，参数填登录人的账号id 
StpUtil.login(10001);
```

无需实现任何接口，无需创建任何配置文件，只需要这一句静态代码的调用，便可以完成会话登录认证。

如果一个接口需要登录后才能访问，我们只需调用以下代码：

``` java
// 校验当前客户端是否已经登录，如果未登录则抛出 `NotLoginException` 异常
StpUtil.checkLogin();
```

在 Sa-Token 中，大多数功能都可以一行代码解决：

踢人下线：

``` java
// 将账号id为 10077 的会话踢下线 
StpUtil.kickout(10077);
```

权限认证：

``` java
// 注解鉴权：只有具备 `user:add` 权限的会话才可以进入方法
@SaCheckPermission("user:add")
public String insert(SysUser user) {
    // ... 
    return "用户增加";
}
```

路由拦截鉴权：

``` java
// 根据路由划分模块，不同模块不同鉴权 
registry.addInterceptor(new SaInterceptor(handler -> {
	SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
	SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
	SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
	SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
	SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
	// 更多模块... 
})).addPathPatterns("/**");
```

**如果您曾经使用过 Shiro、SpringSecurity，在切换到 Sa-Token 后，您将体会到质的飞跃。**



</details>


<details>
<summary> <b>核心模块一览：</b>（点击展开 / 折叠） </summary>

- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。
- **权限认证** —— 权限认证、角色认证、会话二级认证。
- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线。
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离。
- **路由拦截式鉴权** —— 根据路由拦截鉴权，可适配 restful 模式。
- **Session会话** —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。
- **持久层扩展** —— 可集成 Redis，重启数据不丢失。
- **前后台分离** —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。
- **Token风格定制** —— 内置六种 Token 风格，还可：自定义 Token 生成策略。
- **记住我模式** —— 适配 [记住我] 模式，重启浏览器免验证。
- **二级认证** —— 在已登录的基础上再次认证，保证安全性。 
- **模拟他人账号** —— 实时操作任意用户状态数据。
- **临时身份切换** —— 将会话身份临时切换为其它账号。
- **同端互斥登录** —— 像QQ一样手机电脑同时在线，但是两个手机上互斥登录。
- **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。
- **密码加密** —— 提供基础加密算法，可快速 MD5、SHA1、SHA256、AES 加密。
- **会话查询** —— 提供方便灵活的会话查询接口。
- **Http Basic认证** —— 一行代码接入 Http Basic、Digest 认证。
- **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
- **全局过滤器** —— 方便的处理跨域，全局设置安全响应头等操作。
- **多账号体系认证** —— 一个系统多套账号分开鉴权（比如商城的 User 表和 Admin 表）
- **单点登录** —— 内置三种单点登录模式：同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
- **单点注销** —— 任意子系统内发起注销，即可全端下线。
- **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务，支持openid模式 。
- **分布式会话** —— 提供共享数据中心分布式会话方案。
- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。
- **RPC调用鉴权** —— 网关转发鉴权，RPC调用鉴权，让服务调用不再裸奔
- **临时Token认证** —— 解决短时间的 Token 授权问题。
- **独立Redis** —— 将权限缓存与业务缓存分离。
- **Quick快速登录认证** —— 为项目零代码注入一个登录页面。
- **标签方言** —— 提供 Thymeleaf 标签方言集成包，提供 beetl 集成示例。
- **jwt集成** —— 提供三种模式的 jwt 集成方案，提供 token 扩展参数能力。
- **RPC调用状态传递** —— 提供 dubbo、grpc 等集成包，在RPC调用时登录状态不丢失。
- **参数签名** —— 提供跨系统API调用签名校验模块，防参数篡改，防请求重放。
- **自动续签** —— 提供两种Token过期策略，灵活搭配使用，还可自动续签。
- **开箱即用** —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包，开箱即用。
- **最新技术栈** —— 适配最新技术栈：支持 SpringBoot 3.x，jdk 17。

</details>



### 🍃 SSO 单点登录

Sa-Token SSO 分为三种模式，可解决：`同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目` 等架构下的 SSO 认证需求：




| 系统架构						| 采用模式	| 简介						        |  文档链接	|
| :--------						| :--------	|:----------------| :--------	|
| 前端同域 + 后端同 Redis			| 模式一		| 共享Cookie同步会话			 | [文档](https://sa-token.com/doc.html#/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso1-client)	|
| 前端不同域 + 后端同 Redis		| 模式二		| URL重定向传播会话 			  | [文档](https://sa-token.com/doc.html#/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso2-client)	|
| 前端不同域 + 后端 不同Redis		| 模式三		| HTTP请求获取会话			   | [文档](https://sa-token.com/doc.html#/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso3-client)	|


1. 前端同域：就是指多个系统可以部署在同一个主域名之下，比如：`c1.domain.com`、`c2.domain.com`、`c3.domain.com`
2. 后端同 Redis：就是指多个系统可以连接同一个 Redis，共享会话数据。
3. 如果无法做到前端同域、后端同 Redis，可以走托底的模式三：Http请求校验 ticket 获取会话。
4. 提供：NoSdk 模式示例 + sso-server 接口文档，非 Sa-Token 项目、非 java 项目也可以对接。
5. 提供：多重安全校验：域名校验、ticket校验、参数签名校验，有效防 ticket 劫持，防请求重放等攻击。
6. 提供：大量实战痛点教学：sso-server 前后端分离设计、sso-client 前后端分离设计、用户数据同步/迁移方案设计。
7. 提供：直接可运行的 demo 示例，助你快速熟悉 SSO 大致登录流程。
8. 提供：深度细节优化，参数防丢：笔者曾试验多个SSO框架，均有参数丢失情况，比如登录前是：`http://a.com?id=1&name=2`，登录成功后就变成了：`http://a.com?id=1`，Sa-Token-SSO 内有专门算法保证了参数不丢失，登录成功后精准原路返回。




### 🍂 OAuth2 授权认证
Sa-Token OAuth2 模块分为四种授权模式，解决不同场景下的授权需求 

| 授权模式					| 简介						|
| :--------					| :--------					|
| 授权码式					| OAuth2 标准授权步骤，server 端下放 code，client 端获取 code 码兑换 access_token			|
| 隐藏式					| 备用选择，server 端使用 URL 重定向方式直接将 access_token 下放到 client 端页面 			|
| 密码式					| client 直接拿着用户的账号密码换取授权 access_token				|
| 客户端凭证式				| server 端针对 client 级别的 client_token，代表应用自身的资源授权		|

详细参考文档：[https://sa-token.com/doc.html#/oauth2/readme](https://sa-token.com/doc.html#/oauth2/readme)


### 📖❓ 疑问解答

**1、Sa-Token 功能全不全？**

七年磨一剑：五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证，我们提供的是一站式解决方案。


**2、Sa-Token 好不好学？**

中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。


**3、Sa-Token 用的人多不多？**

截止统计日 (2026-1-25) 起，Sa-Token 在：

- Gitee 关注量达到 48627 Star，位列平台所有推荐项目排行榜第一名。
- GitHub 关注量达到 18523 Star，是主要竞争框架 Spring Security 的 1.97 倍，Apache Shiro 的 4.19 倍。
- 25+ 微信粉丝群 (500人)，8+ QQ粉丝群 (1000人 or 2000人) ，在线文档访问量月PV 20万+。

这是众多开发者用脚投票的数据，相信这些数据比任何言语都能证明 Sa-Token 的热度。


**4、Sa-Token 有哪些权威认证？**

曾获荣誉包括但不限于：Gitee GVP 最有价值开源项目、GitCode G-Star 优质开源项目、OSCHINA 2021 人气指数 TOP 30 开源项目、OSCHINA 2022 年度最火热中国开源项目社区之一、开放原子基金会2023快速成长开源项目、 Dromara 组织顶尖项目（之一）、可信开源社区共同体预备成员、所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛（开源）》二等奖。 Gitee High Star 计划项目(5000+star)。Gitee 2025年度开源项目 Web应用开发 Top 2。


**5、Sa-Token 收费吗？**

Sa-Token 采用 Apache-2.0 开源协议，承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token，我们也不回避：[赞助链接](https://sa-token.com/doc.html#/more/sa-token-donate)。
我们将定期同步赞助者名单到在线文档展示。（您需要注意的一点是：该赞助仅为友情赞助，不提供任何商业交换）


**6、Sa-Token 是封装的 SpringSecurity 吗？是套壳 ApacheShiro 吗？**

不是。Sa-Token 不是一个后台模板，也不是针对 xx 框架的二次封装套壳，而是从 0 开始的纯血自研框架，核心包零依赖，完全自主可控的架构内核 + 众多主流框架的集成适配。
						



#### 证书 ⭐ 奖杯 🏆 荣誉展示 

<table align="center">
  <tr>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/gvp.jpg" title="GVP - Gitee 最有价值开源项目" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/g-star.jpg" title="GitCode G-Star 优质开源项目" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/osc-2021.jpg" title="OSCHINA 2021 人气指数 TOP 30 开源项目" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/osc-2022--chang.jpg" title="OSCHINA 2022 年度最火热中国开源项目社区" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/kexin.jpg" title="可信开源社区共同体预备成员" /></td>
  </tr>
  <tr>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/gitee-star-5000.jpg" title="Gitee 5000 star 专属奖杯" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/gitee-2025--chang.jpg" title="Gitee 2025年度开源项目 Web应用开发 Top 2" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/dromara.jpg" title="Dromara 组织顶尖项目（之一）" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/kaifangyuanzi2--chang.jpg" title="开放原子基金会2023快速成长开源项目" /></td>
    <td align="center" width="330"><img src="https://sa-token.com/big-file/index/awards-zip/dromara-2024-tzds.jpg" title="Dromara 荣获《2024中国互联网发展创新与投资大赛（开源）》二等奖" /></td>
  </tr>
</table>



### 🚀 优秀开源集成案例

- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy)：国内首个国密前后分离快速开发平台，采用 Vue3 + Vite + SpringBoot + Mp + HuTool + SaToken。
- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus)：重写RuoYi-Vue所有功能 集成 Sa-Token、Mybatis-Plus、Xxl-Job、knife4j、OSS 定期同步。
- [[ Smart-Admin ]](https://gitee.com/lab1024/smart-admin)：SmartAdmin 国内首个以「高质量代码」为核心，「简洁、高效、安全」中后台快速开发平台。
- [[ 橙单 ]](https://gitee.com/orangeform/orange-admin)： 橙单中台化低代码生成器。可完整支持多应用、多租户、多渠道、工作流、框架技术栈自由组合等。
- [[ 灯灯 ]](https://gitee.com/dromara/lamp-cloud)： 专注于多租户解决方案的中后台快速开发平台。支持独立数据库、共享数据架构 和 非租户模式 ✨
- [[ 拾壹博客 ]](https://gitee.com/quequnlong/shiyi-blog)：一款 vue + springboot 前后端分离的博客系统。



更多案例请参考：[Sa-Token 优秀开源集成案例](https://sa-token.com/cases.html)


### 🌍 其它语言版本

Sa-Token 社区成员贡献了多语言实现版本：

- Rust 版本：[https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust)
- Go 版本：[https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go)
- NodeJS 版本：[https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token)
- PHP 版本：[https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token)

我们诚邀对上述语言较为熟练的开发者，一起建设相关版本。🤝


### 🔗 友情链接
- [[ OkHttps ]](https://gitee.com/ejlchina-zhxu/okhttps)：轻量级 http 通信框架，API无比优雅，支持 WebSocket、Stomp 协议
- [[ Forest ]](https://gitee.com/dromara/forest)：声明式与编程式双修，让天下没有难以发送的 HTTP 请求
- [[ Bean Searcher ]](https://github.com/troyzhxu/bean-searcher)：REST 版的 GraphQL — 实体定义边界，参数驱动查询，一行代码实现复杂查询！
- [[ Jpom ]](https://gitee.com/dromara/Jpom)：简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件。
- [[ TLog ]](https://gitee.com/dromara/TLog)：一个轻量级的分布式日志标记追踪神器。
- [[ hippo4j ]](https://gitee.com/agentart/hippo4j)：强大的动态线程池框架，附带监控报警功能。
- [[ hertzbeat ]](https://gitee.com/dromara/hertzbeat)：易用友好的开源实时监控告警系统，无需Agent，高性能集群，强大自定义监控能力。
- [[ Solon ]](https://gitee.com/noear/solon)：一个更现代感的应用开发框架：更快、更小、更自由。
- [[ Chat2DB ]](https://github.com/chat2db/Chat2DB)：一个AI驱动的数据库管理和BI工具，支持Mysql、pg、Oracle、Redis等22种数据库的管理。



### 📦 代码托管
- Gitee：[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token)
- GitHub：[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token)
- AtomGit：[https://atomgit.com/dromara/sa-token](https://atomgit.com/dromara/sa-token)



### 📚 示例大全

**我们为框架几乎所有技术点均单独制作了对应的集成示例，此压缩包共计 60+ Demo**：涵盖 Sa-Token 登录认证、权限认证、SSO 单点登录、OAuth2 统一认证、微服务鉴权、API Key 认证、JWT集成、跨系统调用参数签名校验 等鉴权认证的方方面面。

下载地址：[https://sa-token.com/doc.html#/more/download-demos](https://sa-token.com/doc.html#/more/download-demos) 

<img class="s-w" src="https://sa-token.com/big-file/contact/show/sa-token-demos-pre-liubai.png" />



### 💬 交流群



QQ交流群：1098917026 [点击加入](https://qm.qq.com/q/26OFBGd2Zy) 

微信交流群：



<img src="https://sa-token.com/big-file/contact/i-wx-qr2.jpg" width="230px" title="微信群" />

PS：扫码添加微信 (备注：sa-token)，邀您加入群聊。

<br>

<img class="s-w" src="https://sa-token.com/big-file/contact/show/wx-group-show3--liubai.png" style="max-width: 50%;" alt="微信群" />


加入群聊的好处：
- 第一时间收到框架更新通知。
- 第一时间收到框架 bug 通知。
- 第一时间收到新增开源案例通知。
- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。

---

### Overview (`sa-token-doc-new/README.md`)

# Sa-Token 文档站

本目录是 [sa-token.com](https://sa-token.com) 的文档源码，基于 VitePress。

## 环境

- Node **20.11+**（建议 22）
- 包管理：npm

```bash
cd sa-token-doc-new
npm i
```

## 常用命令

| 命令 | 作用 |
|---|---|
| `npm run docs:dev` | 本地预览（http://localhost:5173） |
| `npm run docs:build` | 编译到 `dist/` |
| `npm run docs:preview` | 预览已构建的 dist |

开发时：

- `/` 是官网首页（`public/index.html`）
- `/readme.html` 是文档介绍页
- `/blog/` 是独立博客页

改侧栏：直接改 `.vitepress/sidebar.ts`。

## 发布

```bash
cd sa-token-doc-new
npm i
npm run docs:build
```

把 **`dist/` 里的内容** 上传到 `sa-token.com` 根目录。上传的是编译后的 HTML / JS / CSS，不要上传 `docs/` 下的 markdown。

## 目录说明

```
sa-token-doc-new/
  .vitepress/            配置、主题、侧栏
  public/                独立首页、博客、静态资源
  docs/                  文档正文（md）
  dist/                  构建产物（上传这个）
```

---

### Overview (`sa-token-testing/README.md`)

# sa-token-testing

测试基础设施与集成测试。公共接线在 `integration-boot2` 验一次；`integration-boot3/4` 只补版本差异，不重复全量。

## 子模块

### sa-token-test-support

测试支持（无 `@Test`），提供共享测试模型（SysUser / SysRole）供各插件单测复用（如 sa-token-serializer-features）。

### sa-token-json-test-common

测试支持（无 `@Test`），供 JSON 插件单测复用：

- sa-token-jackson
- sa-token-jackson3
- sa-token-fastjson
- sa-token-fastjson2
- sa-token-fory-json
- sa-token-snack3
- sa-token-snack4

### sa-token-redis-dao-test-common

测试支持（无 `@Test`），供 Redis Dao 插件单测复用：

- sa-token-redisson
- sa-token-redisx

### sa-token-http-test-common

测试支持（无 `@Test`），供 HTTP 客户端插件单测复用：

- sa-token-forest
- sa-token-okhttps
- sa-token-rest-client
- sa-token-rest-template

### sa-token-integration-boot2

集成测试（Boot 2 主集成，覆盖 Starter 接线与业务场景）：

- sa-token-spring-boot-starter
- sa-token-spring-boot-webmvc-reactor-v2v3v4-common
- sa-token-servlet
- sa-token-jackson（Starter 传递依赖）

### sa-token-integration-beaninject-boot2

Bean 注入专项集成测试（**独立模块 / 独立 JVM**，避免污染 `SaManager` 等全局静态状态）：

- 覆盖 `SaBeanInject` + OAuth2 / SSO / Sign / ApiKey 全部 `*BeanInject` 注入点
- 仅验证 Spring Bean → Manager/Strategy 的注入链路，不测插件业务

### sa-token-integration-boot3

集成测试（仅 Boot 3 与 Boot 2 的行为差异）：

- sa-token-spring-boot3-starter
- sa-token-spring-boot-webmvc-v3v4-common
- sa-token-jakarta-servlet

### sa-token-integration-boot4

集成测试（仅 Boot 4 差异；`webmvc-v3v4-common` / `jakarta-servlet` 见 boot3）：

- sa-token-spring-boot4-starter
- sa-token-jackson3

### sa-token-integration-reactor-boot2

WebFlux 集成测试（Boot 2 主集成，基于 WebTestClient）：

- sa-token-reactor-spring-boot-starter

### sa-token-integration-reactor-boot3

WebFlux 集成测试（仅 Boot 3 版本差异）：

- sa-token-reactor-spring-boot3-starter（真身 reactor-v3v4-common）

### sa-token-integration-reactor-boot4

WebFlux 集成测试（仅 Boot 4 版本差异）：

- sa-token-reactor-spring-boot4-starter（真身 reactor-v3v4-common）

### sa-token-integration-solon

Solon HTTP 集成测试（Filter / Interceptor、真实请求链路）：

- sa-token-solon-plugin

### sa-token-integration-beaninject-solon

Bean 注入专项集成测试（**独立模块 / 独立 JVM**，避免污染 `SaManager` 等全局静态状态）：

- 覆盖 `SaBeanInject` + OAuth2 / SSO / Sign / ApiKey 全部 `*BeanInject` 注入点
- 仅验证 Solon Bean → Manager/Strategy 的注入链路，不测插件业务

### sa-token-integration-loveqq

LoveQQ 集成测试（HTTP Filter / Interceptor、Bean 注入、Redisson Dao 条件装配，真实请求链路）：

- sa-token-loveqq-boot-starter

### sa-token-integration-jfinal

JFinal HTTP 集成测试（真实 Undertow、`SaTokenActionHandler` 上下文、`SaAnnotationInterceptor` 注解鉴权；不起 Redis）：

- sa-token-jfinal-plugin

### sa-token-integration-jboot

JBoot HTTP 集成测试（真实 Undertow、`SaTokenContextForJboot` 上下文、`SaAnnotationInterceptor` 注解鉴权；不起 Redis / RPC）：

- sa-token-jboot-plugin

### sa-token-integration-sso

SSO 协议 HTTP 集成测试（Spring Boot 2，同一进程挂 Server + Client 两套路由和两套 `StpLogic`；真 302 / Location，模式一 / 二 / 三跳转和单点注销；不起 Redis）：

- sa-token-sso

### sa-token-integration-oauth2

OAuth2 协议 HTTP 集成测试（Spring Boot 2，真 302 / Location 和 token JSON；授权码 / 隐藏式 / 密码 / 凭证 / 刷新 / 回收 / 确认授权；不起 Redis）：

- sa-token-oauth2

### sa-token-integration-dubbo

Dubbo 2.x Filter 集成测试（Spring Boot 2，同一进程 Consumer HTTP + Provider，本机 `dubbo://`，`scope=remote` 避免 injvm 串 ThreadLocal；会话下传 / 回传、Same-Token；不起 Nacos / Redis）：

- sa-token-dubbo

### sa-token-integration-dubbo3

Dubbo3 Filter 集成测试（Spring Boot 2，同一进程 Consumer HTTP + Provider，本机 `dubbo://`，`scope=remote` 避免 injvm 串 ThreadLocal；会话下传 / 回传、Same-Token；不起 Nacos / Redis）：

- sa-token-dubbo3

### sa-token-integration-grpc

gRPC 拦截器集成测试（Spring Boot 2，同一进程 Consumer HTTP + Provider，本机 gRPC 端口；Same-Token；不起 Nacos / Redis）。会话下传 / 回传、未登录匿名，对齐 dubbo 那 4 条：

- sa-token-grpc

### sa-token-coverage

覆盖率聚合（非功能测试），汇总全仓库生产模块 JaCoCo 报告。

## 运行

```bash
mvn test -pl sa-token-testing/sa-token-integration-boot2 -am
mvn test -pl sa-token-testing/sa-token-integration-beaninject-boot2 -am
mvn test -pl sa-token-testing/sa-token-integration-boot3 -am
mvn test -pl sa-token-testing/sa-token-integration-boot4 -am
mvn test -pl sa-token-testing/sa-token-integration-reactor-boot2 -am
mvn test -pl sa-token-testing/sa-token-integration-reactor-boot3 -am
mvn test -pl sa-token-testing/sa-token-integration-reactor-boot4 -am
mvn test -pl sa-token-testing/sa-token-integration-solon -am
mvn test -pl sa-token-testing/sa-token-integration-beaninject-solon -am
mvn test -pl sa-token-testing/sa-token-integration-loveqq -am
mvn test -pl sa-token-testing/sa-token-integration-jfinal -am
mvn test -pl sa-token-testing/sa-token-integration-jboot -am
mvn test -pl sa-token-testing/sa-token-integration-sso -am
mvn test -pl sa-token-testing/sa-token-integration-oauth2 -am
mvn test -pl sa-token-testing/sa-token-integration-dubbo -am
mvn test -pl sa-token-testing/sa-token-integration-dubbo3 -am
mvn test -pl sa-token-testing/sa-token-integration-grpc -am
```

根目录全量：`mvn test.bat`

---

### Overview (`sa-token-demo/sa-token-demo-bug-reproduce/README.md`)

# sa-token-demo-bug-reproduce

临时复现 Issue / Bug 的沙盒（用例随 Issue 更换，平时保持空壳）。

## 怎么跑

```bash
mvn -DskipTests spring-boot:run
```

健康检查：http://localhost:8092/ok

---

### Overview (`sa-token-demo/sa-token-demo-json-typing-security-for-solon/README.md`)

# sa-token-demo-json-typing-security-for-solon

**长期保留**的 Snack4 AutoType 反序列化白名单回归 Demo（Solon + Snack4 + Redisx）。

与 `sa-token-demo-json-typing-security`（Spring Boot + Jackson）对应，便于分别验证不同 JSON 插件的白名单行为。

## 验证内容

模拟 Redis 中被写入带 `@type` 的恶意 JSON，经 `SaTokenDao.getObject()` 反序列化时：

- **期望**：`SaJsonStrategy` 白名单拦截，抛出 `SaJsonConvertException`（消息含 `无法反序列化的类型：…`）
- **失败**：未授权类型被实例化（PoC gadget 会尝试弹出计算器）

关联实现：`SaJsonStrategy`、`SaJsonTemplateForSnack4`

## 前提

- 本地 Redis 已启动（默认 `127.0.0.1:6379`，database `1`）
- 开发本仓库时，请先根目录 `mvn install`，并将 `pom.xml` 中 `sa-token.version` 改为当前开发版本

## 怎么跑

IDEA 运行 `JsonTypingSecurityForSolonApp`，或：

```bash
mvn -DskipTests compile exec:java -Dexec.mainClass="com.pj.JsonTypingSecurityForSolonApp"
```

启动后会自动执行一次 PoC。也可 HTTP 重复触发：

```bash
curl http://127.0.0.1:8094/ok
curl http://127.0.0.1:8094/poc/snack4-autotype
```

**警告：** 仅用于本地验证，勿在生产或公网环境使用。

---

### Overview (`sa-token-demo/sa-token-demo-json-typing-security/README.md`)

# sa-token-demo-json-typing-security

**长期保留**的 Jackson DefaultTyping 反序列化白名单回归 Demo。  
与 `sa-token-demo-bug-reproduce`（临时 Issue 沙盒）分离，避免新漏洞复现时覆盖本用例。

## 验证内容

模拟 Redis 中被写入带 `@class` 的恶意 JSON，经 `SaTokenDao.getObject()` 反序列化时：

- **期望**：`SaJsonStrategy` 白名单拦截，抛出 `SaJsonConvertException`（消息含 `无法反序列化的类型：…`）
- **失败**：未授权类型被实例化（PoC gadget 会尝试弹出计算器）

关联实现：`SaJsonStrategy`、`SaJsonTemplateForJackson`

## 前提

- 本地 Redis 已启动（默认 `127.0.0.1:6379`，database `1`）
- 开发本仓库时，请先根目录 `mvn install`，并将 `pom.xml` 中 `sa-token.version` 改为当前开发版本

## 怎么跑

IDEA 运行 `JsonTypingSecurityApplication`，或：

```bash
mvn -DskipTests spring-boot:run
```

启动后会自动执行一次 PoC。也可 HTTP 重复触发：

```bash
curl http://127.0.0.1:8093/ok
curl http://127.0.0.1:8093/poc/jackson-default-typing
```

**警告：** 仅用于本地验证，勿在生产或公网环境使用。

---

### Overview (`sa-token-demo/sa-token-demo-ssm/README.md`)

## SSM 架构集成 Sa-Token 示例

说是SSM，其实没有M，仅仅是给使用 SpringMVC 非 SpringBoot 的项目提供一个简单的 Sa-Token 集成示例。

### 集成要点（v1.42.0+）

1. 在 `web.xml` 注册 `SaTokenContextFilter`（本示例已配置，逻辑同官方 `SaTokenContextFilterForServlet`）
2. 在 `SaTokenBeanInjection` 中手动注册 `SaStrategy`（路由匹配、SaRequest/SaResponse/SaStorage 创建策略）
3. 不再注入 `SaTokenContextForSpring`，上下文由 Filter + ThreadLocal 方案自动管理

直接运行项目即可，里面注释挺全的，也不必做过多说明了
（其实就是我懒，光搭建起来这个架子就累瘫了，各种版本兼容问题报起错来大汗淋漓，推荐新项目能上 SpringBoot 就赶紧上吧，千万别在SSM上浪费生命）。

推荐 jdk8 + tomcat8。

### 常见问题

- **Filter 启动失败（One or more Filters failed to start）**：① `javax.servlet-api` 必须 `provided`，不能打进 WAR；② 不要开 `async-supported`（SpringBoot 会自动给 `DispatcherServlet` 开异步，纯 SSM 不会）；③ `web.xml` 注册本工程内的 `com.pj.satoken.SaTokenContextFilter`，不要直接引用 starter 里的 Filter 类。
- **NoSuchMethodError: Expiration.keepTtl()**：`spring-data-redis` 版本过低（须 2.7.x，与 `sa-token-redis-jackson` 对齐），勿手动锁死 2.3.x。

---

### Overview (`sa-token-doc-new/docs/readme.md`)

---
title: Sa-Token 框架介绍
description: Sa-Token 是轻量级 Java 权限认证框架，开源免费，覆盖登录认证、权限认证、SSO 单点登录、OAuth2.0、分布式 Session 与微服务网关鉴权；无需写配置文件，一行 StpUtil.login、checkLogin、kickout 与 @SaCheckPermission 即可接入。
keywords: Sa-Token,satoken,sa-token文档,Java权限认证,Spring Boot,框架介绍
---

<p align="center">
	<img alt="logo" src="https://sa-token.com/logo.png" width="150" height="150">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.46.0</h1>
<h5 align="center">✨ 开源、免费、一站式 java 权限认证框架，让鉴权变得简单、优雅！</h5>
<p align="center" class="badge-box">
	<a href="https://gitee.com/dromara/sa-token/stargazers"><img src="https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp"></a>
	<a href="https://gitee.com/dromara/sa-token/members"><img src="https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp"></a>
	<a href="https://atomgit.com/dromara/sa-token/stargazers"><img src="https://atomgit.com/dromara/Sa-Token/star/badge.svg"></a>
	<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>
	<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>
	
	
	<a><img src="https://img.shields.io/badge/JDK-8~25-green.svg?style=flat-square" alt="JDK 8~25"></a>
	<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>
</p>

---

## 📝 前言：️️
为了保证新同学不迷路，请允许我唠叨一下：无论您从何处看到本篇文章，最新开发文档永远在：[https://sa-token.com](https://sa-token.com)，
建议收藏在浏览器书签 (快捷键 **ctrl + D**)，如果您已经身处本网站下，则请忽略此条说明。

回望 2020 年初，我为 Sa-Token 提交第一行代码之际，**彼时市面上 Java 缺少的不仅是一个简洁好用的鉴权框架，更是一整套清晰、自洽的权限架构设计思想**。

因此，这几年间我们将大量时间倾注在 Sa-Token 的文档编写，几乎每一章节、每一句话、每一个字都经过反复修改、精细打磨，以求做到最清晰、干练、易懂的表述。**用心阅读文档，你学习到的将不止是 Sa-Token 框架本身，更是绝大多数场景下权限认证设计的最佳实践。**


## 🛠️ Sa-Token 介绍

**Sa-Token** 是一个轻量级 Java 权限认证框架，主要解决：**登录认证**、**权限认证**、**单点登录**、**OAuth2.0**、**分布式Session会话**、**微服务网关鉴权**
等一系列权限相关问题。



<object class="sa-token-jss-img" data="/big-file/index/intro/sa-token-jss--tran--onclick.svg"></object>

Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分，以登录认证为例，你只需要：

``` java
// 会话登录，参数填登录人的账号id 
StpUtil.login(10001);
```

无需实现任何接口，无需创建任何配置文件，只需要这一句静态代码的调用，便可以完成会话登录认证。

如果一个接口需要登录后才能访问，我们只需调用以下代码：

``` java
// 校验当前客户端是否已经登录，如果未登录则抛出 `NotLoginException` 异常
StpUtil.checkLogin();
```

在 Sa-Token 中，大多数功能都可以一行代码解决：

踢人下线：

``` java
// 将账号id为 10077 的会话踢下线 
StpUtil.kickout(10077);
```

权限认证：

``` java
// 注解鉴权：只有具备 `user:add` 权限的会话才可以进入方法
@SaCheckPermission("user:add")
public String insert(SysUser user) {
    // ... 
    return "用户增加";
}
```

路由拦截鉴权：

``` java
// 根据路由划分模块，不同模块不同鉴权 
registry.addInterceptor(new SaInterceptor(handler -> {
	SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
	SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
	SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
	SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
	SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
	// 更多模块... 
})).addPathPatterns("/**");
```

当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后，你就会明白，相对于这些传统老牌框架，Sa-Token 的 API 设计是多么的简单、优雅！


## 🎉 Sa-Token 功能一览

Sa-Token 目前主要五大功能模块：登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。

- **登录认证** —— 单端登录、多端登录、同端互斥登录、七天内免登录。
- **权限认证** —— 权限认证、角色认证、会话二级认证。
- **踢人下线** —— 根据账号id踢人下线、根据Token值踢人下线。
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离。
- **路由拦截式鉴权** —— 根据路由拦截鉴权，可适配 restful 模式。
- **Session会话** —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。
- **持久层扩展** —— 可集成 Redis，重启数据不丢失。
- **前后台分离** —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。
- **Token风格定制** —— 内置六种 Token 风格，还可：自定义 Token 生成策略。
- **记住我模式** —— 适配 [记住我] 模式，重启浏览器免验证。
- **二级认证** —— 在已登录的基础上再次认证，保证安全性。 
- **模拟他人账号** —— 实时操作任意用户状态数据。
- **临时身份切换** —— 将会话身份临时切换为其它账号。
- **同端互斥登录** —— 像QQ一样手机电脑同时在线，但是两个手机上互斥登录。
- **账号封禁** —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。
- **密码加密** —— 提供基础加密算法，可快速 MD5、SHA1、SHA256、AES 加密。
- **会话查询** —— 提供方便灵活的会话查询接口。
- **Http Basic认证** —— 一行代码接入 Http Basic、Digest 认证。
- **全局侦听器** —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
- **全局过滤器** —— 方便的处理跨域，全局设置安全响应头等操作。
- **多账号体系认证** —— 一个系统多套账号分开鉴权（比如商城的 User 表和 Admin 表）
- **单点登录** —— 内置三种单点登录模式：同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
- **单点注销** —— 任意子系统内发起注销，即可全端下线。
- **OAuth2.0认证** —— 轻松搭建 OAuth2.0 服务，支持openid模式 。
- **分布式会话** —— 提供共享数据中心分布式会话方案。
- **微服务网关鉴权** —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。
- **RPC调用鉴权** —— 网关转发鉴权，RPC调用鉴权，让服务调用不再裸奔
- **临时Token认证** —— 解决短时间的 Token 授权问题。
- **独立Redis** —— 将权限缓存与业务缓存分离。
- **Quick快速登录认证** —— 为项目零代码注入一个登录页面。
- **标签方言** —— 提供 Thymeleaf 标签方言集成包，提供 beetl 集成示例。
- **jwt集成** —— 提供三种模式的 jwt 集成方案，提供 token 扩展参数能力。
- **RPC调用状态传递** —— 提供 dubbo、grpc 等集成包，在RPC调用时登录状态不丢失。
- **参数签名** —— 提供跨系统API调用签名校验模块，防参数篡改，防请求重放。
- **自动续签** —— 提供两种Token过期策略，灵活搭配使用，还可自动续签。
- **开箱即用** —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包，开箱即用。
- **最新技术栈** —— 适配最新技术栈：支持 SpringBoot 3.x，jdk 17。

功能结构图：

<img class="s-w" src="/big-file/index/intro/sa-token-js4.png" alt="Sa-Token 框架功能架构示意图" />


## 📖❓ 疑问解答

**1、Sa-Token 功能全不全？** 

七年磨一剑：五大核心模块(登录、鉴权、SSO、OAuth2、微服务) + 众多实用插件 (短 token、jwt 集成、API 参数签名、API Key 秘钥授权...) 我们提供的不只是权限认证，我们提供的是一站式解决方案。


**2、Sa-Token 好不好学？** 

中文文档 + 中文代码注释 + 中文交流社区 + 大量实战案例博客 + 多个视频教程 + 大量优秀开源项目集成案例。


**3、Sa-Token 用的人多不多？** 

截止统计日 (2026-1-25) 起，Sa-Token 在：

- Gitee 关注量达到 48627 Star，位列平台所有推荐项目排行榜第一名。
- GitHub 关注量达到 18523 Star，是主要竞争框架 Spring Security 的 1.97 倍，Apache Shiro 的 4.19 倍。
- 25+ 微信粉丝群 (500人)，8+ QQ粉丝群 (1000人 or 2000人) ，在线文档访问量月PV 20万+。

这是众多开发者用脚投票的数据，相信这些数据比任何言语都能证明 Sa-Token 的热度。


**4、Sa-Token 有哪些权威认证？** 

曾获荣誉包括但不限于：Gitee GVP 最有价值开源项目、GitCode G-Star 优质开源项目、OSCHINA 2021 人气指数 TOP 30 开源项目、OSCHINA 2022 年度最火热中国开源项目社区之一、开放原子基金会2023快速成长开源项目、 Dromara 组织顶尖项目（之一）、可信开源社区共同体预备成员、所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛（开源）》二等奖。 Gitee High Star 计划项目(5000+star)。Gitee 2025年度开源项目 Web应用开发 Top 2。


**5、Sa-Token 收费吗？** 

Sa-Token 采用 Apache-2.0 开源协议，承诺框架本身与在线文档永久免费开放。当然如果您有心赞助 Sa-Token，我们也不回避：[赞助链接](https://sa-token.com/more/sa-token-donate.html)。
我们将定期同步赞助者名单到在线文档展示。（您需要注意的一点是：该赞助仅为友情赞助，不提供任何商业交换）


**6、Sa-Token 是封装的 SpringSecurity 吗？是套壳 ApacheShiro 吗？** 

不是。Sa-Token 不是一个后台模板，也不是针对 xx 框架的二次封装套壳，而是从 0 开始的纯血自研框架，核心包零依赖，完全自主可控的架构内核 + 众多主流框架的集成适配。
					


## 🌍 其它语言版本

Sa-Token 社区成员贡献了多语言实现版本：

- Rust 版本：[https://github.com/sa-tokens/sa-token-rust](https://github.com/sa-tokens/sa-token-rust)
- Go 版本：[https://github.com/sa-tokens/sa-token-go](https://github.com/sa-tokens/sa-token-go)
- NodeJS 版本：[https://github.com/xiaoLangtou/xlt-token](https://github.com/xiaoLangtou/xlt-token)
- PHP 版本：[https://gitee.com/jinan-jimeng-network_0/real-token](https://gitee.com/jinan-jimeng-network_0/real-token)

我们诚邀对上述语言较为熟练的开发者，一起建设相关版本。🤝


## 📈 开源仓库 Star 趋势

<p class="un-dec-a-pre"></p>

[](https://starchart.cc/dromara/sa-token)

如果 Sa-Token 帮助到了您，希望您可以为其点上一个 `star`：
[码云](https://gitee.com/dromara/sa-token)、
[AtomGit](https://atomgit.com/dromara/sa-token)、
[GitHub](https://github.com/dromara/sa-token)


## 🚀 使用 Sa-Token 的开源项目 
参考：<a href="/cases.html" target="_blank" rel="noopener">Sa-Token 优秀开源集成案例</a>



## 📚 示例大全

**我们为框架几乎所有技术点均单独制作了对应的集成示例，此压缩包共计 60+ Demo**：涵盖 Sa-Token 登录认证、权限认证、SSO 单点登录、OAuth2 统一认证、微服务鉴权、API Key 认证、JWT集成、跨系统调用参数签名校验 等鉴权认证的方方面面。

下载地址：[Sa-Token 集成示例大全下载](/more/download-demos) 

<img class="s-w" src="/big-file/contact/show/sa-token-demos-pre.png" alt="Sa-Token 集成示例大全下载页预览" />



## 💬 交流群
加入 Sa-Token 框架 QQ、微信讨论群：[点击加入](/more/join-group.md)

---

### Overview (`sa-token-plugin/sa-token-redisson/README.md`)

## sa-token-redisson

此扩展，不与生态绑定。可用于不同的生态（SpringBoot，Solon，JFinal等）。

### 1、例 solon 集成

添加关键依赖

```xml
<dependencies>
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-redisson</artifactId>
        <version>${sa-token.version}</version>
    </dependency>
    <dependency>
        <groupId>org.noear</groupId>
        <artifactId>redisson-solon-plugin</artifactId>
        <version>${solon.version}</version>
    </dependency>
</dependencies>
```

添加 dao 配置

```yaml
sa-token-dao:
    config: |
        singleServerConfig:
          password: "123456"
          address: "redis://localhost:6379"
          database: 0
```

开始组装

```java
@Configuration
public class SaTokenConfigure {
	/**
	 * 构造 RedissonClient
	 * */
	@Bean
	public RedissonClient saTokenDaoInit(@Inject("${sa-token-dao}") RedissonSupplier supplier) {
		return supplier.get();
	}

	/**
	 * 构建  SaTokenDao
	 * */
	@Bean
	public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {
		return new SaTokenDaoForRedisson(redissonClient);
	}
}
```


### 2、例 springboot 集成


添加关键依赖

```xml
<dependencies>
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-redisson</artifactId>
        <version>${sa-token.version}</version>
    </dependency>
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>${redisson.version}</version>
    </dependency>
</dependencies>
```

添加 dao 配置

```yaml
spring.redis:
  redisson:
    file: classpath:redisson.yml
```

开始组装

```java
@Configuration
public class SaTokenConfigure {
	/**
	 * 构建  SaTokenDao
	 * */
	@Bean
	public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {
		return new SaTokenDaoForRedisson(redissonClient);
	}
}
```


### 3、Codec

`SaTokenDaoForRedisson` 默认使用 `StringCodec`，与业务 `RedissonClient` 全局 codec 隔离：

```java
// 默认 StringCodec
return new SaTokenDaoForRedisson(redissonClient);

// 指定 codec（例如兼容升级前的 Kryo5 缓存）
return new SaTokenDaoForRedisson(redissonClient, new Kryo5Codec());
```

**升级注意：** 此前跟随 Redisson 全局 codec（未自定义时为 `Kryo5Codec`）。升级后旧缓存无法用 `StringCodec` 读取。请清空 Redis 中的 Sa-Token 数据，或构造时传入 `new Kryo5Codec()`。

---

### Overview (`sa-token-plugin/sa-token-redisx/README.md`)

sa-token-redisx 是中立的扩展。可任何应用开发框架下使用（springboot, solon, jfinal 等..）

### 使用示例

#### 1.配置

```yaml
sa-token: #名字可以随意取
  redis:
    server: "localhost:6379"
    password: 123456
    db: 1
#   serializer: "org.noear.redisx.utils.SerializerJson" #指定自定义序列化实现（默认为 SerializerDefault）
```

#### 2.代码

**注入风格**

```java
@Configuration
public class Config {
    @Bean
    public SaTokenDao saTokenDaoInit(@Inject("${sa-token.redis}") SaTokenDaoOfRedis saTokenDao) {
        return saTokenDao;
    }
}
```

**手动风格**

```java
SaTokenDaoOfRedis saTokenDao = new SaTokenDaoOfRedis(props);
SaManager.setSaTokenDao(saTokenDao);
```

---

### Overview (`sa-token-demo/sa-token-demo-remember-me/page_project/README.md`)

# Vue 3 + Vite

[Node下载地址](https://nodejs.org/zh-cn/)

安装最新版本Node环境, 然后执行如下命令开启开发服务:
```
npm install
npm run dev
```

[cookie/sessionstorage/localstorage三者的区别](https://blog.csdn.net/weixin_45541388/article/details/125367823)

---

### Overview (`sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2/README.md`)

# sa-token-demo-sso-client-vue2
Sa-Token SSO-Client 应用端（前后端分离版-Vue2）

在线文档：[https://sa-token.com/](https://sa-token.com/)

## 运行
先安装依赖
``` bat
npm install --registry=https://registry.npm.taobao.org
```

运行
``` bat
npm run serve
```

打包
``` bat
npm run build
```

---

### Overview (`sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3/README.md`)

# sa-token-demo-sso-client-vue3
Sa-Token SSO-Client 应用端（前后端分离版-Vue3）

在线文档：[https://sa-token.com/](https://sa-token.com/)


## 运行
先安装依赖
``` bat
npm install --registry=https://registry.npm.taobao.org
```

运行
``` bat
npm run dev
```

打包
``` bat
npm run build
```

---

### Overview (`sa-token-doc-new/docs/oauth2/readme.md`)

---
title: "Sa-Token OAuth2.0简述"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,OAuth2.0简述,OAuth2.0,OAuth2"

description: "Sa-Token OAuth2.0 模块：比 SSO 多一层应用授权范围，适合开放平台与第三方接入。覆盖授权码、隐藏式、密码式、客户端凭证四种模式，并拆平台后台、开放平台申请 client、认证中心发 token、资源中心按 scope 出数的对接流程。用户确认授权后用 code 换 access_token。"
---

# Sa-Token-OAuth2.0 模块 

--- 

### 什么是 OAuth2.0 ？解决什么问题？

OAuth2.0 与 SSO 相比，增加了对应用授权范围的控制，减弱了应用之间数据同步的能力。

有关 OAuth2.0 的设计思想网上教程较多，此处不再重复赘述，详细可参考博客：
[OAuth2.0 简单解释](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html)


如果你还不知道你的项目应该选择 SSO 还是 OAuth2.0，可以参考这篇：[技术选型：[ 单点登录 ] VS [ OAuth2.0 ]](/fun/sso-vs-oauth2)



### OAuth2.0 四种模式 

基于不同的使用场景，OAuth2.0设计了四种模式：

1. 授权码（Authorization Code）：OAuth2.0 标准授权步骤，Server 端向 Client 端下放 `Code` 码，Client 端再用 `Code` 码换取授权 `Access-Token`。
2. 隐藏式（Implicit）：无法使用授权码模式时的备用选择，Server 端使用 URL 重定向方式直接将 `Access-Token` 下放到 Client 端页面。
3. 密码式（Password）：Client 端直接拿着用户的账号密码换取授权 `Access-Token`。
4. 客户端凭证（Client Credentials）：Server 端针对 Client 级别的 Token，代表应用自身的资源授权。

<img src="/big-file/doc/oauth2-new/sa-oauth2-setup.png" alt="OAuth2.0 四种授权模式关系示意图" />

接下来我们将通过简单示例演示如何在 Sa-Token-OAuth2 中完成这四种模式的对接: [搭建OAuth2-Server](/oauth2/oauth2-server)

若需下载官方仓库内全套可运行 Demo 压缩包，请见：[Sa-Token 集成示例大全下载](/more/download-demos) 。


### OAuth2.0 第三方开放平台完整开发流程参考

1. oauth2-server 平台端 
	1. 搭建 oauth2-server 数据后台管理端，也称：后台管理。（后台人员对底层数据增删改查维护的地方）。
	2. 搭建 oauth2-server 数据前台申请端，也称：开放平台。（给第三方公司提供一个申请注册 client 的地方）
	3. 搭建 oauth2-server 授权端 以及其接口文档，也称：认证中心。（让第三方公司拿到 access_token）
	4. 搭建 oauth2-server 资源端 以及其接口文档，也称：资源中心。（让第三方公司通过 access_token 拿到对应的资源数据）
	5. 以上四端可以是一个项目，也可以是四个独立的项目，也可以是一个后端 + 多个前端的形式。

2. oauth2-client 第三方公司端
	1. 第三方公司登录 oauth-server 数据前台申请端，申请注册应用，拿到 `clientId`、`clientSecret` 等数据。
	2. 根据自己的业务选择对应的 scope 申请签约，等待平台端审核通过。
	3. 在自己系统通过 `clientId`、`clientSecret` 等参数对接 oauth2-server 授权端，拿到 `access_token`。
	4. 通过 `access_token` 调用 oauth2-server 资源端接口，拿到对应资源数据。

3. 用户端操作
	1. 打开第三方公司开发的网站或APP等程序。
	2. 一般有个“通过xx第三方登录”的按钮，点它。
	3. 跳转到了 oauth2-server 端的网站，在此网站用 oauth2-server 的账号开始登录。
	4. 登录完成，继续跳转到授权页，点击确认授权。
	5. 授权完成，oauth2-server 端生成一个 code 码，重定向回 oauth2-client 的网站，把 code 参数挂到对应的 url 上。
	6. oauth2-client 从 url 中读取 code 参数，提交到 oauth2-client 的后端。
	7. oauth2-client 后端拿着 `code`、`clientId`、`clientSecret` 等信息调用 oauth2-server 授权端 的接口，得到 `access_token`。
	8. 继续拿着 `access_token` 调用 oauth2-server 资源端获取此用户对应的数据。
	9. 一般最终目的拿到一个 openid 值，oauth2-client 根据 openid 进行登录。生成自己的会话 token ，返回到数据到前端。
	10. 前端拿到自己 oauth2-client 生成的会话 token ，完成登录。开始进行业务操作。

---

### Overview (`sa-token-doc-new/docs/sso/readme.md`)

---
title: "Sa-Token 单点登录简述"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,单点登录简述,单点登录,SSO"

description: "Sa-Token SSO 单点登录：一次登录打通商城、论坛等子系统。按前端是否同域、后端是否同 Redis 分三种模式——共享 Cookie、URL 重定向、HTTP 换会话。内置域名校验、Ticket 防劫持，登录后参数不丢原路返回，权限认证与 SSO 可同一框架完成。建议先跑通官方 demo 再接入。"
---

# Sa-Token-SSO 单点登录模块 

<p><a class="case-btn case-btn-video" href="https://www.bilibili.com/video/BV1NF1FBpEe6/" target="_blank">
	观看 SSO 模块视频讲解（B站：王清江唷）
</a></p>

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

--- 

### 什么是单点登录？解决什么问题？

举个场景，假设我们的系统被切割为N个部分：商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次，那么用户将会疯掉，
为了优化用户体验，我们急需一套机制将这N个系统的认证授权互通共享，让用户在一个系统登录之后，便可以畅通无阻的访问其它所有系统。 

单点登录——就是为了解决这个问题而生！

简而言之，单点登录可以做到： **`在多个互相信任的系统中，用户只需登录一次，就可以访问所有系统。`**


### 架构选型
Sa-Token-SSO 由简入难划分为三种模式，解决不同架构下的 SSO 接入问题：

| 系统架构					| 采用模式	| 简介					|  文档链接	|
| :--------					| :--------	| :--------				| :--------	|
| 前端同域 + 后端同 Redis		| 模式一		| 共享 Cookie 同步会话	| [文档](/sso/sso-type1)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso1-client)	|
| 前端不同域 + 后端同 Redis	| 模式二		| URL重定向传播会话 		| [文档](/sso/sso-type2)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client)	|
| 前端不同域 + 后端不同 Redis	| 模式三		| Http请求获取会话		| [文档](/sso/sso-type3)、[示例](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client)	|

集成示例还可下载全套 Demo 压缩包：[Sa-Token 集成示例大全下载](/more/download-demos) 。

1. 前端同域：就是指多个系统可以部署在同一个主域名之下，比如：`c1.domain.com`、`c2.domain.com`、`c3.domain.com`。
2. 后端同Redis：就是指多个系统可以连接同一个Redis。PS：这里并不需要把所有项目的数据都放在同一个Redis中，Sa-Token提供了 **`[权限缓存与业务缓存分离]`** 的解决方案，详情戳： <a href="/plugin/alone-redis.html" target="_blank">Alone独立Redis插件</a>。
3. 如果既无法做到前端同域，也无法做到后端同Redis，那么只能走模式三，Http请求获取会话（Sa-Token对SSO提供了完整的封装，你只需要按照示例从文档上复制几段代码便可以轻松集成）。

<img src="/big-file/doc/sso/sa-token-sso--white.png" alt="Sa-Token SSO 单点登录架构示意图">


### Sa-Token-SSO 特性
1. API 简单易用，文档介绍详细，且提供直接可用的集成示例。
2. 支持三种模式，不论是否跨域、是否共享Redis、是否前后端分离，都可以完美解决。
3. 安全性高：内置域名校验、Ticket校验、秘钥校验等，杜绝`Ticket劫持`、`Token窃取`等常见攻击手段（文档讲述攻击原理和防御手段）。
4. 不丢参数：笔者曾试验多个单点登录框架，均有参数丢失的情况，比如重定向之前是：`http://a.com?id=1&name=2`，登录成功之后就变成了：`http://a.com?id=1`，Sa-Token-SSO内有专门的算法保证了参数不丢失，登录成功之后原路返回页面。
5. 无缝集成：由于Sa-Token本身就是一个权限认证框架，因此你可以只用一个框架同时解决`权限认证` + `单点登录`问题，让你不再到处搜索：xxx单点登录与xxx权限认证如何整合……
6. 高可定制：Sa-Token-SSO模块对代码架构侵入性极低，结合Sa-Token本身的路由拦截特性，你可以非常轻松的定制化开发。


### 学习注意点
1. sa-token-sso 虽然是个单独的插件，但其本质仍是对 Sa-Token 本身各个功能的组合使用，所以先熟练掌握 Sa-Token 可有效降低 SSO 章节的学习压力。
2. 相比单体系统的会话管理，SSO 登录与注销的整体链路较长，出现 bug 时调试步骤也更复杂，因此建议先通过 demo 打通各个技术细节，再正式集成到项目中。
3. 文档对 跨Redis、跨域、前后端分离 等常见场景提供直接可用的示例，但真实项目往往是多种特殊场景交叉组合存在，每个项目各不相同。
所以文档无法依次列出所有技术点交叉组合的 demo 示例，文档会更注重解释清楚每一种特殊场景的特殊点所在，以及其解决原理，
所以推荐大家细心阅读相关段落，以便在真实项目中可以做到灵活组合、举一反三。

---

### Overview (`sa-token-doc-new/.vitepress/theme/reserved/README.md`)

# 备用插件（默认关闭）

从现网 Docsify 注释插件本地化改装而来，**上线默认全关**。

开关在同目录 `index.ts` 的 `reservedPlugins`。改 `true` 后重新 `npm run docs:build`。

| 开关 | 原文件 | 行为 |
|---|---|---|
| `star` | `static/is-star-plugin.js` | Gitee star 检查弹层 |
| `survey` | `static/is-fill-in-wj-plugin.js` | 问卷邀请 |
| `docLock` | `static/custom-docsify-plugins/doc-lock-plugin.js` | 章节锁 |
| `docLockByGzh` | `static/custom-docsify-plugins/doc-lock-by-gzh-plugin.js` | 公众号章节锁 |
| `progress` | `static/docsify-plugins/progress.update.js` | 顶部阅读进度条 |

脚本和样式都在本仓库 `public/static/`，不走 CDN。
路径匹配已改成 VitePress 的 `location.pathname`（`/sso/xxx.html`），不再用 Docsify `vm.route.path`。

---

### Sa Token Doc New Docs Arch Dir Intro (`sa-token-doc-new/docs/arch/dir-intro.md`)

---
title: "Sa-Token 仓库目录"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,仓库目录,框架设计"
description: "Sa-Token 仓库目录：core 内核、Spring Boot 2/3/4 依赖隔离；starter 整合 WebMvc、WebFlux、Solon；plugin 含 Redis、JWT、SSO、OAuth2；另有 60+ demo 与测试。文末介绍 Awesome-Sa-Token 及 Rust、Go 生态仓。"
---

# 仓库目录介绍

--- 

### 1、仓库根目录介绍：

``` js
── sa-token
	├── sa-token-core                         // [核心] Sa-Token 核心模块
	├── sa-token-dependencies                 // [依赖] Sa-Token 依赖版本信息
	├── sa-token-special-dependencies         // [依赖] Sa-Token 特殊依赖（SpringBoot2/3/4 版本隔离）
	├── sa-token-bom                          // [核心] Sa-Token bom 包
	├── sa-token-starter                      // [整合] Sa-Token 与其它框架整合
	├── sa-token-plugin                       // [插件] Sa-Token 插件合集
	├── sa-token-demo                         // [示例] Sa-Token 示例合集
	├── sa-token-test                         // [测试] Sa-Token 单元测试合集
	├── sa-token-doc                          // [文档] Sa-Token 开发文档 
	├── MEMO                                  // [备忘] 内部备忘录、开发记录
	├── pom.xml                               // [依赖] 顶级pom文件 
	├── LICENSE                               // 开源协议
	├── mvn clean.bat                         // 一键 mvn clean 核心包+所有示例包
	├── mvn test.bat                          // 一键单元测试 
	├── preview-doc.bat                       // 一键预览开发文档
	├── README.md                             // 仓库自述文件 
```


### 2、所有目录详细介绍：

```
/* Detailed source-code truncated for AI context efficiency. */
```



其它（[sa-tokens](https://gitee.com/sa-tokens) 组织下相关仓库）：

- [Awesome-Sa-Token](https://gitee.com/sa-tokens/awesome-sa-token)：集成 Sa-Token 的优秀开源案例收集。
- [sa-token-rust](https://gitee.com/sa-tokens/sa-token-rust)：Sa-Token 的 Rust 版本，轻量级 Rust 权限认证框架。
- [sa-token-go](https://gitee.com/sa-tokens/sa-token-go)：Sa-Token 的 Go 版本，轻量级 Go 权限认证框架。
- [Sa-Token-Study](https://gitee.com/sa-tokens/Sa-Token-Study)：Sa-Token 涉及技术点学习笔记与实战。
- [Sa-Token-Login-Demos](https://gitee.com/sa-tokens/Sa-Token-Login-Demos)：各种登录方式示例集合，一站式学习 Sa-Token 登录认证。
- [sa-token-doc-big-file](https://gitee.com/sa-tokens/sa-token-doc-big-file)：sa-token-doc 文档中的图片资源文件。
- [sa-token-three-plugin](https://gitee.com/sa-tokens/sa-token-three-plugin)：Sa-Token 第三方插件合集。
- [sa-token-demo-cross](https://gitee.com/sa-tokens/sa-token-demo-cross)：Sa-Token 处理跨域场景示例。
- [auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test)：Java 权限认证框架功能 测试 / 对比 / 迁移。

---

### Sa Token Doc New Docs Micro Import Intro (`sa-token-doc-new/docs/micro/import-intro.md`)

---
title: "Sa-Token 依赖引入说明"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,依赖引入说明,微服务,微服务鉴权"
description: "Sa-Token 微服务依赖引入：网关与子服务分开引 Starter，勿放顶级父 pom。Servlet/Zuul 用 sa-token-spring-boot-starter，Gateway/ShenYu 用 reactor-starter 并注册全局过滤器，二者不可同项目共存，另需 Redis 同步。"
---


# 微服务中使用Sa-Token 依赖引入说明 

--- 

虽然在 [开始] 章节已经说明了依赖引入规则，但是交流群里不少小伙伴提出bug解决到最后发现都是因为依赖引入错误导致的，此处再次重点强调一下：

::: tip
**在微服务架构中使用Sa-Token时，网关和内部服务要分开引入Sa-Token依赖（不要直接在顶级父pom中引入Sa-Token）**
:::

总体来讲，我们需要关注的依赖就是两个：`sa-token-spring-boot-starter` 和 `sa-token-reactor-spring-boot-starter`：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-boot-starter</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。

== Gradle 方式

``` gradle
// Sa-Token 权限认证，在线文档：https://sa-token.com
implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。

:::


:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。

== Gradle 方式

``` gradle
// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.com
implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。

:::



至于怎么分辨我们需要引入哪个呢？这个要看你使用的基础框架：

对于内部基础服务来讲，我们一般都是使用SpringBoot默认的web模块：SpringMVC，
因为这个SpringMVC是基于Servlet模型的，在这里我们需要引入的是`sa-token-spring-boot-starter`

对于网关服务，大体来讲分为两种：
- 一种是基于Servlet模型的，如：Zuul，我们需要引入的是：`sa-token-spring-boot-starter`，详细戳：[在SpringBoot环境集成](/start/example)；理论上`Zuul`并不支持`Spring Boot3`
- 一种是基于Reactor模型的，如：SpringCloud Gateway、ShenYu 等等，我们需要引入的是：`sa-token-reactor-spring-boot-starter`，**并且注册全局过滤器！**，详细戳：[在WebFlux环境集成](/start/webflux-example)

注：切不可直接在一个项目里同时引入这两个依赖，否则会造成项目无法启动

另外，我们需要引入Redis集成包，因为我们的网关和子服务主要通过Redis来同步数据 

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 整合 RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'
implementation 'org.apache.commons:commons-pool2'
```

:::


详细参考：[集成 Redis](/up/integ-redis)

---

### Sa Token Doc New Docs More Noun Intro (`sa-token-doc-new/docs/more/noun-intro.md`)

---
title: "Sa-Token 框架名词解释"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,框架名词解释,附录"
description: "Sa-Token 名词解释：区分会话 Token、临时 Token、OAuth2 令牌与 Same-Token；对照 timeout 与 active-timeout、三类 Session、loginId 与 loginType；并梳理单地、多地、同端互斥登录，以及代码、注解、路由三种鉴权方式，避免概念偏差。"
---

# Sa-Token 名词解释 

Sa-Token 无意发明任何晦涩概念提升逼格，但在处理 issue 、Q群解答时还是发现不少同学因为一些基本概念理解偏差导致代码出错，
所以整理本篇针对一些比较容易混淆的地方加以解释说明。

也希望各位同学在提交 issue、Q群提问之前充分阅读本篇文章，保证不要因为基本概念理解偏差，增加不必要的沟通成本。


--- 

#### 几种 Token
- token：指通过 `StpUtil.login()` 登录产生的身份令牌，用来维护用户登录状态，也称：satoken、会话Token。 
- temp-token：指通过 `SaTempUtil.createToken()` 临时验证模块产生的Token，也称：临时Token。
- Access-Token：在 OAuth2 模块产生的身份令牌，也称：访问令牌、资源令牌。
- Refresh-Token：在 OAuth2 模块产生的刷新令牌，也称：刷新令牌。
- Same-Token：在 SaSameUtil 模块生成的Token令牌，用于提供子服务外网隔离功能。


#### 两种过期时间：
- timeout：会话 Token 的长久有效期。
- active-timeout：会话 Token 的最低活跃频率。

两者的差别详见：[Token有效期详解](/fun/token-timeout)


#### 三种Session：
- Account-Session：框架为每个账号分配的 Session 对象，也称：账号Session。 
- Token-Session：框架为每个 Token 分配的 Session 对象，也称：令牌Session。 
- Custom-Session：以一个特定的值作为SessionId，来分配的 Session 对象，也称：自定义Session。

三者差别详见：[Session模型详解](/fun/session-model)


#### 账号标识：
- loginId：账号id，用来区分不同账号，通过 `StpUtil.login(id)` 来指定。
- device：登录设备类型，例如：`PC`、`APP`，通过 `StpUtil.login(id, device)` 来指定。
- loginType：账号类型，用来区分不同体系的账号，如同一系统的 `User账号` 和 `Admin账号`，详见：[多账号认证](/up/many-account) 


#### 几种登录策略：
- 单地登录：指同一时间只能在一个地方登录，新登录会挤掉旧登录，也可以叫：单端登录。
- 多地登录：指同一时间可以在不同地方登录，新登录会和旧登录共存，也可以叫：多端登录。
- 同端互斥登录：在同一类型设备上只允许单地点登录，在不同类型设备上允许同时在线，参考腾讯QQ的登录模式：手机和电脑可以同时在线，但不能两个手机同时在线。
- 限量登录：限定账号登录设备总数量，低于此数量时可以正常登录，高于此数量后每次登录自动清退一个之前的登录。
- 记住我模式：指在一个设备终端登录成功，该设备重启之后依然保持登录状态。 
- 单点登录：在进入多个系统时，只需要登录一次即可。解决用户在不同系统间频繁登录的问题。
- 同端多登录：指在一个终端可以同时登录多个账号。


#### 几种注销策略：
- 单端注销：只在调用登录的一端注销。
- 全端注销：一端注销，全端下线。
- 同端注销：发起注销后，同类型设备端一起下线，不同设备类型不受影响。
- 单点注销：与单点登录对应，一个系统注销，所有系统一起下线。

<p><a class="case-btn case-btn-video" href="https://www.bilibili.com/video/BV1XrzABbEa1/" target="_blank">
	视频讲解：如何设计出最优的登录会话策略？单端登录、强制下线、多端互踢、记住我登录
</a></p>


#### 几种鉴权方式：
- 代码鉴权：在代码里直接调用 `StpUtil.checkXxx` 相关 API 进行鉴权。
- 注解鉴权：在方法或类上添加 `@SaCheckXxx` 注解进行鉴权。
- 路由拦截鉴权：在全局过滤器或拦截里通过：`SaRouter.match()` 拦截路由进行鉴权。

---

### Sa Token Doc New Docs Api Sa Session (`sa-token-doc-new/docs/api/sa-session.md`)

---
title: "Sa-Token SaSession-会话对象"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,SaSession-会话对象,API手册"
description: "SaSession 会话对象 API：覆盖 Account-Session 与 Token-Session 的 id、创建时间、loginType、loginId 读写；SaTerminalInfo 终端列表增删、按设备类型筛选与信任设备判断；以及 get/set 类型转换、dataMap、超时续期与注销等缓存用法。"
---

# SaSession-会话对象

SaSession-会话对象，专业数据缓存组件。

--- 

### 1、常量 
``` java
SaSession.USER= "USER";   // 在 Session 上存储用户对象时建议使用的key 
SaSession.ROLE_LIST = "ROLE_LIST";   // 在 Session 上存储角色时建议使用的key 
SaSession.PERMISSION_LIST = "PERMISSION_LIST";   // 在 Session 上存储权限时建议使用的key 
```


### 2、构建相关 
``` java
session.getId();   // 获取此 Session 的 id 
session.setId(id);   // 写入此 Session 的 id
session.getCreateTime();   // 返回当前会话创建时间（时间戳）
session.setCreateTime(createTime);   // 写入此 Session 的创建时间（时间戳）
session.getType();   // 获取此 Session 的类型
session.setType(type);   // 写入此 Session 的类型
session.getLoginType();   // 获取此 Session 的账号类型
session.setLoginType(loginType);   // 写入此 Session 的账号类型
session.getLoginId();   // 获取此 Session 的账号 id
session.setLoginId(loginId);   // 写入此 Session 的账号 id
session.getToken();   // 获取此 Session 的 token 值
session.setToken(token);   // 写入此 Session 的 token 值
```


### 3、SaTerminalInfo 相关 
``` java
session.setTerminalList(terminalList);   // 写入登录终端信息列表
session.getTerminalList();   // 获取登录终端信息列表
session.terminalListCopy();   // 获取 登录终端信息列表 (拷贝副本)
session.getTerminalListByDeviceType(deviceType);   // 获取 登录终端信息列表 (拷贝副本)，根据 deviceType 筛选
session.getTokenValueListByDeviceType(deviceType);   // 获取指定设备类型的 token 值列表
session.getTerminal(tokenValue);   // 查找一个终端信息，根据 tokenValue
session.addTerminal(terminal);   // 添加一个终端信息
session.removeTerminal(tokenValue);   // 移除一个终端信息
session.forEachTerminalList((session, terminal) -> {});   // 遍历登录终端信息列表
session.getHistoryTerminalCount();   // 获取历史终端数量
session.setHistoryTerminalCount(count);   // 设置历史终端数量
session.isTrustDeviceId("xxxxxxxxxxxxxxxxxxxxxxxx");   // 判断指定设备 id 是否为可信任设备
```


### 4、一些操作
``` java
session.update();   // 更新Session（从持久库更新刷新一下）
session.logout();   // 注销Session (从持久库删除)
session.logoutByTerminalCountToZero();   // 当 Session 上的 SaTerminalInfo 数量为零时，注销会话 
session.timeout();   // 获取此Session的剩余存活时间 (单位: 秒)
session.updateTimeout(timeout);   // 修改此Session的剩余存活时间
session.updateMinTimeout(minTimeout);   // 修改此Session的最小剩余存活时间 (只有在 Session 的过期时间低于指定的 minTimeout 时才会进行修改)
session.updateMaxTimeout(maxTimeout);   // 修改此Session的最大剩余存活时间 (只有在 Session 的过期时间高于指定的 maxTimeout 时才会进行修改)
```


### 5、存取值 
``` java
session.get(key);   // 取值
session.get(key, defaultValue);   // 取值 (指定默认值)
session.get(key, () -> {});   // 取值 (如果值为 null，则执行 fun 函数获取值，并把函数返回值写入缓存) 
session.getString(key);   // 取值 (转String类型)
session.getInt(key);   // 取值 (转int类型)
session.getLong(key);   // 取值 (转long类型)
session.getDouble(key);   // 取值 (转double类型)
session.getFloat(key);   // 取值 (转float类型)
session.getModel(key, clazz);   // 取值 (指定转换类型)
session.getModel(key, clazz, defaultValue);   // 取值 (指定转换类型, 并指定值为Null时返回的默认值)
session.has(key);   // 是否含有某个key
session.set(key, value);   // 写值
session.setByNull(key, value);   // 写值 (只有在此 key 原本无值的情况下才会写入)
session.delete(key);   // 删值 
session.keys();   // 返回当前Session的所有key 
session.clear();   // 清空所有值 
session.getDataMap();   // 获取数据挂载集合（如果更新map里的值，请调用session.update()方法避免产生脏数据 ） 
session.setDataMap(dataMap);   // 写入数据挂载集合
session.refreshDataMap(dataMap);   // 写入数据集合 (不改变底层对象，只将此dataMap所有数据进行替换) 
```

---

### Sa Token Doc New Docs Api Sa Strategy (`sa-token-doc-new/docs/api/sa-strategy.md`)

---
title: "Sa-Token SaStrategy-全局策略"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,SaStrategy-全局策略,API手册"
description: "SaStrategy 全局策略 API：用函数代理封装 createToken、createSession、路由匹配、CORS 与自动续期，可用 set 连缀重写。getSaTokenConfig 支持从数据库动态读配置并须自行缓存，策略内勿再调 SaManager.getConfig() 以免递归。"
---

# SaStrategy-全局策略

SaStrategy-全局策略，核心逻辑的代理封装。

--- 

### 核心策略

```
/* Detailed source-code truncated for AI context efficiency. */
```


### 重写策略（set 连缀风格）

``` java
SaStrategy.instance.setCreateToken(createToken);   // 重写创建 Token 的策略
SaStrategy.instance.setCreateSession(createSession);   // 重写创建 Session 的策略
SaStrategy.instance.setHasElement(hasElement);   // 重写集合模糊匹配策略
SaStrategy.instance.setGenerateUniqueToken(generateUniqueToken);   // 重写生成唯一 token 的策略
SaStrategy.instance.setCreateStpLogic(createStpLogic);   // 重写创建 StpLogic 的策略
SaStrategy.instance.setAutoRenew(autoRenew);   // 重写是否自动续期策略
SaStrategy.instance.setGetSaTokenConfig(getSaTokenConfig);   // 重写获取 SaTokenConfig 的策略
```

#### getSaTokenConfig 使用示例

适用于需要从数据库等外部数据源动态读取配置的场景：

``` java
SaStrategy.instance.setGetSaTokenConfig(() -> {
	// 从数据库读取配置，自行做好缓存
	SaTokenConfig config = new SaTokenConfig();
	config.setTokenName("satoken");
	config.setTimeout(30 * 24 * 60 * 60);
	return config;
});
```

注意：
- 策略内**不要**调用 `SaManager.getConfig()`，否则会无限递归。
- 启用后，每次 `SaManager.getConfig()` 都会走此策略；请做好缓存处理。

---

### Sa Token Doc New Docs Api Sa Token Dao (`sa-token-doc-new/docs/api/sa-token-dao.md`)

---
title: "SaTokenDao-数据持久接口"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,SaTokenDao-数据持久接口,API手册"
description: "SaTokenDao 数据持久接口：负责 Session、Token 等权限数据读写。含 NEVER_EXPIRE、NOT_VALUE_EXPIRE 常量，字符串、对象、Session 三套 get/set/update/delete 与超时 API，以及 searchData 搜索；对接 Redis 时按此签名实现。"
---

# SaTokenDao-数据持久接口

SaTokenDao 是数据持久层接口，负责所有会话数据的底层写入和读取。

--- 

### 1、常量 
``` java
SaTokenDao.NEVER_EXPIRE = -1;   // 常量，表示一个key永不过期 (在一个key被标注为永远不过期时返回此值)
SaTokenDao.NOT_VALUE_EXPIRE = -2;   // 常量，表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值)  
```


### 2、字符串读写 
``` java
dao.get(key);   // 获取Value，如无返空
dao.set(key, value, timeout);   // 写入Value，并设定存活时间 (单位: 秒)
dao.update(key, value);   // 更新Value (过期时间不变)
dao.delete(key);   // 删除Value
dao.getTimeout(key);   // 获取Value的剩余存活时间 (单位: 秒) 
dao.updateTimeout(key, timeout);   // 修改Value的剩余存活时间 (单位: 秒) 
```


### 3、对象读写 
``` java
dao.getObject(key);   // 获取Object，如无返空
dao.getObject(key, classType);   // 获取Object，并指定转换类型
dao.setObject(key, value, timeout);   // 写入Object，并设定存活时间 (单位: 秒)
dao.updateObject(key, value);   // 更新Object (过期时间不变)
dao.deleteObject(key);   // 删除Object
dao.getObjectTimeout(key);   // 获取Object的剩余存活时间 (单位: 秒) 
dao.updateObjectTimeout(key, timeout);   // 修改Object的剩余存活时间 (单位: 秒) 
```


### 4、Session读写 
``` java
dao.getSession(sessionId);   // 获取Session，如无返空
dao.setSession(session, timeout);   // 写入Session，并设定存活时间 (单位: 秒)
dao.updateSession(session);   // 更新Session (过期时间不变)
dao.deleteSession(sessionId);   // 删除Session
dao.getSessionTimeout(sessionId);   // 获取Session的剩余存活时间 (单位: 秒) 
dao.updateSessionTimeout(sessionId, timeout);   // 修改Session的剩余存活时间 (单位: 秒) 
```


### 5、会话管理
``` java
dao.searchData(prefix, keyword, start, size, sortType);   // 搜索数据 
```

---

### Sa Token Doc New Docs Api Stp Util (`sa-token-doc-new/docs/api/stp-util.md`)

---
title: "Sa-Token StpUtil-鉴权工具类"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,StpUtil-鉴权工具类,API手册"

description: "StpUtil 鉴权工具类是 Sa-Token 功能核心：覆盖登录、注销、踢人与顶人下线，会话查询与 loginId 反查 Token，Account-Session 与匿名 Token-Session，角色权限校验，Token 有效期与冻结续签，账号封禁与阶梯封禁，以及身份切换、二级认证方法一览。"
---

# StpUtil - 鉴权工具类

StpUtil 是 Sa-Token 整体功能的核心，大多数功能均由此工具类提供。

--- 

### 1、常规操作 
``` java
StpUtil.getStpLogic();   // 获取底层 StpLogic 对象。
StpUtil.setStpLogic(newStpLogic);   // 安全的重置底层 StpLogic 引用。
StpUtil.getLoginType();   // 获取账号类型 （例如：login、user、admin、teacher、student等等）。
StpUtil.getTokenName();   // 获取 Token 的名称 
StpUtil.getTokenValue();   // 获取本次请求前端提交的 Token。
StpUtil.getTokenValueNotCut();   // 获取本次请求前端提交的 Token (不裁剪前缀) 。
StpUtil.setTokenValue(tokenValue);   // 在当前会话中写入 Token 值。
StpUtil.setTokenValue(tokenValue, timeout);   // 在当前会话中写入 Token 值，并指定 Cookie 有效期。
StpUtil.setTokenValue(tokenValue, loginParameter);   // 在当前会话中写入 Token 值，并指定登录参数。
StpUtil.setTokenValueToStorage(tokenValue);   // 将 Token 写入当前请求的 Storage 存储器。
StpUtil.getTokenInfo();   // 获取当前 Token 的详细参数。
StpUtil.createSaLoginParameter();   // 根据当前配置对象创建一个 SaLoginParameter 对象
```


### 2、登录相关  
``` java
StpUtil.login(10001);   // 会话登录
StpUtil.login(10001, "APP");   // 会话登录，并指定设备类型
StpUtil.login(10001, true);   // 会话登录，并指定是否 [记住我]
StpUtil.login(10001, 86400);   // 会话登录，并指定此次 token 有效期（单位：秒）
StpUtil.login(10001, loginParameter);   // 会话登录，并指定所有登录参数Model
StpUtil.createLoginSession(10001);   // 创建指定账号id的登录会话，此方法不会将 Token 注入到上下文 
StpUtil.createLoginSession(10001, loginParameter);   // 创建指定账号id的登录会话，此方法不会将 Token 注入到上下文 
StpUtil.getOrCreateLoginSession(10001);   // 获取指定账号的登录会话，若不存在则创建并返回
```

更多 SaLoginParameter 登录参数的用法与示例，请参考 [登录参数示例章节](/up/login-parameter)。


### 3、注销相关
``` java
StpUtil.logout();   // 会话注销 
StpUtil.logout(logoutParameter);   // 会话注销，根据注销参数
StpUtil.logout(10001);   // 会话注销，根据账号id
StpUtil.logout(10001, "PC");   // 会话注销，根据账号id 和 设备类型
StpUtil.logout(10001, logoutParameter);   // 会话注销，根据账号id 和 注销参数
StpUtil.logoutByTokenValue(token);   // 指定 Token 强制注销
StpUtil.logoutByTokenValue(token, logoutParameter);   // 指定 Token 强制注销，带注销参数
StpUtil.kickout(10001);   // 踢人下线，根据账号id
StpUtil.kickout(10001, "PC");   // 踢人下线，根据账号id 和 设备类型
StpUtil.kickout(10001, logoutParameter);   // 踢人下线，根据账号id 和 注销参数
StpUtil.kickoutByTokenValue(token);   // 踢人下线，根据token
StpUtil.kickoutByTokenValue(token, logoutParameter);   // 踢人下线，根据token，带注销参数
StpUtil.replacedByTokenValue(token);   // 顶人下线，根据token
StpUtil.replacedByTokenValue(token, logoutParameter);   // 顶人下线，根据token，带注销参数
StpUtil.replaced(10001);   // 顶人下线，根据账号id
StpUtil.replaced(10001, "PC");   // 顶人下线，根据账号id 和 设备类型
StpUtil.replaced(10001, logoutParameter);   // 顶人下线，根据账号id 和 注销参数
```

更多 SaLogoutParameter 注销参数的用法与示例，请参考 [注销参数示例章节](/up/login-parameter#2、注销参数)。


### 4、会话查询
``` java
StpUtil.isLogin();   // 当前会话是否已经登录 
StpUtil.isLogin(10001);   // 判断指定账号是否已登录
StpUtil.checkLogin();   // 检验当前会话是否已经登录，如未登录，则抛出异常
StpUtil.getLoginId();   // 获取当前会话账号id, 如果未登录，则抛出异常 
StpUtil.getLoginId(defaultValue);   // 获取当前会话账号id, 如果未登录，则返回默认值 
StpUtil.getLoginIdDefaultNull();   // 获取当前会话账号id, 如果未登录，则返回null 
StpUtil.getLoginIdAsString();   // 获取当前会话账号id, 并转换为String类型
StpUtil.getLoginIdAsInt();   // 获取当前会话账号id, 并转换为int类型
StpUtil.getLoginIdAsLong();   // 获取当前会话账号id, 并转换为long类型 
StpUtil.getLoginIdByToken(token);   // 获取指定 Token 对应的账号 id，若 token 无效/被踢/被顶/被冻结则返回 null 
StpUtil.getLoginIdByTokenNotThinkFreeze(token);   // 获取指定Token对应的账号id（不考虑冻结状态），如果未登录，则返回 null 
StpUtil.getExtra(key);   // 获取当前 Token 的扩展信息（此函数只在jwt模式下生效）
StpUtil.getExtra(token, key);   // 获取指定 Token 的扩展信息（此函数只在jwt模式下生效）
```


### 5、Session 相关
``` java
// Account-Session 相关 
StpUtil.getSession();   // 获取当前会话的Session，如果Session尚未创建，则新建并返回 
StpUtil.getSession(true);   // 获取当前会话的Session, 如果Session尚未创建，isCreate=是否新建并返回
StpUtil.getSessionByLoginId(10001);   // 获取指定账号id的Session，如果Session尚未创建，则新建并返回
StpUtil.getSessionByLoginId(10001, true);   // 获取指定账号id的Session, 如果Session尚未创建，isCreate=是否新建并返回

// Token-Session 相关 
StpUtil.getTokenSession();   // 获取当前 Token-Session，如果 Session 尚未创建，则新建并返回 
StpUtil.getTokenSessionByToken(token);   // 获取指定Token-Session，如果Session尚未创建，则新建并返回
StpUtil.getAnonTokenSession();   // 获取当前匿名 Token-Session （可在未登录情况下使用的Token-Session）

// 其它
StpUtil.getSessionBySessionId("xxxx-xxxx-xxxx");   // 获取指定 sessionId 的 Session，若不存在则返回 null
StpUtil.isTrustDeviceId(123456, "xxxxxxxxxxxxxxxxxxxxxxxx");   // 判断对于指定 loginId 来讲，指定设备 id 是否为可信任设备
```


### 6、Token有效期相关
``` java
// Token 最低活跃频率
StpUtil.getTokenActiveTimeout();   // 获取当前 token 距离被冻结还剩多少时间 (单位: 秒)
StpUtil.getTokenLastActiveTime();  // 获取当前 token 最后活跃时间
StpUtil.checkActiveTimeout();   // 检查当前token 是否已经被冻结，如果是则抛出异常  
StpUtil.updateLastActiveToNow();   // 续签当前token：(将 [最后操作时间] 更新为当前时间戳)   

// Token 有效期
StpUtil.getTokenTimeout();   // 获取当前登录者的 token 剩余有效时间 (单位: 秒)
StpUtil.getTokenTimeout(token);   // 获取指定 token 的剩余有效时间 (单位: 秒)
StpUtil.getSessionTimeout();   // 获取当前登录者的 Account-Session 剩余有效时间 (单位: 秒)
StpUtil.getTokenSessionTimeout();   // 获取当前 Token-Session 剩余有效时间 (单位: 秒) 
StpUtil.renewTimeout(timeout);   // 对当前 Token 的 timeout 值进行续期 
StpUtil.renewTimeout(token, timeout);   // 对指定 Token 的 timeout 值进行续期 
```


### 7、角色认证
``` java
StpUtil.getRoleList();   // 获取：当前账号的角色集合
StpUtil.getRoleList(10001);   // 获取：指定账号的角色集合 
StpUtil.hasRole(role);   // 判断：当前账号是否拥有指定角色, 返回true或false 
StpUtil.hasRole(loginId, role);   // 判断：指定账号是否含有指定角色标识, 返回true或false 
StpUtil.hasRoleAnd(...roleArray);   // 判断：当前账号是否含有指定角色标识 [指定多个，必须全部验证通过] 
StpUtil.hasRoleOr(...roleArray);   // 判断：当前账号是否含有指定角色标识 [指定多个，只要其一验证通过即可] 
StpUtil.checkRole(role);   // 校验：当前账号是否含有指定角色标识, 如果验证未通过，则抛出异常: NotRoleException 
StpUtil.checkRoleAnd(...roleArray);   // 校验：当前账号是否含有指定角色标识 [指定多个，必须全部验证通过] 
StpUtil.checkRoleOr(...roleArray);   // 校验：当前账号是否含有指定角色标识 [指定多个，只要其一验证通过即可] 
```


### 8、权限认证
``` java
StpUtil.getPermissionList();   // 获取：当前账号的权限集合
StpUtil.getPermissionList(10001);   // 获取：指定账号的权限集合 
StpUtil.hasPermission(permission);   // 判断：当前账号是否拥有指定权限, 返回true或false 
StpUtil.hasPermission(loginId, permission);   // 判断：指定账号是否含有指定权限标识, 返回true或false 
StpUtil.hasPermissionAnd(...permissionArray);   // 判断：当前账号是否含有指定权限标识 [指定多个，必须全部验证通过] 
StpUtil.hasPermissionOr(...permissionArray);   // 判断：当前账号是否含有指定权限标识 [指定多个，只要其一验证通过即可] 
StpUtil.checkPermission(permission);   // 校验：当前账号是否含有指定权限标识, 如果验证未通过，则抛出异常: NotPermissionException 
StpUtil.checkPermissionAnd(...permissionArray);   // 校验：当前账号是否含有指定权限标识 [指定多个，必须全部验证通过] 
StpUtil.checkPermissionOr(...permissionArray);   // 校验：当前账号是否含有指定权限标识 [指定多个，只要其一验证通过即可] 
```


### 9、id 反查 Token
``` java
StpUtil.getTokenValueByLoginId(10001);   // 获取指定账号id的tokenValue 
StpUtil.getTokenValueByLoginId(10001, "PC");   // 获取指定账号id指定设备类型端的tokenValue
StpUtil.getTokenValueListByLoginId(10001);   // 获取指定账号id的tokenValue集合 
StpUtil.getTokenValueListByLoginId(10001, "APP");   // 获取指定账号id指定设备类型端的tokenValue 集合 
StpUtil.getTerminalListByLoginId(10001);   // 获取指定账号 id 已登录设备信息集合
StpUtil.getTerminalListByLoginId(10001, "PC");   // 获取指定账号 id 指定设备类型端的已登录设备信息集合
StpUtil.forEachTerminalList(10001, (session, terminal) -> {});   // 遍历指定账号的已登录设备列表
StpUtil.getTerminalInfo();   // 获取当前会话的终端信息
StpUtil.getTerminalInfoByToken(token);   // 获取指定 token 的终端信息
StpUtil.getLoginDeviceId();   // 返回当前会话的登录设备 id
StpUtil.getLoginDeviceIdByToken(token);   // 返回指定 token 的登录设备 id
StpUtil.getLoginDeviceType();   // 返回当前会话的登录设备类型
StpUtil.getLoginDeviceTypeByToken(token);   // 返回指定 token 的登录设备类型
```


### 10、会话管理
``` java
StpUtil.searchTokenValue(keyword, start, size, sortType);   // 根据条件查询Token
StpUtil.searchSessionId(keyword, start, size, sortType);   // 根据条件查询SessionId 
StpUtil.searchTokenSessionId(keyword, start, size, sortType);   // 根据条件查询Token专属Session的Id 
```
详细可参考：[会话治理](/up/search-session)


### 11、账号封禁
``` java
StpUtil.disable(10001, 1200);   // 封禁：指定账号 指定时间(单位s)
StpUtil.isDisable(10001);   // 判断：指定账号是否已被封禁 (true=已被封禁, false=未被封禁) 
StpUtil.checkDisable(10001);   // 校验：指定账号是否已被封禁，如果被封禁则抛出异常 `DisableServiceException`
StpUtil.getDisableTime(10001);   // 获取：指定账号剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）
StpUtil.untieDisable(loginId);   // 解封：指定账号
```


### 12、分类封禁 (version >= 1.31.0)
``` java
StpUtil.disable(10001, "<业务标识>", 86400);   // 封禁：指定账号的指定服务 指定时间(单位s)
StpUtil.isDisable(10001, "<业务标识>");   // 判断：指定账号的指定服务 是否已被封禁 (true=已被封禁, false=未被封禁) 
StpUtil.checkDisable(10001, "<业务标识>");   // 校验：指定账号的指定服务 是否已被封禁，如果被封禁则抛出异常 `DisableServiceException`（支持传入多个业务标识）
StpUtil.getDisableTime(10001, "<业务标识>");   // 获取：指定账号的指定服务 剩余封禁时间，单位：秒（-1=永久封禁，-2=未被封禁）
StpUtil.untieDisable(loginId, "<业务标识>");   // 解封：指定账号的指定服务（支持传入多个业务标识）
```


### 13、阶梯封禁 (version >= 1.31.0)
``` java
StpUtil.disableLevel(10001, "comment", 3, 10000);   // 分类阶梯封禁，参数：封禁账号、封禁服务、封禁级别、封禁时间 
StpUtil.disableLevel(10001, 3, 10000);   // 阶梯封禁（无业务标识，使用默认服务），参数：封禁账号、封禁级别、封禁时间
StpUtil.getDisableLevel(10001, "comment");   // 获取：指定账号的指定服务 封禁的级别 （如果此账号未被封禁则返回 -2）
StpUtil.getDisableLevel(10001);   // 获取：指定账号的封禁等级（无业务标识，未封禁返回 -2）
StpUtil.isDisableLevel(10001, "comment", 3);   // 判断：指定账号的指定服务 是否已被封禁到指定级别，返回 true 或 false
StpUtil.isDisableLevel(10001, 3);   // 判断：指定账号是否被封禁到指定级别（无业务标识）
StpUtil.checkDisableLevel(10001, "comment", 2);   // 校验：指定账号的指定服务 是否已被封禁到指定级别（例如 comment服务 已被3级封禁，这里校验是否达到2级），如果已达到此级别，则抛出异常 
StpUtil.checkDisableLevel(10001, 2);   // 校验：指定账号是否被封禁到指定级别（无业务标识）
```


### 14、身份切换
``` java
StpUtil.switchTo(10044);   // 临时切换身份为指定账号id 
StpUtil.endSwitch();   // 结束临时切换身份
StpUtil.isSwitch();   // 当前是否正处于[身份临时切换]中 
StpUtil.switchTo(10044, () -> {});   // 在一个代码段里方法内，临时切换身份为指定账号id
```


### 15、二级认证
``` java
StpUtil.openSafe(safeTime);   // 在当前会话 开启二级认证 
StpUtil.isSafe();   // 当前会话 是否处于二级认证时间内 
StpUtil.checkSafe();   // 检查当前会话是否已通过二级认证，如未通过则抛出异常 
StpUtil.getSafeTime();   // 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.closeSafe();   // 在当前会话 结束二级认证 
```


### 16、带有业务标识的二级认证
``` java
StpUtil.openSafe("<业务标识>", safeTime);   // 在当前会话 指定业务标识开启二级认证 
StpUtil.isSafe("<业务标识>");   // 当前会话 指定业务标识是否处于二级认证时间内 
StpUtil.isSafe(tokenValue, "<业务标识>");   // 判断指定 token 的指定业务是否处于二级认证时间内
StpUtil.checkSafe("<业务标识>");   // 检查当前会话，指定业务标识是否已通过二级认证，如未通过则抛出异常 
StpUtil.getSafeTime("<业务标识>");   // 获取当前会话的指定业务标识二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.closeSafe("<业务标识>");   // 在当前会话 结束指定业务标识二级认证 
```

---

### Sa Token Doc New Docs Arch Data Structure (`sa-token-doc-new/docs/arch/data-structure.md`)

---
title: "Sa-Token 数据结构"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,数据结构,框架设计"
description: "Sa-Token Redis 数据结构：登录会话 token 到 loginId、last-active 时间戳、三类 Session 的 key 命名与 value 字段；异常值 -1 至 -7 表示未读到、无效、过期、顶踢冻结等；并覆盖 SSO ticket、OAuth2 令牌与 API Key 插件索引。"
---

# 数据结构


## 1、登录会话

### 1.1、token -> loginId 映射

``` js
// ttl = 此 token 的 timeout 有效期
{tokenName}:{loginType}:token:{tokenValue}  -->  {loginId}
```

<details>
<summary>详细</summary>

示例：
``` js
satoken:login:token:47ab0105-2be1-400c-b517-82f81a0cfcf8  -->  10001
```

异常 value 格式

``` js
-1       未能从请求中读取到有效 token
-2       已读取到 token，但是 token 无效
-3       已读取到 token，但是 token 已经过期 (详)
-4       已读取到 token，但是 token 已被顶下线
-5       已读取到 token，但是 token 已被踢下线
-6       已读取到 token，但是 token 已被冻结
-7       未按照指定前缀提交 token
```

</details>


### 1.2、active-timeout

``` js
// ttl = 对应 token 的 timeout 有效期值
{tokenName}:{loginType}:last-active:{tokenValue}  -->  {13位时间戳}
```

<details>
<summary>详细</summary>

示例：
``` js
satoken:login:last-active:06d1f12b-614e-4c00-8d8e-c07fef5f4aa9   -->   1722334954193
```

value 格式分两种：
```
1722334954193          // 单值时：此 token 最后访问日期
1722334954193, 1200    // 双值时：此 token 最后访问日期，此 token 指定的动态 active-timeout 值 
```

注意：判断一个 token 是否 active-timeout 过期，与 ttl 无关，而是利 value 值计算：
``` js
当前时间 - token 最后访问时间 > active-timeout   （true=token 已冻结，false=token 未冻结 ）
```

</details>



### 1.3、SaSession

``` js
{tokenName}:{loginType}:session:{loginId}  -->  {SaSession 对象}        // Account-Session
{tokenName}:{loginType}:token-session:{loginId}  -->  {SaSession 对象}  // Token-Session
{tokenName}:custom:session:{sessionId}  -->  {SaSession 对象}           // Custom-Session
```

<details>
<summary>详细</summary>

key 示例 
``` js
// Account-Session
satoken:login:session:1000001

// Token-Session
satoken:login:session:47ab0105-2be1-400c-b517-82f81a0cfcf8

// Custom-Session
satoken:custom:session:role-1001
```

value 格式 

``` js
{
  "@class": "cn.dev33.satoken.dao.SaSessionForJacksonCustomized",    // java calss 信息
  "id": "satoken:login:session:10001",    // sessionId
  "type": "Account-Session",    // session类型：Account-Session / Token-Session / Custom-Session
  "loginType": "login",     // 账号类型 
  "loginId": [    // 对应登录id 值（Account-Session才会有值）
    "java.lang.Long",
    10001
  ],    
  "token": null,    // 对应 token 值 （Token-Session才会有值）
  "createTime": 1722334954145,    // 此 session 创建时间，13位时间戳 
  "dataMap": {    // 此 session 挂载数据 
    "@class": "java.util.concurrent.ConcurrentHashMap", 
    "name": "张三"    // 此 session 挂载数据 详情
	// 更多值 ...
  },
  "terminalList": [		// 已登录终端信息列表（Account-Session才会有值）
    "java.util.Vector",
    [
      {
        "@class": "cn.dev33.satoken.session.SaTerminalInfo",
        "index": 1,
        "tokenValue": "2551663f-bb98-47d7-9af3-e2e6a28dadce",   // 客户端 token 值
        "deviceType": "DEF",  // 登录设备类型 
        "deviceId": "xxxxxxxxx",  // 登录设备id 
        "extraData": {
			// 扩展信息列表 （手动自定义值）
			"@class": "java.util.LinkedHashMap",
			"deviceSimpleTitle": "XiaoMi 15 Ultra",
			"loginAddress": "浙江省杭州市西湖区",
			"loginIp": "127.0.0.1",
			"loginTime": "2025-03-08 15:00:02"
        },
        "createTime": 1741406340845 // 登录时间 
      }
    ]
  ]
}
```

</details>


### 1.4、二级认证
``` js
{tokenName}:{loginType}:safe:{service}:{tokenValue}  -->  SAFE_AUTH_SAVE_VALUE
```
value 为常亮值：`SAFE_AUTH_SAVE_VALUE`


### 1.5、账号服务封禁
``` js
{tokenName}:{loginType}:disable:{service}:{loginId}  -->  {level}
```
value 为封禁等级，int类型 


### 1.6、其它
SaApplication 全局变量
``` js
{tokenName}:var:{变量名}
```

本次请求新创建 token，在 SaStorage 存储 key 
``` js
JUST_CREATED_  -->  {token}
```

本次请求新创建 token，在 SaStorage 存储 key  （无前缀方式）
``` js
JUST_CREATED_NOT_PREFIX_  -->  {token}
```

临时身份切换，使用的key
``` js
SWITCH_TO_SAVE_KEY_{loginType}  -->  {loginId}
```


## 2、SSO 单点登录

### 2.1、ticket -> loginId 映射
``` js
// ttl = 此 ticket 有效期，下同理 
{tokenName}:ticket:{ticket}  -->  {loginId}
```

### 2.2、ticket -> client 映射
``` js
{tokenName}:ticket-client:{ticket}  -->  {client}
```

### 2.3、loginId -> ticket 映射（client + loginId 反查 ticket）
``` js
{tokenName}:ticket-index:{client}:{loginId}  -->  {ticket}
```



## 3、OAuth2 统一认证 

### 3.1、Code 授权码
``` js
{tokenName}:oauth2:code:{code}  -->  {CodeModel 对象}
```

<details>
<summary>详细</summary>

value 示例：

``` js
{
	"@class": "cn.dev33.satoken.oauth2.model.CodeModel",    // java class 信息
	"code": "AbRVp2HrgyklE0BXYWszskGJWAGY7xhGu6Zaco4zJECzGYagCCFWj0jOlHza",    // code值
	"scope": "",    // 所申请权限列表，多个用逗号隔开
	"loginId": "10001",    // 对应的loginId
	"redirectUri": "",    // 重定向地址
}
```

</details>

clientId + loginId 反查 code
``` js
{tokenName}:oauth2:code-index:{clientId}:{loginId}  -->  {code 值}
```



### 3.2、Access-Token 资源令牌
``` js
{tokenName}:oauth2:access-token:{accessToken}  -->  {AccessTokenModel 对象}
```

<details>
<summary>详细</summary>

value 示例：

``` js
{
  "@class": "cn.dev33.satoken.oauth2.data.model.AccessTokenModel",    // java class 信息
  "accessToken": "Pu3t55dJIgvkmVoHz50FqaVQOJ6Flggjr2eHTiS74Ooai8e3nNyYPq78K80P",    // 资源令牌值
  "refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m",    // 刷新令牌值
  "expiresTime": 1738280553695,    // 资源令牌到期时间
  "refreshExpiresTime": 1740865353760,    // 刷新令牌到期时间
  "clientId": "1001",    // 对应的应用id
  "loginId": "10001",    // 对应的loginId
  "scopes": [   // 所具有的权限列表 
    "java.util.ArrayList",
    [
      "userinfo",
      "userid",
      "openid",
      "unionid",
      "oidc"
    ]
  ],
  "tokenType": "Bearer",   // tokenType 
  "grantType": "authorization_code",   // 授权方式 
  "extraData": {   // 扩展数据  
    "@class": "java.util.LinkedHashMap",
    "userid": "10001",
    "openid": "ded91dc189a437dd1bac2274be167d50",
    "unionid": "11d48faa74c4e5f19355ccc53c1c5c7a",
    "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g"
  },
  "createTime": 1738273353694,   // 创建时间   
  "expiresIn": 7199    // 资源令牌剩余有效时间，单位秒
  "refreshExpiresIn": 2592000,    // 刷新令牌剩余有效时间，单位秒
}

```

</details>

clientId + loginId 反查 Access-Token
``` js
{tokenName}:oauth2:access-token-index:{clientId}:{loginId}  -->  {access_token 值}
```


### 3.3、Refresh-Token 资源令牌
``` js
{tokenName}:oauth2:refresh-token:{refreshToken}  -->  {RefreshTokenModel 对象}
```

<details>
<summary>详细</summary>

value 示例：

``` js
{
  "@class": "cn.dev33.satoken.oauth2.data.model.RefreshTokenModel",    // java class 信息
  "refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m",    // 刷新令牌值
  "expiresTime": 1740865353760,   // 刷新令牌到期时间
  "clientId": "1001",    // 对应的应用id
  "loginId": "10001",    // 对应的loginId
  "scopes": [    // 所具有的权限列表 
    "java.util.ArrayList",
    [
      "userinfo",
      "userid",
      "openid",
      "unionid",
      "oidc"
    ]
  ],
  "extraData": {   // 扩展数据  
    "@class": "java.util.LinkedHashMap",
    "userid": "10001",
    "openid": "ded91dc189a437dd1bac2274be167d50",
    "unionid": "11d48faa74c4e5f19355ccc53c1c5c7a",
    "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g"
  },
  "createTime": 1738273353760,   // 创建时间   
  "expiresIn": 2591999    // 刷新令牌剩余有效时间，单位秒
}
```

</details>

clientId + loginId 反查 Refresh-Token
``` js
{tokenName}:oauth2:refresh-token-index:{clientId}:{loginId}  -->  {refresh_token 值}
```


### 3.4、Client-Token 应用令牌
``` js
{tokenName}:oauth2:client-token:{clientToken}  -->  {ClientTokenModel 对象}
```

<details>
<summary>详细</summary>

value 示例：

``` js
{
  "@class": "cn.dev33.satoken.oauth2.data.model.ClientTokenModel",    // java class 信息
  "clientToken": "lIpS3fKEACKMFauEWVpR7Zmzh7SoFetPVuB9aDzISnqzHKu8R3OwpWFy5nLv",    // 应用令牌值 
  "expiresTime": 1738280930646,    // 应用令牌到期时间
  "clientId": "1001",    // 对应的应用id
  "scopes": [    // 所具有的权限列表 
    "java.util.ArrayList",
    [
      "userinfo",
      "userid",
      "openid",
      "unionid",
      "oidc"
    ]
  ],
  "tokenType": "Bearer",   // tokenType   
  "grantType": "client_credentials",   // 授权类型    
  "extraData": {   // 扩展数据
    "@class": "java.util.LinkedHashMap"
  },
  "createTime": 1738273730646,   // 创建时间   
  "expiresIn": 7199    // 应用令牌剩余有效时间，单位秒
}
```

</details>

clientId 反查 Client-Token
``` js
{tokenName}:oauth2:client-token-index:{clientId}  -->  {client_token 值}
```

Lower-Client-Token 次级应用令牌索引
``` js
{tokenName}:oauth2:lower-client-token-index:{clientId}  -->  {client_token 值}
```

### 3.5、用户授权记录
``` js
{tokenName}:oauth2:grant-scope:{clientId}:{loginId}  -->  {scope列表}
```
值为 scope 列表，多个用逗号隔开，例如：`userinfo,openid,userid`。




## 4、插件

### 4.1、临时 token 会话 

temp-token -> value

``` js
// namespace 默认值为 "temp-token"
{tokenName}:{namespace}:{temp-token}  -->  {value}
```

value 反查 temp-token

``` js
{tokenName}:raw-session:{namespace}:{value}  -->  {Raw#SaSession 对象}
```

- 在 SaSession 以 `__HD_TEMP_TOKEN_MAP` 为 key 存储 temp-token 索引列表。值类型为 Map。
- 其中：Map 的 key = temp-token 值，Map 的 value = 此 temp-token 到期时间戳。



### 4.2、 Same-Token 

Same-Token 
``` js
{tokenName}:var:same-token  -->  {same-token 值}
```

Past-Same-Token 
``` js
{tokenName}:var:past-same-token  -->  {same-token 值}
```


### 4.3、Sign 签名

随机字符串
``` js
// nonce 值 默认为 32位随机字符
{tokenName}:sign:nonce:{nonce}  -->  {nonce 值}
```

### 4.4、API Key

``` js
// namespace 默认值为 "apikey" (全小写), ttl = 此 API Key 剩余有效期 
{tokenName}:{namespace}:{apikey}  -->  {ApiKeyModel 对象}
```

<details>
<summary>详细</summary>

key 示例：

``` js
satoken:apikey:AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R
```

value 示例：
``` js
{
	"@class": "cn.dev33.satoken.apikey.model.ApiKeyModel",   // java class 信息
	"title": "test",   // API Key 名称 
	"intro": null,   // 用途介绍
	"apiKey": "AK-XCoJLP2E7Q9GXyPiiZWMM8Sqi6Fm0JoFC41R",   // API Key 值
	"loginId": "10001",   // 所属用户 id
	"createTime": 1766509019137,   // 创建时间戳
	"expiresTime": 1769101019136,   // 到期时间戳
	"isValid": true,   // 是否有效
	"scopes": [   // 含有权限
		"java.util.ArrayList",
		[
		  "userinfo",
		  "user-update"
		]
	],
	"extraData": null   // 扩展数据：Map<String, Object> 类型 
}
```

</details>


value 反查 API Key 

``` js
{tokenName}:raw-session:{namespace}:{value}  -->  {Raw#SaSession 对象}
```

- 在 SaSession 以 `__HD_API_KEY_LIST` 为 key 存储 API Key 索引列表。值类型为 List (API Key 列表)。

---

### Sa Token Doc New Docs Fun Async  Mock (`sa-token-doc-new/docs/fun/async--mock.md`)

---
title: "Sa-Token 异步 & Mock 上下文"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,异步 & Mock 上下文,附录"
description: "Sa-Token 异步与 Mock 上下文：new Thread、线程池、@Async、@Scheduled、MQ 等非 Web 线程调 StpUtil 会抛上下文未初始化。需 setMockContext 并写入 token；WebFlux 用 SaReactorSyncHolder.setContext。"
---

# 异步 & Mock 上下文

### 异步上下文

有一些方法（例如`StpUtil.isLogin()`）只可以在同步的 Web 上下文中才可以调用，如果在异步上下文中调用则会抛出异常：

``` text
cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化
```

这是因为这些方法需要从前端的 `HttpServletRequest` 中读取 Token 参数，而异步上下文通常不是一次“请求”，不具有 `HttpServletRequest` 的概念，所以无法成功调用。

一般哪些场景属于异步上下文？
- 通过 `new Thread(() -> { ... }).start()` 启动子线程。
- 通过 `taskExecutor.execute(() -> { ... })` 线程池启动异步任务。
- 通过 `@Async` 注解标注的方法。
- 通过 `@Scheduled(cron = "")` 启动的定时任务。
- 消息队列中消费消息的函数。
- ...

凡是不通过 web 请求调用触发的线程，在 Sa-Token 中均属于异步上下文，也可以称作 “非 Web 上下文”。

此时调用 `StpUtil.isLogin()`、`StpUtil.getLoginId()` 等需要 Web 上下文的 API，就会抛出上述异常。

如果你需要在 非 Web 上下文 中调用上述 API，则需要手动 mock 一个上下文，才可以调用成功：

例如：
``` java
// 【异步】new Thread  
@RequestMapping("isLogin2")
public SaResult isLogin2() {
	System.out.println("是否登录：" + StpUtil.isLogin());
	String tokenValue = StpUtil.getTokenValue();
	new Thread(() -> {
		SaTokenContextMockUtil.setMockContext(()->{
			StpUtil.setTokenValueToStorage(tokenValue);
			System.out.println("是否登录：" + StpUtil.isLogin());
		});
	}).start();
	return SaResult.data(StpUtil.getTokenValue());
}
```

参考上述方法，你需要先调用 `SaTokenContextMockUtil.setMockContext(() -> { ... })` Mock 出一个 Web 上下文填充到上下文管理器中，
然后在 Mock 上下文范围内调用 `StpUtil.setTokenValueToStorage(tokenValue)` 指定当前上下文的 token 值，其效果等同于在 web 上下文中前端提交了此 token 值。

更多使用姿势请参考仓库示例：[Async-TestController.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-async/src/main/java/com/pj/test/TestController.java)


### 响应式上下文

在 WebFlux / Spring Cloud 等响应式环境下调用 Sa-Token 的同步 API 也有可能发生上下文异常：

```
cn.dev33.satoken.exception.SaTokenContextException: SaTokenContext 上下文尚未初始化
	at cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff.getModelBox(SaTokenContextForThreadLocalStaff.java:73) ~[classes/:na]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaReactorFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor [DefaultWebFilterChain]
	*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenCorsFilterForReactor [DefaultWebFilterChain]
	*__checkpoint ⇢ cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor [DefaultWebFilterChain]
	*__checkpoint ⇢ HTTP GET "/test/isLogin" [ExceptionHandlingWebHandler]
```

如果是在自定义 Filter 中报的这个错，需要你在调用 Sa-Token 的同步 API 之前手动 set 一下上下文：
``` java
// 自定义过滤器
@Component
public class MyFilter implements WebFilter {
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
		try {
			// 先 set 上下文，再调用 Sa-Token 同步 API，并在 finally 里清除上下文
			SaReactorSyncHolder.setContext(exchange);
			System.out.println(StpUtil.isLogin());
		}
		finally {
			SaReactorSyncHolder.clearContext();
		}
		return chain.filter(exchange);
	}
}
```

参考：[MyFilter.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/satoken/MyFilter.java)

在 Controller 里同理：

``` java
@RequestMapping("isLogin2")
public SaResult isLogin2(ServerWebExchange exchange) {
	SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{
		System.out.println("是否登录：" + StpUtil.isLogin());
		return SaResult.data(StpUtil.getTokenInfo());
	});
	return SaResult.data(res);
}
```

更多示例请参考：
[TestController.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/test/TestController.java)

---

### Sa Token Doc New Docs Fun Auth Flow (`sa-token-doc-new/docs/fun/auth-flow.md`)

---
title: "Sa-Token功能结构图"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Sa-Token功能结构图,附录"
description: "Sa-Token 功能结构图与认证流程图：上图梳理登录鉴权、Session 管理与插件模块整体架构，下图展开从提交凭证、读取 Token 到校验通过的路径。页面只放图不写步骤；图分别为权限认证流程与登录认证流程示意，鼠标右键「在新窗口打开图片」可高清查看。高清模式查看后可对照登录、权限与插件相关章节。"
---

# Sa-Token 功能结构图

--- 

### Sa-Token 功能结构图：

<img class="s-w" src="/big-file/doc/fun/sa-token-js4--2.png" alt="Sa-Token 权限认证流程示意图" />

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

<img class="s-w" src="/big-file/doc/fun/sa-token-rz2.png" alt="Sa-Token 登录认证流程示意图" />



PS：鼠标右键选择 **`[在新窗口打开图片]`** 即可高清模式查看图片。

---

### Sa Token Doc New Docs Fun Auth Framework Function Test (`sa-token-doc-new/docs/fun/auth-framework-function-test.md`)

---
title: "Sa-Token 从 Shiro、SpringSecurity、JWT 迁移"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,从 Shiro、SpringSecurity、JWT 迁移,附录"
description: "从 Shiro、Spring Security、JWT 迁移到 Sa-Token：对照登录注销、MD5 加盐、角色权限、注解与路由鉴权、Thymeleaf、前后端分离及 Redis 等常见能力的写法差异。案例仓库见 Gitee auth-framework-function-test，欢迎补充 PR。"
---

# Java 权限认证框架功能 测试 / 对比 / 迁移。

对比以下框架的常见功能，为项目技术栈迁移提供代码示例

- Sa-Token
- Apache Shiro
- Spring Security
- JWT


::: tip 注意事项
- 因个人精力&能力有限，本篇只展示部分常见功能的对比，也欢迎大家一起贡献案例，提交pr。
- 代码案例仓库：[https://gitee.com/sa-tokens/auth-framework-function-test](https://gitee.com/sa-tokens/auth-framework-function-test)
- 注：本篇主要展示一些常见功能不同框架的实现差异，而非每个框架的所含功能点对比。
:::
 

--- 


### 依赖引入

:::tabs
== Sa-Token

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-boot3-starter</artifactId>
	<version>1.39.0</version>
</dependency>
```

== Shiro

``` xml

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring-boot-web-starter</artifactId>
	<version>1.13.0</version>
</dependency>
```

== SpringSecurity

``` xml

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
	<version>3.3.2</version>
</dependency>
```
SpringBoot 项目下一般不用特别指定 SpringSecurity 版本号

== JWT

``` xml

<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.8.29</version>
</dependency>
```

:::




### 会话登录 & 会话状态查询

:::tabs
== Sa-Token

测试 Controller 
``` java 
@RestController
@RequestMapping("/acc/")
public class LoginController {
	
	@Autowired
	SysUserDao sysUserDao;
		
	// 测试登录 
	@RequestMapping("doLogin")
	public AjaxJson doLogin(String username, String password) {
		// 校验
		SysUser user = sysUserDao.findByUsername(username);
		if(user == null) {
			return AjaxJson.getError("用户不存在");
		}
		if(!user.getPassword().equals(password)) {
			return AjaxJson.getError("密码错误");
		}
		// 登录
		StpUtil.login(user.getId());
		StpUtil.getSession().set("user", user);
		return AjaxJson.getSuccess("登录成功");
	}
	
	// 查询登录状态 
	@RequestMapping("isLogin")
	public AjaxJson isLogin() {
		if(StpUtil.isLogin()) {
			return AjaxJson.getSuccess("已登录，账号id：" + StpUtil.getLoginId());
		}
		return AjaxJson.getError("未登录");
	}

}
```

== Shiro

自定义 Realm 
``` java
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private SysUserDao sysUserDao;

    // 加载用户信息
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        String username = (String)token.getPrincipal();
        SysUser sysUser = sysUserDao.findByUsername(username);
        if(sysUser == null){
            return null;
        }
        return new SimpleAuthenticationInfo(
                sysUser,
                sysUser.getPassword(),
                getName()
        );
    }
	
    // 加载权限信息
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

}
```

Shiro 配置类
``` java
@Configuration
public class ShiroConfigure {

    @Bean
    public MyRealm myRealm() {
        return new MyRealm();
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm());
        return manager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager());
        return bean;
    }

}
```

测试 Controller 
``` java
@RestController
@RequestMapping("/acc/")
public class LoginController {

	// 测试登录  ---- http://localhost:8082/acc/doLogin?username=zhang&password=123456
	@RequestMapping("doLogin")
	public AjaxJson doLogin(String username, String password) {
		Subject subject = SecurityUtils.getSubject();
		try {
			subject.login(new UsernamePasswordToken(username, password));
			return AjaxJson.getSuccess("登录成功!");
		} catch (AuthenticationException e) {
			e.printStackTrace();
			return AjaxJson.getError(e.getMessage());
		}
	}

	// 查询登录状态  ---- http://localhost:8082/acc/isLogin
	@RequestMapping("isLogin")
	public AjaxJson isLogin() {
		Subject subject = SecurityUtils.getSubject();
		if(subject.isAuthenticated()) {
			SysUser sysUser = (SysUser)subject.getPrincipal();
			return AjaxJson.getSuccess("已登录，账号id：" + sysUser.getId());
		}
		return AjaxJson.getError("未登录");
	}

}
```

== SpringSecurity

定义 SpringSecurity 配置类
``` java
@Configuration
public class SpringSecurityConfigure {

    /**
     * Spring Security的核心过滤器链配置
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 定义安全请求拦截规则
        httpSecurity.authorizeHttpRequests(router -> {
            router
                    // 放行接口
                    .requestMatchers("/acc/doLogin", "/acc/isLogin").permitAll()

					// 所有请求都需要认证
                    .anyRequest().authenticated(); 
                    ;
                });

        // 默认的表单登录
        httpSecurity.formLogin(withDefaults());

        // 是否启用 csrf 防御
        httpSecurity.csrf( csrf -> csrf.disable() );

        // 一些安全相关的全局响应头
        httpSecurity.headers(httpSecurityHeadersConfigurer -> {
            httpSecurityHeadersConfigurer.cacheControl(HeadersConfigurer.CacheControlConfig::disable);
            httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);
        });
		
        return httpSecurity.build();
    }

    /**
     * Spring Security 认证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

}
```

定义 SpringSecurity UserDetails 管理器
``` java
/**
 * 自定义 SpringSecurity UserDetails 管理器
 *
 * @author click33
 * @since 2024/8/8
 */
@Component
public class CustomUserDetailsManager implements UserDetailsManager {

    @Autowired
    SysUserDao sysUserDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserDao.findByUsername(username);
        if(sysUser == null){
            throw new UsernameNotFoundException("用户不存在");
        }
        return User.withUsername(sysUser.getUsername())
                .password("{noop}" + sysUser.getPassword())
                .build();
    }

    @Override
    public void createUser(UserDetails user) {

    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }

}
```

测试 Controller 

```
/* Detailed source-code truncated for AI context efficiency. */
```

== JWT

测试 Controller 
``` java
@RestController
@RequestMapping("/acc/")
public class LoginController {

	@Autowired
	SysUserDao sysUserDao;

	// 测试登录 
	@RequestMapping("doLogin")
	public AjaxJson doLogin(String username, String password) {
		// 校验
		SysUser user = sysUserDao.findByUsername(username);
		if(user == null) {
			return AjaxJson.getError("用户不存在");
		}
		if(!user.getPassword().equals(password)) {
			return AjaxJson.getError("密码错误");
		}
		// 登录
		String token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2);
		return AjaxJson.getSuccess("登录成功").set("token", token);
	}

	// 查询登录状态 
	@RequestMapping("isLogin")
	public AjaxJson isLogin(HttpServletRequest request) {
		try{
			String token = request.getHeader("token");
			JWT jwt = JwtUtil.parseToken(token);
			return AjaxJson.getSuccess("已登录")
					.set("id", jwt.getPayload("userId"))
					.set("user", jwt.getPayload("user"));
		} catch (Exception e) {
			e.printStackTrace();
			return AjaxJson.getError("未登录");
		}
	}

}
```

:::




### 会话注销

:::tabs
== Sa-Token

``` java 
@RequestMapping("logout")
public AjaxJson logout() {
	StpUtil.logout();
	return AjaxJson.getSuccess("注销成功");
}
```

== Shiro

``` java
@RequestMapping("logout")
public AjaxJson logout() {
	SecurityUtils.getSubject().logout();
	return AjaxJson.getSuccess("注销成功");
}
```

== SpringSecurity

``` java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
	
	// 其它配置 ...
	
	// 注销相关配置
	httpSecurity.logout(logout -> {
		logout.logoutUrl("/acc/logout");
		logout.logoutSuccessHandler((request, response, authentication) -> {
			response.setStatus(200);
			response.setCharacterEncoding("UTF-8");
			response.setContentType("application/json; charset=utf-8");
			String jsonStr = new ObjectMapper().writeValueAsString(AjaxJson.getSuccess("注销成功!"));
			response.getWriter().write(jsonStr);
		});
	});

	return httpSecurity.build();
}
```

== JWT

JWT 无法注销已经颁发的 token 。

:::




### 账号密码登录（MD5 加 salt）

:::tabs
== Sa-Token

测试 Controller 
``` java 
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password) {
	// 校验
	SysUser user = sysUserDao.findByUsername(username);
	if(user == null) {
		return AjaxJson.getError("用户不存在");
	}
	String salt = "abc";
	if(!user.getPassword().equals(SaSecureUtil.md5(salt + password))) {
		return AjaxJson.getError("密码错误");
	}
	// 登录
	StpUtil.login(user.getId());
	StpUtil.getSession().set("user", user);
	return AjaxJson.getSuccess("登录成功");
}
```

== Shiro

自定义 Realm Bean 设定密码凭证器
``` java
@Bean
public MyRealm myRealm() {
	MyRealm realm = new MyRealm();
	// 设定凭证匹配器
	HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
	credentialsMatcher.setHashAlgorithmName("md5");
	realm.setCredentialsMatcher(credentialsMatcher);
	// 返回
	return realm;
}
```

自定义 Realm 实现类 doGetAuthenticationInfo 方法返回 slat 信息
``` java
// 加载用户信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
	String username = (String)token.getPrincipal();
	SysUser sysUser = sysUserDao.findByUsername(username);
	if(sysUser == null){
		return null;
	}

	return new SimpleAuthenticationInfo(
			sysUser,
			sysUser.getPassword(),
			ByteSource.Util.bytes("abc"), // 指定 slat 信息 
			getName()
	);
}
```

登录代码照旧 

== SpringSecurity

CustomUserDetailsManager 的 loadUserByUsername 指定 MD5 算法

``` java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	SysUser sysUser = sysUserDao.findByUsername(username);
	if(sysUser == null){
		throw new UsernameNotFoundException("用户不存在");
	}
	return User.withUsername(sysUser.getUsername())
			.password("{MD5}" + sysUser.getPassword())
			.build();
}
```

登录时指定 salt 

``` java
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password, HttpServletRequest request) {
	try {
		// 验证账号密码
		String salt = "abc";
		UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, salt + password);
		// 其它代码照旧 ... 
		
		// 返回
		return AjaxJson.getSuccess("登录成功!");
	} catch (Exception e) {
		e.printStackTrace();
		return AjaxJson.getError(e.getMessage());
	}
}
```

== JWT

测试 Controller 
``` java 
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password) {
	// 校验
	SysUser user = sysUserDao.findByUsername(username);
	if(user == null) {
		return AjaxJson.getError("用户不存在");
	}
	String salt = "abc";
	if(!user.getPassword().equals(SecureUtil.md5(salt + password))) {
		return AjaxJson.getError("密码错误");
	}
	// 登录
	String token = JwtUtil.createToken(user.getId(), user, 60 * 60 * 2);
	return AjaxJson.getSuccess("登录成功").set("token", token);
}
```

:::




### 从上下文获取当前登录 User 信息
:::tabs
== Sa-Token

``` java
// 从上下文获取当前登录 User 信息 
@RequestMapping("getCurrUser")
public AjaxJson getCurrUser() {
	return AjaxJson.getSuccess()
			.set("id", StpUtil.getLoginId())
			.set("user", StpUtil.getSession().get("user"));
}
```

== Shiro

``` java
// 从上下文获取当前登录 User 信息 
@RequestMapping("getCurrUser")
public AjaxJson getCurrUser() {
	Subject subject = SecurityUtils.getSubject();
	SysUser sysUser = (SysUser)subject.getPrincipal();
	return AjaxJson.getSuccess()
			.set("id", sysUser.getId())
			.set("user", sysUser);
}
```

== SpringSecurity

``` java
// 从上下文获取当前登录 User 信息 
@RequestMapping("getCurrUser")
public AjaxJson getCurrUser() {
	Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
	if(!(authentication instanceof AnonymousAuthenticationToken)) {
		SysUser sysUser = (SysUser)authentication.getDetails();
		return AjaxJson.getSuccess()
						.set("id", sysUser.getId())
						.set("user", sysUser);
	}
	return AjaxJson.getError("未登录");
}
```

== JWT

``` java
// 从上下文获取当前登录 User 信息 
@RequestMapping("getCurrUser")
public AjaxJson getCurrUser(HttpServletRequest request) {
	try{
		String token = request.getHeader("token");
		JWT jwt = JwtUtil.parseToken(token);
		SysUser sysUser = jwt.getPayloads().get("user", SysUser.class);
		return AjaxJson.getSuccessData(sysUser);
	} catch (Exception e) {
		e.printStackTrace();
		return AjaxJson.getError("未登录");
	}
}
```

:::




### 从会话上下文上存取值
:::tabs
== Sa-Token

``` java
// 测试从从会话上下文存取值 
@RequestMapping("testSession")
public AjaxJson test() {
	SaSession session = StpUtil.getSession();

	System.out.println("从 session 上取值：" + session.get("name"));
	session.set("name", "zhang");
	System.out.println("从 session 上取值：" + session.get("name"));

	return AjaxJson.getSuccess();
}
```

== Shiro

``` java
// 测试从从会话上下文存取值
@RequestMapping("testSession")
public AjaxJson test() {
	Subject subject = SecurityUtils.getSubject();
	Session session = subject.getSession();

	System.out.println("从 session 上取值：" + session.getAttribute("name"));
	session.setAttribute("name", "zhang");
	System.out.println("从 session 上取值：" + session.getAttribute("name"));

	return AjaxJson.getSuccess();
}
```

== SpringSecurity

``` java
// 测试从从会话上下文存取值
@RequestMapping("testSession")
public AjaxJson testSession(HttpServletRequest request) {
	HttpSession session = request.getSession();

	System.out.println("从 session 上取值：" + session.getAttribute("name"));
	session.setAttribute("name", "zhang");
	System.out.println("从 session 上取值：" + session.getAttribute("name"));
	return AjaxJson.getSuccess();
}
```

== JWT

无

:::







### 角色认证 & 权限认证

:::tabs
== Sa-Token

自定义 StpInterface 实现类 

``` java
@Component
public class StpInterfaceImpl implements StpInterface {

	// 加载角色信息
	@Override
	public List<String> getRoleList(Object loginId, String loginType) {
		return Arrays.asList("admin", "super-admin", "ceo");
	}

	// 加载权限信息
	@Override
	public List<String> getPermissionList(Object loginId, String loginType) {
		return Arrays.asList("user:add", "user:delete", "user:update");
	}

}
```

测试 Controller 
``` java
@RestController
@RequestMapping("/jur/")
public class JurController {

	// 角色判断  ---- http://localhost:8082/jur/assertRole
	@RequestMapping("assertRole")
	public AjaxJson assertRole() {
		// is 模式，返回 true 或 false
		System.out.println("单个角色判断：" + StpUtil.hasRole("admin"));
		System.out.println("多个角色判断(and)：" + StpUtil.hasRoleAnd("admin", "dev-admin"));
		System.out.println("多个角色判断(or)：" + StpUtil.hasRoleOr("admin", "dev-admin"));

		// check 模式，无角色时抛出异常
		StpUtil.checkRole("admin");  // 单个 check
		StpUtil.checkRoleAnd("admin", "dev-admin"); // 多个 check (and)
		StpUtil.checkRoleOr("admin", "dev-admin"); // 多个 check (or)

		return AjaxJson.getSuccess();
	}

	// 权限判断  ---- http://localhost:8082/jur/assertPermission
	@RequestMapping("assertPermission")
	public AjaxJson assertPermission() {
		// is 模式，返回 true 或 false
		System.out.println("单个权限判断：" + StpUtil.hasPermission("user:add"));
		System.out.println("多个权限判断(and)：" + StpUtil.hasPermissionAnd("user:add", "user:delete22"));
		System.out.println("多个权限判断(or)：" + StpUtil.hasPermissionOr("user:add", "user:delete22"));

		// check 模式，无权限时抛出异常
		StpUtil.checkPermission("user:add");  // 单个 check
		StpUtil.checkPermissionAnd("user:add", "user:delete22"); // 多个 check (and)
		StpUtil.checkPermissionOr("user:add", "user:delete22"); // 多个 check (or)

		return AjaxJson.getSuccess();
	}

}
```

== Shiro

自定义 Realm 里重写方法 doGetAuthorizationInfo

``` java
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
	SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
	// 加载角色信息
	authorizationInfo.addRoles(Arrays.asList("admin", "super-admin", "ceo"));
	// 加载权限信息
	authorizationInfo.addStringPermissions(Arrays.asList("user:add", "user:delete", "user:update"));
	return authorizationInfo;
}
```

测试 Controller 
``` java
@RestController
@RequestMapping("/jur/")
public class JurController {

	// 角色判断 
	@RequestMapping("assertRole")
	public AjaxJson assertRole() {
		Subject subject = SecurityUtils.getSubject();

		// is 模式，返回 true 或 false
		System.out.println("单个角色判断：" + subject.hasRole("admin"));
		System.out.println("多个角色判断(and)：" + subject.hasAllRoles(Arrays.asList("admin", "dev-admin")));
		System.out.println("多个角色判断(or)：" + (subject.hasRole("admin") || subject.hasRole("dev-admin")));

		// check 模式，无角色时抛出异常
		subject.checkRole("admin");  // 单个 check
		subject.checkRoles("admin", "dev-admin"); // 多个 check (and)

		return AjaxJson.getSuccess();
	}

	// 权限判断 
	@RequestMapping("assertPermission")
	public AjaxJson assertPermission() {
		Subject subject = SecurityUtils.getSubject();

		// is 模式，返回 true 或 false
		System.out.println("单个权限判断：" + subject.isPermitted("user:add"));
		System.out.println("多个权限判断(and)：" + subject.isPermittedAll("user:add", "user:delete22"));
		System.out.println("多个权限判断(or)：" + (subject.isPermitted("user:add") || subject.isPermitted("user:delete22")));

		// check 模式，无权限时抛出异常
		subject.checkPermission("user:add");  // 单个 check
		subject.checkPermissions("user:add", "user:delete22"); // 多个 check (and)

		return AjaxJson.getSuccess();
	}

}
```

== SpringSecurity

CustomUserDetailsManager 的 loadUserByUsername 里返回用户的 角色 或 权限 信息
``` java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	SysUser sysUser = sysUserDao.findByUsername(username);
	if(sysUser == null){
		throw new UsernameNotFoundException("用户不存在");
	}

    // 不可以同时返回 roles 和 authorities，因为会相互覆盖，SpringSecurity 源码有bug
	return User.withUsername(sysUser.getUsername())
			.password("{noop}" + sysUser.getPassword())
			// .roles("admin", "super-admin", "ceo")
			.authorities("user:add", "user:delete", "user:update")
			.build();
}
```

测试 Controller 
``` java
@RestController
@RequestMapping("/jur/")
public class JurController {

	// 角色判断  
	@RequestMapping("assertRole")
	public AjaxJson assertRole() {
		SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};

		System.out.println("单个角色判断：" + securityExpressionRoot.hasRole("admin"));
		System.out.println("多个角色判断(and)：" + (securityExpressionRoot.hasRole("admin") && securityExpressionRoot.hasRole("dev-admin")));
		System.out.println("多个角色判断(or)：" + securityExpressionRoot.hasAnyRole("admin", "dev-admin"));

		return AjaxJson.getSuccess();
	}

	// 权限判断 
	@RequestMapping("assertPermission")
	public AjaxJson assertPermission() {
		SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {};

		System.out.println("单个权限判断：" + securityExpressionRoot.hasAuthority("user:add"));
		System.out.println("多个权限判断(and)：" + (securityExpressionRoot.hasAuthority("user:add") && securityExpressionRoot.hasAuthority("user:delete2")));
		System.out.println("多个权限判断(or)：" + securityExpressionRoot.hasAnyAuthority("user:add", "user:delete2"));

		return AjaxJson.getSuccess();
	}

}
```

== JWT

无

:::





### 注解鉴权

:::tabs
== Sa-Token

SaTokenConfigure 配置注解拦截器
``` java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
	}
}
```

测试 Controller 
``` java
@RestController
@RequestMapping("/at-check/")
public class AtCheckController {

    // 登录校验 
    @SaCheckLogin
    @RequestMapping("checkLogin")
    public AjaxJson checkLogin() {
        return AjaxJson.getSuccess();
    }

    // 角色校验 
    @SaCheckRole("admin")
    @RequestMapping("checkRole")
    public AjaxJson checkRole() {
        return AjaxJson.getSuccess();
    }

    // 权限校验 
    @SaCheckPermission("user:add")
    @RequestMapping("checkPermission")
    public AjaxJson checkPermission() {
        return AjaxJson.getSuccess();
    }

    // 忽略认证校验 
    @SaIgnore
    @SaCheckLogin
    @RequestMapping("ignoreCheck")
    public AjaxJson ignoreCheck() {
        return AjaxJson.getSuccess();
    }

}
```

== Shiro

测试 Controller 
``` java
@RestController
@RequestMapping("/at-check/")
public class AtCheckController {

    // 登录校验 
    @RequiresAuthentication
    @RequestMapping("checkLogin")
    public AjaxJson checkLogin() {
        return AjaxJson.getSuccess();
    }

    // 角色校验  
    @RequiresRoles("admin")
    @RequestMapping("checkRole")
    public AjaxJson checkRole() {
        return AjaxJson.getSuccess();
    }

    // 权限校验 
    @RequiresPermissions("user:add")
    @RequestMapping("checkPermission")
    public AjaxJson checkPermission() {
        return AjaxJson.getSuccess();
    }

}
```

== SpringSecurity

`SpringSecurityConfigure` 配置类加上 `@EnableMethodSecurity` 注解
``` java
@Configuration
@EnableMethodSecurity
public class SpringSecurityConfigure {
	// ...
}
```

测试 Controller 
``` java
@RestController
@RequestMapping("/at-check/")
public class AtCheckController {

    // 登录校验  
    @PreAuthorize("isAuthenticated()")
    @RequestMapping("checkLogin")
    public AjaxJson checkLogin() {
        return AjaxJson.getSuccess();
    }

    // 角色校验  
    @PreAuthorize("hasRole('admin')")
    @RequestMapping("checkRole")
    public AjaxJson checkRole() {
        return AjaxJson.getSuccess();
    }

    // 权限校验 
    @PreAuthorize("hasAuthority('user:add')")
    @RequestMapping("checkPermission")
    public AjaxJson checkPermission() {
        return AjaxJson.getSuccess();
    }

}
```

== JWT

无

:::




### 路由拦截鉴权
:::tabs
== Sa-Token

SaTokenConfigure 配置 
``` java
@Override
public void addInterceptors(InterceptorRegistry registry) {
	// 注册 Sa-Token 拦截器打开注解鉴权功能 
	registry.addInterceptor(new SaInterceptor(handle -> {
		SaRouter.match("/route-check/getInfo1").stop(); // 不拦截
		SaRouter.match("/route-check/getInfo2").check(r -> StpUtil.checkLogin()); // 需要登录
		SaRouter.match("/route-check/getInfo3").check(r -> StpUtil.checkRole("admin2")); // 需要角色
		SaRouter.match("/route-check/getInfo4").check(r -> StpUtil.checkPermission("user:add3")); // 需要权限
	})).addPathPatterns("/**");
}
```

== Shiro

过滤器配置 
``` java
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
	ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
	bean.setSecurityManager(securityManager());

	// 路由拦截鉴权
	Map<String,String> filterMap = new LinkedHashMap<>();
	filterMap.put("/route-check/getInfo", "anon"); // 不拦截
	filterMap.put("/route-check/getInfo2", "authc"); // 需要登录
	filterMap.put("/route-check/getInfo3", "perms[admin2]"); // 需要角色
	filterMap.put("/route-check/getInfo4", "perms[user:add3]"); // 需要权限
	bean.setFilterChainDefinitionMap(filterMap);
	bean.setLoginUrl("/401");  // 未登录时跳转的 url
	bean.setUnauthorizedUrl("/403");  // 未授权时跳转的 url

	return bean;
}
```

== SpringSecurity

SpringSecurityConfigure 配置 

``` java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
	// 定义安全请求拦截规则
	httpSecurity.authorizeHttpRequests(router -> {
		router
			.requestMatchers("/route-check/getInfo1").permitAll()    // 不拦截
			.requestMatchers("/route-check/getInfo2").authenticated()    // 需要登录
			.requestMatchers("/route-check/getInfo3").hasRole("admin")    // 需要 admin 角色
			.requestMatchers("/route-check/getInfo4").hasAuthority("user:add")    // 需要 user:add 权限
			.anyRequest().permitAll(); // 所有请求都放行
		});

	return httpSecurity.build();
}
```

== JWT

无

:::




### 鉴权未通过的处理方案
:::tabs
== Sa-Token

定义全局异常处理类 
``` java
@RestControllerAdvice
public class GlobalException {

	@ExceptionHandler(NotLoginException.class)
	public AjaxJson handlerException(NotLoginException e) {
		return AjaxJson.get(401, "未登录");
	}

	@ExceptionHandler(NotRoleException.class)
	public AjaxJson handlerException(NotRoleException e) {
		return AjaxJson.get(403, "缺少角色：" + e.getRole());
	}

	@ExceptionHandler(NotPermissionException.class)
	public AjaxJson handlerException(NotPermissionException e) {
		return AjaxJson.get(403, "缺少权限：" + e.getPermission());
	}

}
```

== Shiro

过滤器配置 
``` java
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
	ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
	
	// ... 
	
	bean.setLoginUrl("/401");  // 未登录时跳转的 url
	bean.setUnauthorizedUrl("/403");  // 未授权时跳转的 url

	return bean;
}
```

定义路由 
``` java
@RestController
public class ShiroErrorController {

    @RequestMapping("/401")
    public Object error401(HttpServletRequest request, HttpServletResponse response) {
        response.setStatus(200);
        return AjaxJson.get(401, "not login");
    }

    @RequestMapping("/403")
    public Object error403(HttpServletRequest request, HttpServletResponse response) {
        response.setStatus(200);
        return AjaxJson.get(403, "鉴权未通过");
    }

}
```

== SpringSecurity

实现 `AccessDeniedHandler`, `AuthenticationEntryPoint` 接口

``` java
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable {

    // 未登录异常
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        //验证为未登陆状态会进入此方法，认证错误
        response.setStatus(401);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        String body = "请先进行登录";
        printWriter.write(body);
        printWriter.flush();
    }

    // 权限不足
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        // 登陆状态下，权限不足执行该方法
        response.setStatus(200);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        String body = "权限不足";
        printWriter.write(body);
        printWriter.flush();
    }
}
```

注入 `SecurityFilterChain`

``` java
// 未登录处理逻辑、权限不足处理逻辑
@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
	
	// 异常处理
	httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
		// 权限不足处理方案
		httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler);
		// 未登录 处理逻辑
		httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler);
	});

	return httpSecurity.build();
}
```

== JWT

使用 `try-catch` 捕获，或定义全局异常处理
``` java
@RestControllerAdvice
public class GlobalException {
	// 全局异常拦截（拦截项目中的所有异常）
	@ExceptionHandler
	public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {

		// 打印堆栈，以供调试
		System.out.println("全局异常---------------");
		e.printStackTrace();

		// 返回给前端
		return AjaxJson.getError(e.getMessage());
	}
}
```

:::




### 和 Thymeleaf 集成

:::tabs
== Sa-Token

`pom.xml` 依赖 
``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-dialect-thymeleaf</artifactId>
	<version>1.39.0</version>
</dependency>
```

`SaTokenConfigure` 增加配置 `Sa-Token` 标签方言对象 

``` java
// Sa-Token 标签方言 (Thymeleaf版)
@Bean
public SaTokenDialect getSaTokenDialect() {
	return new SaTokenDialect();
}
```

新建 `ThymeleafConfigure` 注入全局变量
``` java
@Configuration
public class ThymeleafConfigure {
    // 为 Thymeleaf 注入全局变量，以便在页面中调用 Sa-Token 的方法
    @Autowired
    public void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) {
        viewResolver.addStaticVariable("stp", StpUtil.stpLogic);
    }
}
```

新建 `Controller` 
``` java
@Controller
public class HomeController {
    @RequestMapping("/")
    public Object index(HttpServletRequest request) {
        request.setAttribute("isLogin", StpUtil.isLogin());
        return new ModelAndView("index.html");
    }
}
```
	
新建 `templates/index.html`
``` html
<!DOCTYPE html>
<html lang="zh" xmlns:sa="http://www.thymeleaf.org/extras/sa-token">
<head>
    <title>Sa-Token 集成 Thymeleaf 标签方言</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div class="view-box" style="padding: 30px;">
    <h2>Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面</h2>
    <p>当前是否登录：<span th:text="${stp.isLogin()}"></span></p>
    <p>
        <a href="login" target="_blank">登录</a>
        <a href="logout" target="_blank">注销</a>
    </p>

    <p>登录之后才能显示：<span sa:login>value</span></p>
    <p>不登录才能显示：<span sa:notLogin>value</span></p>

    <p>具有角色 admin 才能显示：<span sa:hasRole="admin">value</span></p>
    <p>同时具备多个角色才能显示：<span sa:hasRoleAnd="admin, ceo, cto">value</span></p>
    <p>只要具有其中一个角色就能显示：<span sa:hasRoleOr="admin, ceo, cto">value</span></p>
    <p>不具有角色 admin 才能显示：<span sa:notRole="admin">value</span></p>

    <p>具有权限 user-add 才能显示：<span sa:hasPermission="user-add">value</span></p>
    <p>同时具备多个权限才能显示：<span sa:hasPermissionAnd="user-add, user-delete, user-get">value</span></p>
    <p>只要具有其中一个权限就能显示：<span sa:hasPermissionOr="user-add, user-delete, user-get">value</span></p>
    <p>不具有权限 user-add 才能显示：<span sa:notPermission="user-add">value</span></p>

    <p th:if="${stp.isLogin()}">
        从SaSession中取值：
        <span th:text="${stp.getSession().get('name', '')}"></span>
    </p>

</div>
</body>
</html>
```

== Shiro

`pom.xml` 依赖 
``` xml

<dependency>
	<groupId>com.github.theborakompanioni</groupId>
	<artifactId>thymeleaf-extras-shiro</artifactId>
	<version>2.1.0</version>
</dependency>
```

`ShiroConfigure` 增加配置 `Shiro` 方言对象 
``` java
@Bean
public ShiroDialect shiroDialect() {
	return new ShiroDialect();
}
```

新建 `Controller` 
``` java
@Controller
public class HomeController {
    @RequestMapping("/")
    public Object index(HttpServletRequest request) {
        Subject subject = SecurityUtils.getSubject();
        request.setAttribute("isLogin", subject.isAuthenticated());
        return new ModelAndView("index.html");
    }
}
```

新建 `templates/index.html`

```
/* Detailed source-code truncated for AI context efficiency. */
```

== SpringSecurity

`pom.xml` 引入依赖 
``` xml

<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity6</artifactId>
	<version>3.1.2.RELEASE</version>
</dependency>
```
		
新建 `Controller` 
``` java
@RestController
public class HomeController {
    // 首页  
    @RequestMapping("/")
    public Object index(HttpServletRequest request) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        request.setAttribute("isLogin", !(authentication instanceof AnonymousAuthenticationToken));
        return new ModelAndView("index.html");
    }
}
```

新建 `templates/index.html`

```
/* Detailed source-code truncated for AI context efficiency. */
```

== JWT

无

:::




### 前后端分离
:::tabs
== Sa-Token

1、在登录时，将 token 信息返回到前端 
``` java
// 测试登录 
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password) {
	// 校验
	SysUser user = sysUserDao.findByUsername(username);
	// user 信息校验代码不再赘述 ... 
	
	// 登录
	StpUtil.login(user.getId());
	StpUtil.getSession().set("user", user);
	return AjaxJson.getSuccess("登录成功").set("satoken", StpUtil.getTokenValue());  // ⚠️ 关键代码 
}
```

2、前端改造
- 1、在登录请求时，将返回的 token 保存到本地 `localStorage.setItem('satoken', res.satoken)`。
- 2、在后续每次请求中，读取本地保存的 satoken 塞到请求 header 中

``` js
const header = {};
if(localStorage.satoken) {
	header.satoken = localStorage.satoken;
}
// 后续提交请求...
```

== Shiro

1、自定义 SessionManager，从请求 header 里读取前端提交的 token
``` java
public class MySessionManager extends DefaultWebSessionManager {

    private static final String TOKEN = "token";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public MySessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(TOKEN);
        // 如果请求头中有 token 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}
```

2、注入到 SecurityManager 中 
``` java
@Configuration
public class ShiroConfigure {

	// 省略其它次要代码 ... 

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm());
        manager.setSessionManager(sessionManager());
        return manager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager());
        return bean;
    }

    // 自定义sessionManager
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        return mySessionManager;
    }
}
```

3、测试 Controller，登录时将 token 信息返回到前端 
``` java
// 测试登录 
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password) {
	Subject subject = SecurityUtils.getSubject();
	try {
		subject.login(new UsernamePasswordToken(username, password));
		String token = subject.getSession().getId().toString();    // ⚠️ 关键代码
		return AjaxJson.getSuccess("登录成功!").set("token", token);    // ⚠️ 关键代码
	} catch (AuthenticationException e) {
		e.printStackTrace();
		return AjaxJson.getError(e.getMessage());
	}
}
```

4、前端改造
- 1、在登录请求时，将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。
- 2、在后续每次请求中，读取本地保存的 token 塞到请求 header 中

``` js
const header = {};
if(localStorage.token) {
	header.token = localStorage.token;
}
// 后续提交请求...
```

== SpringSecurity

见下方 “集成 Redis” 部分，同时做到：集成 Redis + 前后端分离。

== JWT

`JWT` 不依赖 `Cookie` 保存/传输 token，因此无需特殊定制即可原生支持前后端分离模式。

:::




### 集成 Redis

:::tabs
== Sa-Token

pom.xml 引入依赖
``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>1.39.0</version>
</dependency>


<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>
```

application.yml 新增连接配置 
``` yaml
spring:
    data:
        # redis配置
        redis:
            # Redis数据库索引（默认为0）
            database: 1
            # Redis服务器地址
            host: 127.0.0.1
            # Redis服务器连接端口
            port: 6379
            # Redis服务器连接密码（默认为空）
            password:
            # 连接超时时间
            timeout: 10s
            lettuce:
                pool:
                    # 连接池最大连接数
                    max-active: 200
                    # 连接池最大阻塞等待时间（使用负值表示没有限制）
                    max-wait: -1ms
                    # 连接池中的最大空闲连接
                    max-idle: 10
                    # 连接池中的最小空闲连接
                    min-idle: 0
```

其它代码照旧

== Shiro

pom.xml 引入依赖 

``` xml

<dependency>
	<groupId>org.crazycake</groupId>
	<artifactId>shiro-redis</artifactId>
	<version>3.3.1</version>
</dependency>
```

application.yml 新增连接配置 
``` yaml

spring:
    redis:
        shiro:
            # Redis服务器地址
            host: 127.0.0.1:6379
            # Redis服务器连接密码（默认为空）
            password:
            # Redis数据库索引（默认为0）
            database: 2
            # 连接超时时间
            timeout: 1800
        
```


ShiroConfigure 注入相关 Bean 
```
/* Detailed source-code truncated for AI context efficiency. */
```

SysUser 实体类要实现 Serializable 接口
``` java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysUser implements Serializable {
	// ... 
}
```

其它代码照旧 

== SpringSecurity

（结合上部分，同时做到集成 Redis + 前后端分离）

1、`pom.xml` 引入依赖 
``` xml

<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```

2、`yml` 增加配置
``` yml
spring:
    session:
        store-type: redis
        timeout: 8H
        redis:
            namespace: spring:session

    data:
        # redis配置
        redis:
            # Redis数据库索引（默认为0）
            database: 3
            # Redis服务器地址
            host: 127.0.0.1
            # Redis服务器连接端口
            port: 6379
            # Redis服务器连接密码（默认为空）
            password:
            # 连接超时时间
            timeout: 10s
            lettuce:
                pool:
                    # 连接池最大连接数
                    max-active: 200
                    # 连接池最大阻塞等待时间（使用负值表示没有限制）
                    max-wait: -1ms
                    # 连接池中的最大空闲连接
                    max-idle: 10
                    # 连接池中的最小空闲连接
                    min-idle: 0
```

3、在 `CustomAccessDeniedHandler` 自定义认证异常处理类中，返回 `json` 格式数据

``` java
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable {

    // 未登录异常
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        //验证为未登陆状态会进入此方法，认证错误
        response.setStatus(401);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        String body = new ObjectMapper().writeValueAsString(AjaxJson.get(401, "请先进行登录"));
        printWriter.write(body);
        printWriter.flush();
    }

    // 权限不足
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        // 登陆状态下，权限不足执行该方法
        response.setStatus(200);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        String body = new ObjectMapper().writeValueAsString(AjaxJson.get(403, "权限不足"));
        printWriter.write(body);
        printWriter.flush();
    }
    
}
```

4、别忘了注入到 `SecurityFilterChain` 过滤器链 
``` java
 // 异常处理
httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
	// 权限不足处理方案
	httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler);
	// 未登录 处理逻辑
	httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler);
});
```

5、在登录时，返回对应 token 信息
``` java
// 测试登录 
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password, HttpServletRequest request) {
	try {
		// 验证账号密码
		UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
		usernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username));
		Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
		// 存入上下文
		SecurityContextHolder.getContext().setAuthentication(authentication);
		request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
		// 返回
		String token = request.getSession().getId();
		return AjaxJson.getSuccess("登录成功!").set("token", token);
	} catch (Exception e) {
		e.printStackTrace();
		return AjaxJson.getError(e.getMessage());
	}
}
```

6、前端改造
- 1、在登录请求时，将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。
- 2、在后续每次请求中，读取本地保存的 token 塞到请求 header 中

``` js
const header = {};
if(localStorage.token) {
	header.token = localStorage.token;
}
// 后续提交请求...
```

7、新建 `HttpSessionConfigure` 配置重写 `HttpSessionId` 读取策略，改为从 `header` 头读取 `token` 参数作为 `SessionId`
``` java
@Configuration
public class HttpSessionConfigure {
    // HttpSession 读取策略，从 header 头读取 token 参数作为 session id
    @Bean
    public HeaderHttpSessionIdResolver httpSessionStrategy() {
        System.out.println("----------------- 自定义 HttpSession Id 读取方式");
        return new HeaderHttpSessionIdResolver("token");
    }
}
```

== JWT

无

:::

---

### Sa Token Doc New Docs Fun Cors Filter (`sa-token-doc-new/docs/fun/cors-filter.md`)

---
title: "Sa-Token 解决跨域问题"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,解决跨域问题,附录"
description: "Sa-Token 解决跨域问题：前后端分离时 CORS 需与鉴权过滤器协同，否则浏览器会拦跨域响应。本页按参考1至3给出三篇外部实践（掘金一篇、微信公众号两篇），打开链接对照跨域头、预检与过滤器顺序，本页不重复贴配置代码。三篇均为社区实践而非框架内置示例，建议打开后按文章步骤改过滤器顺序与跨域响应头。"
---

# 解决跨域问题 



参考1: [https://juejin.cn/post/7491603065944129590](https://juejin.cn/post/7491603065944129590)

参考2: [https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g](https://mp.weixin.qq.com/s/tbqjCKrTMj-l1lZbeyu81g)

参考3: [https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg](https://mp.weixin.qq.com/s/8aziIhqGCb_qsr8kLiqzmg)

---

### Sa Token Doc New Docs Fun Curr Domain (`sa-token-doc-new/docs/fun/curr-domain.md`)

---
title: "Sa-Token 解决反向代理 uri 丢失的问题"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,解决反向代理 uri 丢失的问题,附录"
description: "Sa-Token 反向代理 URI 丢失：Nginx 转发后 getUrl() 会偏址，SSO 等模块依赖该 API。可追加 Public-Network-URL 并重写 SaRequest.getUrl()，或配置 sa-token.curr-domain；header 方案两步须同步，防止前端假传。"
---

# 解决反向代理 uri 丢失的问题

--- 

使用 `request.getRequestURL()` 可获取当前程序所在外网的访问地址，在 Sa-Token 中，其 `SaHolder.getRequest().getUrl()` 也正是借助此API完成，
有很多模块都用到了这个能力，比如SSO单点登录。

我们可以使用如下代码测试此API
``` java
// 显示当前程序所在外网的都访问地址
@RequestMapping("test")
public String test() {
	return "您访问的是：" + SaHolder.getRequest().getUrl();
}
```

从浏览器访问此接口，我们可以看到：

<img class="s-w-sh" src="/big-file/doc/fun/test-curr-domain.png" alt="本地访问时 SaHolder.getRequest().getUrl() 返回结果" />

此 API 在本地开发时一般可以正常工作，然而如果我们在部署时使用 Nginx 做了一层反向代理后，其最终结果可能会和我们预想的有一点偏差：

<img class="s-w-sh" src="/big-file/doc/fun/test-curr-domain-fxdl.png" alt="Nginx 反代后 URI 丢失的 getUrl() 返回结果" />

不仅是 Nginx，所有包含路由转发的地方都有可能导致上述丢失 uri 的现象，解决方案也很简单，既然程序无法自动识别，我们改成手动获取即可，Sa-Token 提供两个方案：


### 方案一：Nginx转发时追加 header 参数

##### 1、首先在 Nginx 代理转发的地方增加参数

<img class="s-w-sh" src="/big-file/doc/fun/nginx-add-header.png" alt="Nginx 配置 proxy_set_header 传递公网 URL" />

重点是这一句：`proxy_set_header Public-Network-URL http://$http_host$request_uri;`

##### 2、在程序中重写 SaRequest 创建策略，自定义获取 uri 的逻辑

``` java
@Configuration
public class SaTokenConfigure {

	@PostConstruct
	public void configSaStrategy() {
		SaStrategy.instance.setCreateSaRequest(source -> {
			HttpServletRequest request = (HttpServletRequest) source;
			return new SaRequestForServlet(request) {
				@Override
				public String getUrl() {
					String publicUrl = request.getHeader("Public-Network-URL");
					if (publicUrl != null) {
						return publicUrl;
					}
					return super.getUrl();
				}
			};
		});
	}

}
```

其它逻辑保持不变，框架即可正确获取 uri 地址

::: danger 风险警告
注意：步骤一与步骤二需要同步存在，否则可能有前端假传 header 参数造成安全问题 
:::


### 方案二：直接在yml中配置当前项目的网络访问地址

在 `application.yml` 中增加配置：

:::tabs
== yaml 风格

``` yaml
sa-token: 
    # 配置当前项目的网络访问地址
    curr-domain: http://local.dev33.cn:8902/api
```

== properties 风格

``` properties
# 配置当前项目的网络访问地址
sa-token.curr-domain=http://local.dev33.cn:8902/api
```

:::


即可避免路由转发过程中丢失 uri 的问题

---

### Sa Token Doc New Docs Fun Custom Annotations (`sa-token-doc-new/docs/fun/custom-annotations.md`)

---
title: "Sa-Token 自定义注解"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,自定义注解,附录"
description: "Sa-Token 自定义注解：实现 SaAnnotationHandlerInterface 处理 @CheckAccount 等业务校验。Spring 用 @Component 注册，非 IOC 调 registerAnnotationHandler。也可用 @SaUserCheckLogin 简化多账号鉴权。"
---

# 自定义注解

如果框架内置的注解无法满足你的业务需求，你还可以自定义注解注入到框架中。

---

### 1、自定义注解

假设有以下业务需求

::: info 需求场景
自定义一个注解 `@CheckAccount`，具有 `name`、`pwd` 两个字段，在标注一个方法上时，要求前端必须提交相应的账号密码参数才能访问方法。
:::


#### 1.1、第一步，创建一个注解

``` java
/**
 * 账号校验：在标注一个方法上时，要求前端必须提交相应的账号密码参数才能访问方法。
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface CheckAccount {

    /**
     * 需要校验的账号
     */
    String name();

    /**
     * 需要校验的密码
     */
    String pwd();

}
```

#### 1.2、第二步，创建注解处理器 

实现 `SaAnnotationHandlerInterface` 接口，指定泛型为刚才自定义的注解 

``` java
/**
 * 注解 CheckAccount 的处理器
 */
@Component
public class CheckAccountHandler implements SaAnnotationHandlerInterface<CheckAccount> {

    // 指定这个处理器要处理哪个注解
    @Override
    public Class<CheckAccount> getHandlerAnnotationClass() {
        return CheckAccount.class;
    }

    // 每次请求校验注解时，会执行的方法
    @Override
    public void checkMethod(CheckAccount at, AnnotatedElement element) {
        // 获取前端请求提交的参数
        String name = SaHolder.getRequest().getParamNotNull("name");
        String pwd = SaHolder.getRequest().getParamNotNull("pwd");

        // 与注解中指定的值相比较
        if(name.equals(at.name()) && pwd.equals(at.pwd()) ) {
            // 校验通过，什么也不做
        } else {
            // 校验不通过，则抛出异常
            throw new SaTokenException("账号或密码错误，未通过校验");
        }
    }

}
```

参考上述代码，实现类上指定了 `@Component` 注解，使其可以在 ioc 环境下（如 Spring）被自动扫描注册 Sa-Token 中，
如果你的项目属于非 ioc 环境，则需要手动将其注册到 Sa-Token 框架中：
``` java
SaAnnotationStrategy.instance.registerAnnotationHandler(new CheckAccountHandler());
```

#### 1.3、测试自定义的注解

我们在一个请求接口上指定这个注解，来测试一下效果 

``` java
@RestController
@RequestMapping("/test/")
public class TestController {

	@RequestMapping("test")
	@CheckAccount(name = "sa", pwd = "123456")
	public SaResult test() {
		System.out.println("------------进来了");
		return SaResult.ok(); 
	}
	
}
```

启动项目，使用浏览器访问此接口。

先来个错误的账号密码访问测试一下：[http://localhost:8081/test/test?name=sa&pwd=123](http://localhost:8081/test/test?name=sa&pwd=123)

返回结果：

``` js
{
  "code": 500,
  "msg": "账号或密码错误，未通过校验",
  "data": null
}
```

使用正确账号密码测试访问：[http://localhost:8081/test/test?name=sa&pwd=123456](http://localhost:8081/test/test?name=sa&pwd=123456)

返回结果：

``` js
{
  "code": 200,
  "msg": "ok",
  "data": null
}
```



### 2、使用自定义注解优化多账号鉴权

在之前的 [ 多账号鉴权 ] 章节，我们介绍了利用 “spring 注解处理器” 达到注解合并的目的，从而简化多账号体系下的注解鉴权写法。

此种方案比较简单，但是也有一些缺点。
- 1、强依赖 Spring，无法在非 Spring 环境中使用。
- 2、注解递归检查可能会造成一些性能下降。
- 3、扩展性较低，只能略微简化框架内置好的注解写法，无法灵活扩展功能。

此处我们再演示一种方案，使用自定义注解的方式达到相同的目的。


#### 2.1、首先定义注解

``` java
/**
 * 登录认证(User版)：只有登录之后才能进入该方法 
 * <p> 可标注在函数、类上（效果等同于标注在此类的所有方法上） 
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaUserCheckLogin {
	
}
```

#### 2.2、定义注解处理器
``` java
/**
 * 注解 SaUserCheckLogin 的处理器
 */
@Component
public class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface<SaUserCheckLogin> {

    @Override
    public Class<SaUserCheckLogin> getHandlerAnnotationClass() {
        return SaUserCheckLogin.class;
    }

    @Override
    public void checkMethod(SaUserCheckLogin at, AnnotatedElement element) {
        SaCheckLoginHandler._checkMethod(StpUserUtil.TYPE);
    }

}
```

#### 2.3、使用新注解
接下来就可以使用我们的自定义注解了：

``` java
// 使用 @SaUserCheckLogin 的效果等同于使用：@SaCheckLogin(type = "user")
@SaUserCheckLogin
@RequestMapping("info")
public String info() {
    return "查询用户信息";
}
```

注：其它注解 `@SaCheckRole("xxx")`、`@SaCheckPermission("xxx")` 同理， 完整示例参考 Gitee 代码：
[自定义注解](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/custom_annotation)。

---

### Sa Token Doc New Docs Fun Dynamic Router Check (`sa-token-doc-new/docs/fun/dynamic-router-check.md`)

---
title: "Sa-Token 参考：把路由拦截鉴权动态化"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,参考：把路由拦截鉴权动态化,附录"
description: "Sa-Token 动态路由鉴权：把 excludePaths()、getAuthRules() 放进 SaInterceptor 的 lambda，运行时从库加载登录豁免 path 与 path→权限规则。excludePathPatterns 只在启动时执行一次，写在 lambda 外无法动态化。"
---

# 参考：把路由拦截鉴权动态化

框架提供的示例是硬代码写死的，不过稍微做一下更改，你就可以让他动态化

--- 

参考如下：

``` java
@Override
public void addInterceptors(InterceptorRegistry registry) {
	registry.addInterceptor(new SaInterceptor(handle -> {
		SaRouter
			.match("/**")
			.notMatch(excludePaths())
			.check(r -> StpUtil.checkLogin());
	})).addPathPatterns("/**");
}

// 动态获取哪些 path 可以忽略鉴权 
public List<String> excludePaths() {
	// 此处仅为示例，实际项目你可以写任意代码来查询这些path
	return Arrays.asList("/path1", "/path2", "/path3");
}
```

如果不仅仅是登录校验，还需要鉴权，那也很简单：
``` java
@Override
public void addInterceptors(InterceptorRegistry registry) {
	registry.addInterceptor(new SaInterceptor(handle -> {
		// 遍历校验规则，依次鉴权 
		Map<String, String> rules = getAuthRules();
		for (String path : rules.keySet()) {
			SaRouter.match(path, () -> StpUtil.checkPermission(rules.get(path)));
		}
	})).addPathPatterns("/**");
}

// 动态获取鉴权规则 
public Map<String, String> getAuthRules() {
	// key 代表要拦截的 path，value 代表需要校验的权限 
	Map<String, String> authMap = new LinkedHashMap<>();
	authMap.put("/user/**", "user");
	authMap.put("/admin/**", "admin");
	authMap.put("/article/**", "article");
	// 更多规则 ... 
	return authMap;
}
```


--- 

错误的写法：

``` java
@Override
public void addInterceptors(InterceptorRegistry registry) {
	registry.addInterceptor(new SaInterceptor(handle -> {
		StpUtil.checkLogin();
	}))
	.addPathPatterns("/**")
	.excludePathPatterns(excludePaths());
}

// 动态获取哪些 path 可以忽略鉴权 
public List<String> excludePaths() {
	// 此处仅为示例，实际项目你可以写任意代码来查询这些path
	return Arrays.asList("/path1", "/path2", "/path3");
}
```

错误点：因为 lambda 表达式之外的代码只会在启动时执行一次，所以 `excludePaths()` 方法是无法做到动态化读取的，
若要在项目运行时动态读写，必须把调用 `excludePaths()` 的时机放在 lambda 里。

---

### Sa Token Doc New Docs Fun Exception Code (`sa-token-doc-new/docs/fun/exception-code.md`)

---
title: "Sa-Token 异常细分状态码"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,异常细分状态码,附录"
description: "Sa-Token 异常细分状态码：同类 SaSsoException 可用 e.getCode() 区分，如 30001 无效 redirect、30004 无效 ticket。文内给出 core、Servlet、Spring Boot、Reactor、Solon、SSO、OAuth2、JWT 的 code 对照表。"
---

# 异常细分状态码

--- 

### 获取异常细分状态码

Sa-Token 中的基础异常类是 `SaTokenException`，在此基础上，又针对一些特定场景划分出诸如 `NotLoginException`、`NotPermissionException` 等。

但是框架中异常抛出点远远多于异常种类的划分，比如在 SSO 插件中，[ redirect 重定向地址无效 ] 和 [ ticket 参数值无效 ] 都会导致 SSO 授权的失败，
但是它们抛出的异常都是 `SaSsoException`，如果你需要对这两种异常情形做出不同的处理，仅仅判断异常的 ClassType 显然不够。

为了解决上述需求，Sa-Token 对每个异常抛出点都会指定一个特定的 code 值，就像这样：

``` java
if(SaFoxUtil.isUrl(url) == false) {
	throw new SaSsoException("无效redirect：" + url).setCode(SaSsoErrorCode.CODE_30001);	
}
```

就像是打上一个特定的标记，不同异常情形标记的 code 码值也会不同，这就为你精细化异常处理提供了前提。

要在捕获异常时获取这个 code 码也非常简单：

``` java
@RestControllerAdvice
public class GlobalExceptionHandler {
	@ExceptionHandler(SaTokenException.class)
	public SaResult handlerSaTokenException(SaTokenException e) {
		
		// 根据不同异常细分状态码返回不同的提示 
		if(e.getCode() == 30001) {
			return SaResult.error("redirect 重定向 url 是一个无效地址");
		}
		if(e.getCode() == 30002) {
			return SaResult.error("redirect 重定向 url 不在 allowUrl 允许的范围内");
		}
		if(e.getCode() == 30004) {
			return SaResult.error("提供的 ticket 是无效的");
		}
		// 更多 code 码判断 ... 
		
		// 默认的提示 
		return SaResult.error("服务器繁忙，请稍后重试...");
	}
}
```

SaToken 中的所有异常都是继承于 `SaTokenException` 的，也就是说，所有异常你都可以通过 `e.getCode()` 的方式获取对应的异常细分状态码。





### 异常细分状态码-参照表

#### sa-token-code 核心包

| code码值	| 含义									|
| :--------	| :--------								|
| -1		| 代表这个异常在抛出时未指定异常细分状态码	|
| 10001		| 未能获取有效的上下文处理器				|
| 10002		| 未能获取有效的上下文					|
| 10003		| JSON 转换器未实现						|
| 10011		| 未能从全局 StpLogic 集合中找到对应 type 的 StpLogic					|
| 10021		| 指定的配置文件加载失败					|
| 10022		| 配置文件属性无法正常读取				|
| 10031		| 重置的侦听器集合不可以为空				|
| 10032		| 注册的侦听器不可以为空					|
| 10301		| 提供的 Same-Token 是无效的				|
| 10311		| 表示未能通过 Http Basic 认证校验		|
| 10321		| 提供的 HttpMethod 是无效的				|
| 11001		| 未能读取到有效Token						|
| 11002		| 登录时的账号id值为空					|
| 11003		| 更改 Token 指向的 账号Id 时，账号Id值为空						|
| 11011		| 未能读取到有效Token						|
| 11012		| Token无效 								|
| 11013		| Token已过期							|
| 11014		| Token已被顶下线						|
| 11015		| Token已被踢下线						|
| 11016		| Token已被冻结						|
| 11031		| 在未集成 sa-token-jwt 插件时调用 getExtra() 抛出异常					|
| 11041		| 缺少指定的角色							|
| 11051		| 缺少指定的权限							|
| 11061		| 当前账号未通过服务封禁校验				|
| 11062		| 提供要解禁的账号无效					|
| 11063		| 提供要解禁的服务无效					|
| 11064		| 提供要解禁的等级无效					|
| 11071		| 二级认证校验未通过						|
| 12001		| 请求中缺少指定的参数					|
| 12002		| 构建 Cookie 时缺少 name 参数			|
| 12003		| 构建 Cookie 时缺少 value 参数		|
| 12101		| Base64 编码异常					|
| 12102		| Base64 解码异常					|
| 12103		| URL 编码异常						|
| 12104		| URL 解码异常						|
| 12111		| md5 加密异常						|
| 12112		| sha1 加密异常						|
| 12113		| sha256 加密异常					|
| 12114		| AES 加密异常						|
| 12115		| AES 解密异常						|
| 12116		| RSA 公钥加密异常					|
| 12117		| RSA 私钥加密异常					|
| 12118		| RSA 公钥解密异常					|
| 12119		| RSA 私钥解密异常					|
| 12201		| 参与参数签名的秘钥不可为空			|
| 12202		| 给定的签名无效						|
| 12203		| timestamp 超出允许的范围			|


#### sa-token-servlet

| code码值	| 含义												|
| :--------	| :--------											|
| 20001		| 转发失败											|
| 20002		| 重定向失败											|


#### sa-token-spring-boot-starter

| code码值	| 含义												|
| :--------	| :--------											|
| 20101		| 企图在非 Web 上下文获取 Request、Response 等对象		|
| 20103		| 对象转 JSON 字符串失败								|
| 20104		| JSON 字符串转 Map 失败								|
| 20105		| 默认的 Filter 异常处理函数							|


#### sa-token-reactor-spring-boot-starter

| code码值	| 含义												|
| :--------	| :--------											|
| 20203		| 对象转 JSON 字符串失败								|
| 20204		| JSON 字符串转 Map 失败								|
| 20205		| 默认的 Filter 异常处理函数							|


#### sa-token-solon-plugin

| code码值	| 含义												|
| :--------	| :--------											|
| 20301		| 默认的拦截器异常处理函数							|
| 20302		| 默认的 Filter 异常处理函数							|


#### sa-token-sso 单点登录相关：

| code码值	| 含义												|
| :--------	| :--------											|
| 30001		| `redirect` 重定向 url 是一个无效地址					|
| 30002		| `redirect` 重定向 url 不在 allowUrl 允许的范围内		|
| 30003		| 接口调用方提供的 `secretkey` 秘钥无效					|
| 30004		| 提供的 `ticket` 是无效的								|
| 30005		| 在模式三下，sso-client 调用 sso-server 端 校验ticket接口 时，得到的响应是校验失败	|
| 30006		| 在模式三下，sso-client 调用 sso-server 端 单点注销接口 时，得到的响应是注销失败	|
| 30007		| http 请求调用 提供的 `timestamp` 与当前时间的差距超出允许的范围	|
| 30008		| http 请求调用 提供的 `sign` 无效						|
| 30009		| 本地系统没有配置 `secretkey` 字段						|
| 30010		| 本地系统没有配置 http 请求处理器（参考 [HTTP 请求扩展](/plugin/http-extend)）	|
| 30011		| 该 ticket 不属于当前 client								|


#### sa-token-oauth2 相关：
| code码值	| 含义									|
| :--------	| :--------								|
| 30101		| client_id 不可为空						|
| 30102		| scope 不可为空							|
| 30103		| redirect_uri 不可为空					|
| 30104		| LoginId 不可为空						|
| 30105		| 无效 client_id							|
| 30106		| 无效 access_token						|
| 30107		| 无效 client_token						|
| 30108		| Access-Token 不具备指定的 Scope		|
| 30109		| Client-Token 不具备指定的 Scope		|
| 30110		| 无效 code 码							|
| 30111		| 无效 Refresh-Token						|
| 30112		| 请求的 Scope 暂未签约					|
| 30113		| 无效 redirect_url						|
| 30114		| 非法 redirect_url						|
| 30115		| 无效 client_secret						|
| 30120		| redirect_uri 不一致					|
| 30122		| client_id	不一致						|
| 30125		| 无效 response_type						|
| 30126		| 无效 grant_type						|
| 30127		| 无效 state								|
| 30141		| 系统暂未开放的授权模式					|
| 30142		| 应用暂未开放的授权模式					|
| 30151		| 无效的请求 Method						|
| 30191		| 其它异常								|


#### sa-token-jwt 插件相关：

| code码值	| 含义									|
| :--------	| :--------								|
| 30201		| 对 jwt 字符串解析失败					|
| 30202		| 此 jwt 的签名无效						|
| 30203		| 此 jwt 的 `loginType` 字段不符合预期	|
| 30204		| 此 jwt 已超时							|
| 30205		| 没有配置jwt秘钥						|
| 30206		| 登录时提供的账号id为空					|
| 30207		| extraData 包含 jwt 保留字段				|


#### sa-token-temp-jwt 插件相关：
| code码值	| 含义									|
| :--------	| :--------								|
| 30301		| jwt 模式没有提供秘钥					|
| 30302		| jwt 模式不可以删除 Token				|
| 30303		| Token已超时							|


::: warning 注意
部分插件因异常抛出点较少，暂未做状态码细分处理
:::

---

### Sa Token Doc New Docs Fun Firewall (`sa-token-doc-new/docs/fun/firewall.md`)

---
title: "Sa-Token 防火墙"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,防火墙,附录"
description: "Sa-Token 防火墙 SaFirewallStrategy：默认拦截双斜杠 path 等鉴权绕行。hooks 覆盖白黑名单、危险字符、目录遍历、Host、Method、Header、参数；可 registerHook、removeHook 或 IOC 注入，也可用 checkFailHandle 输出 JSON。"
---

# 防火墙

Sa-Token 内置防火墙组件 `SaFirewallStrategy`，用于拦截一些可能造成攻击的危险请求。
例如当前端提交的 path 为 `/test//login` 时，框架将会强制截断请求，响应以下内容：

``` txt
非法请求：/test//login
```

因为包含双斜杠的 path 请求通常被用于鉴权绕行攻击。类似的拦截规则还有很多， `SaFirewallStrategy` 采用 hooks 机制，允许开发者自由扩展拦截规则，
框架默认具有以下 hook 拦截规则：

- `SaFirewallCheckHookForWhitePath`：请求 path 白名单放行。
- `SaFirewallCheckHookForBlackPath`：请求 path 黑名单校验。
- `SaFirewallCheckHookForPathDangerCharacter`：请求 path 危险字符校。
- `SaFirewallCheckHookForPathBannedCharacter`：请求 path 禁止字符校验。
- `SaFirewallCheckHookForDirectoryTraversal`：请求 path 目录遍历符检测。
- `SaFirewallCheckHookForHost`：Host 检测。
- `SaFirewallCheckHookForHttpMethod`：请求 Method 检测。
- `SaFirewallCheckHookForHeader`：请求头检测。
- `SaFirewallCheckHookForParameter`：请求参数检测。


### 1、默认 hook 配置：

假设我们想要增加请求 path 黑名单，可以使用如下代码：

``` java
@Configuration
public class SaTokenConfigure {
	@PostConstruct
	public void saTokenPostConstruct() {
		SaFirewallCheckHookForBlackPath.instance.resetConfig("/abc");
	}
}
```

现在从浏览器访问 `/abc`，将会被防火墙组件直接拦截：

``` txt
非法请求：/abc
```


除了 `SaFirewallCheckHookForBlackPath` 以外，其它所有 hook 均可通过此方式重载配置，在此暂不冗余演示。


### 2、注册新的 hook 规则：
你可以使用如下代码注册新的 hook 规则：

``` java
@PostConstruct
public void saTokenPostConstruct() {
	// 注册新 hook 演示，拦截所有带有 pwd 参数的请求，拒绝响应 
	SaFirewallStrategy.instance.registerHook((req, res, extArg)->{
		if(req.getParam("pwd") != null) {
			throw new FirewallCheckException("请求中不可包含 pwd 参数");
		}
	});
}
```

除了注册新 hook 规则，你还可以移除默认 hook ，来删减你认为不必要存在的校验规则：

``` java
// 移除指定类型的 hook 验证
SaFirewallStrategy.instance.removeHook(SaFirewallCheckHookForHost.class);
```


### 3、利用自动注入特性注册 hook
如果你的项目属于 IOC 环境（例如 SpringBoot 项目），还可以这样注册 hook：
``` java
// 自定义防火墙校验 hook 
@Component
public class SaFirewallCheckHookForXxx implements SaFirewallCheckHook {
    @Override
    public void execute(SaRequest req, SaResponse res, Object extArg) {
        System.out.println("----------- 自定义防火墙校验 hook ");
    }
}
```


### 4、指定异常处理：

被防火墙拦截的请求不会做出格式化响应，因为通常这些请求为非正常业务请求，只需阻断即可，无需前端依照响应做出页面提示。

如果你的业务切实需要对防火墙拦截做出格式化响应，可以通过以下代码完成：

``` java
@PostConstruct
public void saTokenPostConstruct() {
	// 指定防火墙校验不通过时的处理方案
	SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {
		System.out.println("防火墙校验不通过：" + e.getMessage());
		try {
			HttpServletResponse response = (HttpServletResponse)res.getSource();
			response.setContentType("application/json;charset=UTF-8");
			String resJson = SaResult.error(e.getMessage()).toString();
			response.getWriter().print(resJson);
			response.getWriter().flush();
		} catch (IOException ex) {
			throw new RuntimeException(ex);
		}
	};
}
```

浏览器将得到以下 json 格式响应：

``` js
{
  "code": 500,
  "msg": "非法请求：/abc",
  "data": null
}

---

### Sa Token Doc New Docs Fun Git Pr (`sa-token-doc-new/docs/fun/git-pr.md`)

---
title: "为Sa-Token贡献代码"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,为Sa-Token贡献代码,附录"
description: "为 Sa-Token 贡献代码：文档页底可跳 Gitee/GitHub 在线编辑提交审核；代码需 Fork dromara/sa-token，配 SSH、JDK 8+ 与 Maven，在 IDEA 提交后对 dev 分支提 Pull Request。国内推荐 Gitee（网速与中文界面），主仓用同步按钮更新。"
---

# 如何更新在线文档
1. 打开要修改的文档页面
2. 滑动右侧页面滑块, 查看页面内容最下方, 评论区上方
3. 找到这一行文字
   
<img src="/big-file/doc/fun/online_1.png" alt="在线编辑提示" />

4. 点击Gitee或GitHub按钮中的任意一个, 国内用户推荐使用 [Gitee](https://gitee.com) (请先注册登录后再往下浏览)
5. 此时会进入当前页面源码预览页面,找到下方按钮组

<img src="/big-file/doc/fun/online_2.png" alt="按钮组" />

6. 点击编辑按钮
7. 此时进入待修改页面的源码页面, 按照markdown格式编辑为需要的结果(Ctrl+P可查看最终效果,再次按下可恢复源码界面)
8. 滑动到最下方点击提交审核即可


# 如何提交代码
## 环境安装过程
1. 在本地[下载Git软件](https://pc.qq.com/detail/13/detail_22693.html)并安装
2. 配置用户名和邮件地址(Gitee或GitHub上关联的邮箱)

```
git config --global user.name "这里替换为你在项目中希望展示的昵称"
git config --global user.email 这里替换为你的关联邮箱
// 查看是否配置正确
git config --list  
```

3. 为了让Gitee服务器认可你的身份,需要配置一次SSH Key, 在本地生成密匙对, 公钥上传到Gitee服务器后台
4. 具体方法见[Gitee如何配置SSH](https://gitee.com/help/articles/4181#article-header0), [Github如何配置SSH](https://docs.github.com/cn/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account)
5. 最小开发环境安装包括[Java JDK 8+](https://pc.qq.com/detail/0/detail_18360.html),[Maven 最新版](http://maven.apache.org/download.cgi) 和[idea IDE 社区版](https://www.jetbrains.com/zh-cn/idea/download/#section=windows)
6. 在idea 中[配置Java环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEjava%E7%8E%AF%E5%A2%83)和[配置maven环境](https://www.baidu.com/s?wd=idea%20%E9%85%8D%E7%BD%AEmaven%E7%8E%AF%E5%A2%83), 基础部分不再赘述

## 项目下载过程
1. 点击[Gitee](https://gitee.com/dromara/sa-token)或[Github](https://github.com/dromara/sa-token)进入Sa-Token项目主页, 以下以Gitee为例,Github类似(请先注册登录后再往下浏览)
2. 找到页面右上角的按钮组, 点击Forked按钮
   
<img src="/big-file/doc/fun/code_1.png" alt="按钮组" />

3. 选择个人仓库并点击确认
4. 此时在你的个人仓库中会多了一个Sa-Token项目
5. 在新的Sa-Token项目中, 点击 <img src="/big-file/doc/fun/code_2.png" alt="克隆/下载" /> 按钮, 点击弹出框里面的复制按钮
6.  在本地某空文件夹下右键选择: git bash here

<img src="/big-file/doc/fun/code_4.png" alt="git bash" />

<img src="/big-file/doc/fun/code_3.png" alt="git bash 打开后的图" />

14. 在里面输入如下命令, 按换行后自动下载整个项目

```
git clone 这里替换为复制后的链接
```

## 项目载入过程
1. 下载结束后, 开启 idea, 选择 File->Open... 选中项目下载后的Sa-Token文件夹(Trust Project 相信此项目, 否则不可编辑)
2. 这时项目就是可编辑状态, 修改完代码并测试完成后即可提交

## 项目暂存并提交远程
### 方式一
1. 在idea中打开项目进入Commit选项

<img src="/big-file/doc/fun/code_5.png" alt="本地暂存" />

2. 勾选需要本地暂存的文件
3. 在同一页面的下方输入提示信息

<img src="/big-file/doc/fun/code_6.png" alt="提示信息" />

4. 点击Commit按钮暂存到本地, 点击Commit and Push按钮暂存之后提交到远程
### 方式二
1. 除了点击Commit and Push按钮外,还有一个地方可以提交git

<img src="/big-file/doc/fun/code_7.png" alt="git按钮" />

2. 位置在idea右上方的工具栏里面
3. 指向左下箭头为拉取项目,可以随时更新
4. 打对号为本地暂存
5. 指向右上箭头提交远程
## 私人项目推送到主项目
1. 提交后进入Gitee个人仓库中克隆的Sa-Token项目
2. 找到下图的Pull Request按钮

<img src="/big-file/doc/fun/code_8.png" alt="工具栏" />

3. 点击提交, 进入如下页面

<img class="s-width" src="/big-file/doc/fun/code_9.png" alt="提交信息填写页面" />

4. 在这里,你可以选择要提交的分支,一般都是dev开发分支.可以填写合并信息,其他测试审查之类的可以不填写, 最后点击创建即可完成一次提交.

## 远程项目更新
1. 有时候主项目更新了,之前克隆的项目代码陈旧,如何处理?
2. 在个人仓库的Sa-Token项目主页面中, 找到下图的圆圈

<img src="/big-file/doc/fun/code_10.png" alt="更新按钮" />

3. 点击右侧圆圈按钮后Gitee会自动同步主项目, 这样就不用像我之前一样,删除项目又重新fork了.

## 为什么在国内推荐Gitee
1. 近期Github下载网速较慢
2. Gitee上中文界面方便操作

---

### Sa Token Doc New Docs Fun Issue Template (`sa-token-doc-new/docs/fun/issue-template.md`)

---
title: "Sa-Token issue 提问模板"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,issue 提问模板,附录"
description: "Sa-Token issue 提问模板：提交前先查常见问题并搜已有 issue，勿重复开单；超 24 小时无追问将默认关闭。提供预期不符、Bug、功能提问、新功能建议、踩坑记录五套格式。须写清版本号与复现步骤，代码截图带行号，异常堆栈截全，Ajax 还要带请求地址、请求头和参数。描述不清可对照名词解释组织语句。"
---

# issue 提问模板

在线提问链接：[Gitee issue](https://gitee.com/dromara/sa-token/issues)、[GitHub issue](https://github.com/dromara/sa-token/issues)

::: tip 请在新建 issue 时，尽量复制模板格式进行提交
1. 提交之前率先参考 <a href="/more/common-questions.html" target="_blank">Sa-Token 常见问题解答</a> 以及善用 Gitee issues 搜索功能，查阅问题是否已有答案，已存在的 issue 就不要再重复提交了。
2. 问题已得到处理的 issue 请大家及时手动关闭，如果超过24小时没有追问，我们将默认提交者已找到解决方案，关闭issue。
3. 有时候 issue 提交之后，没有得到及时回复，大家可以加入QQ群@管理员寻求帮助。
4. 请大家新建 issue 时删除不必要的模板信息、精简语句、**做好代码排版**，对于不方便描述的业务场景，可参阅 <a href="/more/noun-intro.html" target="_blank">Sa-Token 名词解释</a> 方便组织语句，这样有助于减低大家的沟通成本。
5. **代码截图要带上行号！报错信息要把异常堆栈截全！页面截图要把地址栏带上！Ajax请求要把请求地址、请求头、请求参数都截全！**
:::


--- 


### 预期不符：
``` js
### 使用版本:


### 涉及的功能模块：


### 测试步骤：
+ 我经过以下步骤测试：

+ 得出以下结果：

+ 其中第 xx 行的代码输出表现 和文档上描述的不一致：

+ 我的理解是：

请问，是我的理解不对，还是文档出了问题？
```


### bug反馈：
``` js
### 使用版本:


### 报错信息：


### 希望结果：


### 复现步骤：


< 备注：如果复现步骤比较复杂，请将 demo 上传到 gitee 并留下地址 >
```


### 功能提问：
``` js
### 对以下问题有疑问:


< 备注：请尽量详细描述问题所在 >
```


### 建议增加新功能：
``` js
### 建议增加的新功能:


### 应用场景阐述：


< 备注：请尽量详细描述功能应用场景 >
```


### 踩坑记录：
``` js
### 遇到的问题:


### 解决方案：


< 备注：请尽量描述详细一点，为后人提供清晰的排查思路，人人为我，我为人人 >
```

---

### Sa Token Doc New Docs Fun Jur Cache (`sa-token-doc-new/docs/fun/jur-cache.md`)

---
title: "Sa-Token 参考：把权限放在缓存里"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,参考：把权限放在缓存里,附录"
description: "Sa-Token 权限缓存参考：StpInterface 默认不缓存，可用 SaTokenDao 把角色与权限写入 Redis，示例 TTL 30 天。推荐缓存「账号id→角色id→权限列表」，不要直接缓存账号权限，否则改一个角色就要清十万账号，容易缓存雪崩。权限缓存模型要跟着 RBAC 走，角色缓存同理。"
---

# 参考：将权限数据放在缓存里
前面我们讲解了如何通过`StpInterface`接口注入权限数据，框架默认是不提供缓存能力的，如果你想减小数据库的访问压力，则需要将权限数据放到缓存中

--- 

参考如下：
``` java
/**
 * 自定义权限验证接口扩展 
 */
@Component  
public class StpInterfaceImpl implements StpInterface {
    
	// 返回一个账号所拥有的权限码集合
	@Override
	@SuppressWarnings("unchecked")
	public List<String> getPermissionList(Object loginId, String loginType) {

		// 1. 声明权限码集合
		List<String> list = new ArrayList<>();

		// 2. 遍历角色列表，查询拥有的权限码
		for (String roleId : getRoleList(loginId, loginType)) {
			List<String> permissionList = (List<String>)SaManager.getSaTokenDao().getObject("satoken:role-find-permission:" + roleId);
			if(permissionList == null) {
				// 从数据库查询这个角色 id 所拥有的权限列表
				permissionList = ...
				// 查好后，set 到缓存中
				SaManager.getSaTokenDao().setObject("satoken:role-find-permission:" + roleId, permissionList, 60 * 60 * 24 * 30);
			}
			list.addAll(permissionList);
		}

		// 3. 返回权限码集合
		return list;
	}

	// 返回一个账号所拥有的角色标识集合
	@Override
	@SuppressWarnings("unchecked")
	public List<String> getRoleList(Object loginId, String loginType) {
		List<String> roleList = (List<String>)SaManager.getSaTokenDao().getObject("satoken:loginId-find-role:" + loginId);
		if(roleList == null) {
			// 从数据库查询这个账号id拥有的角色列表，
			roleList = ... 
			// 查好后，set 到缓存中
			SaManager.getSaTokenDao().setObject("satoken:loginId-find-role:" + loginId, roleList, 60 * 60 * 24 * 30);
		}
		return roleList;
	}
	
}
```

##### 疑问：为什么不直接缓存 `[账号id->权限列表]`的关系，而是 `[账号id -> 角色id -> 权限列表]`？



答：`[账号id->权限列表]`的缓存方式虽然更加直接粗暴，却有一个严重的问题：

- 通常我们系统的权限架构是RBAC模型：权限与用户没有直接的关系，而是：用户拥有指定的角色，角色再拥有指定的权限
- 而这种'拥有关系'是动态的，是可以随时修改的，一旦我们修改了它们的对应关系，便要同步修改或清除对应的缓存数据 

现在假设如下业务场景：我们系统中有十万个账号属于同一个角色，当我们变动这个角色的权限时，难道我们要同时清除这十万个账号的缓存信息吗？
这显然是一个不合理的操作，同一时间缓存大量清除容易引起Redis的缓存雪崩

而当我们采用 `[账号id -> 角色id -> 权限列表]` 的缓存模型时，则只需要清除或修改 `[角色id -> 权限列表]` 一条缓存即可 

一言以蔽之：权限的缓存模型需要跟着权限模型走，角色缓存亦然

---

### Sa Token Doc New Docs Fun Log (`sa-token-doc-new/docs/fun/log.md`)

---
title: "Sa-Token 全局 Log 输出"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,全局 Log 输出,附录"
description: "Sa-Token 全局 Log 输出：yml 设 is-log=true 后，登录、注销、二级认证等关键步骤会打日志，默认打到控制台。实现 SaLog 可转接到 Slf4j；也可在 SaServletFilter.setAuth 里打印请求 path 与提交 token，方便开发调试与排查登录问题。"
---

# 参考：全局 Log 输出

--- 

### 打开全局日志输出

以下配置可以打开全局日志输出：

:::tabs
== yaml 风格

``` yaml
sa-token: 
	# 是否输出操作日志 
	is-log: true
```

== properties 风格

``` properties
# 是否输出操作日志 
sa-token.is-log=true
```

:::



此配置项打开之后，框架将会在账号登录、注销、二级认证 等关键性步骤打印日志，以方便项目开发调试。

框架默认将日志信息打印到控制台，如果需要将日志输出到其它地方，你可以重写 SaLog 对象，例如以下代码将会把日志转接到 Slf4j 下：

``` java
/**
 * 将 Sa-Token log 信息转接到 Slf4j 
 */
@Component
public class SaLogForSlf4j implements SaLog {
	Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class);
	
	@Override
	public void trace(String str, Object... args) {
		log.trace(str, args);
	}
	@Override
	public void debug(String str, Object... args) {
		log.debug(str, args);
	}
	@Override
	public void info(String str, Object... args) {
		log.info(str, args);
	}
	@Override
	public void warn(String str, Object... args) {
		log.warn(str, args);
	}
	@Override
	public void error(String str, Object... args) {
		log.error(str, args);
	}
	@Override
	public void fatal(String str, Object... args) {
		log.error(str, args);
	}
}
```

重新启动项目，观察日志打印变化。

### 增加API访问日志

手动增加 API 请求日志信息，这将非常有助于你调试代码，例如：

``` java
@Bean
public SaServletFilter getSaServletFilter() {
	return new SaServletFilter()
			.addInclude("/**")
			.addExclude("/favicon.ico")
			.setAuth(obj -> {
				// 输出 API 请求日志，方便调试代码 
				SaManager.getLog().debug("----- 请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
				// 其它校验代码... 
			})
			// 异常处理函数：每次认证函数发生异常时执行此函数 
			.setError(e -> {
				System.out.println("---------- sa全局异常 ");
				return SaResult.error(e.getMessage());
			})
			;
}
```

---

### Sa Token Doc New Docs Fun Not Login Scene (`sa-token-doc-new/docs/fun/not-login-scene.md`)

---
title: "Sa-Token 未登录场景值详解"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,未登录场景值详解,附录"
description: "Sa-Token 未登录场景值：NotLoginException.getType() 区分七种情形：-1 未读到 token、-2 无效、-3 过期、-4 被顶、-5 被踢、-6 冻结、-7 未按前缀提交。全局 @ExceptionHandler 可按常量定制提示，覆盖未登录、被踢与被顶下线等不同处理。"
---

# NotLoginException 场景值

本篇介绍如何根据`NotLoginException`异常的场景值，来定制化处理未登录的逻辑 <br/>
应用场景举例：未登录、被顶下线、被踢下线等场景需要不同方式来处理 


## 何为场景值
在前面的章节中，我们了解到，在会话未登录的情况下尝试获取`loginId`会使框架抛出`NotLoginException`异常，而同为未登录异常却有五种抛出场景的区分 

| 场景值   | 对应常量  |  含义说明                         |
|---   	  |---        |---                            |
| -1      | NotLoginException.NOT_TOKEN  	| 未能从请求中读取到有效 token         |
| -2      | NotLoginException.INVALID_TOKEN	| 已读取到 token，但是 token 无效  |
| -3      | NotLoginException.TOKEN_TIMEOUT	| 已读取到 token，但是 token 已经过期 ([详](/fun/token-timeout)) |
| -4      | NotLoginException.BE_REPLACED	| 已读取到 token，但是 token 已被顶下线  |
| -5      | NotLoginException.KICK_OUT		| 已读取到 token，但是 token 已被踢下线  |
| -6      | NotLoginException.TOKEN_FREEZE	| 已读取到 token，但是 token 已被冻结  |
| -7      | NotLoginException.NO_PREFIX		| 未按照指定前缀提交 token		  |



那么，如何获取场景值呢？废话少说直接上代码：


``` java
// 全局异常拦截（拦截项目中的NotLoginException异常）
@ExceptionHandler(NotLoginException.class)
public SaResult handlerNotLoginException(NotLoginException nle)
		throws Exception {

	// 打印堆栈，以供调试
	nle.printStackTrace(); 
	
	// 判断场景值，定制化异常信息 
	String message = "";
	if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
		message = "未能读取到有效 token";
	}
	else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
		message = "token 无效";
	}
	else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
		message = "token 已过期";
	}
	else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
		message = "token 已被顶下线";
	}
	else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
		message = "token 已被踢下线";
	}
	else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {
		message = "token 已被冻结";
	}
	else if(nle.getType().equals(NotLoginException.NO_PREFIX)) {
		message = "未按照指定前缀提交 token";
	}
	else {
		message = "当前会话未登录";
	}
	
	// 返回给前端
	return SaResult.error(message);
}
```

<br/>
注意：以上代码并非处理逻辑的最佳方式，只为以最简单的代码演示出场景值的获取与应用，大家可以根据自己的项目需求来定制化处理

---

### Sa Token Doc New Docs Fun Plugin Dev (`sa-token-doc-new/docs/fun/plugin-dev.md`)

---
title: "Sa-Token 插件开发指南"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,插件开发指南,插件"
description: "Sa-Token 插件开发指南：重写 SaStrategy、替换全局组件或自定义 SaTokenContext 即可扩展。自用加 @Component；对外用 Spring SPI 或 META-INF/satoken 的 SaTokenPlugin.install。文末练习用 hutool-json 替换序列化。"
---

# Sa-Token 插件开发指南 



插件，从字面意思理解就是可拔插的组件，作用是在不改变 Sa-Token 现有架构的情况下，替换或扩展一部分底层代码逻辑。

--- 


## 1、插件开发
			
为 Sa-Token 开发插件非常简单，以下是几种可行的方式：

- 1、自定义全局策略。
- 2、更改全局组件实现。
- 3、实现自定义SaTokenContext。
- 4、其它自由扩展。

下面依次介绍这几种方式。

### 方式1：自定义全局策略

Sa-Token 将框架的一些关键逻辑抽象出一个统一的概念 —— 策略，并统一定义在 `SaStrategy` 中，源码参考：[SaStrategy](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java) 。

SaStrategy 的每一个函数都可以单独重写，以 “自定义Token生成策略” 这一需求为例：

``` java
// 重写 Token 生成策略 
SaStrategy.instance.createToken = (loginId, loginType) -> {
	return SaFoxUtil.getRandomString(60);    // 随机60位长度字符串
};
```

就像变量的重新赋值一样，你只需重新指定一个新的策略函数，即可自定义 Token 生成的逻辑。


### 方式2：更改全局组件实现

你可以找到不符合你需求的组件，重新定义一个子类，以 临时令牌认证 模块为例，你需要自定义 `SaTempTemplate` 的子类：

``` java
/**
 * 临时认证模块 自定义子类实现 
 */
@Component
public class MySaTempTemplate extends SaTempTemplate {

    @Override
    public String createToken(Object value, long timeout, boolean isRecordIndex) {
        System.out.println("------- 自定义一些逻辑 createToken ");
        return super.createToken(value, timeout, isRecordIndex);
    }

    @Override
    public Object parseToken(String token) {
        System.out.println("------- 自定义一些逻辑 parseToken ");
        return super.parseToken(token);
    }

}
```

### 方式3：实现自定义SaTokenContext
SaTokenContext 是对接不同框架的上下文接口，篇幅限制，可参考：[自定义 SaTokenContext 指南](/fun/sa-token-context)


### 方式4：其它自由扩展
这种方式就无需注入什么全局组件替换内部实现了，你可以在 Sa-Token 的基础之上封装任何代码，进行功能扩展。



## 2、插件注册

在你完成插件开发之后，你还需要考虑一个问题，如何让插件代码注入到项目中。

首先这里需要分两种情况：
- 情况1：只打算自己的项目使用这个插件。
- 情况2：准备提交 pr 到 Sa-Token 仓库，让更多人使用。


### 情况1：只打算自己的项目使用这个插件

这种情况比较简单，如果是 SpringBoot 项目，你可以在自定义插件类上添加注解 `@Component`：

``` java
@Component
public class MySaTempTemplate extends SaTempTemplate {
	// ... 
}
```

这样在项目启动时， sa-token-spring-boot-starter 集成包将会扫描到这个自定义组件，注入到框架中。

如果是重写全局策略的代码，也可以通过 `@PostConstruct` 注解做到项目启动时自动执行：

``` java
@PostConstruct
public void rewriteSaStrategy() {
	// 重写 token 生成策略
	SaStrategy.instance.createToken = (loginId, loginType) -> {
		return SaFoxUtil.getRandomString(60);
	};
}
```

如果是非 SpringBoot 项目，项目环境无法做到自动注入，保底的方案是在 main 方法中，手动注册组件：

``` java
public static void main(String[] args) {
	// 示例：手动替换 Sa-Token 内部组件
	// Sa-Token 大部分全局组件都定义在 SaManager 之上，参考：https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java 
	SaManager.setSaTempTemplate(new MySaTempTemplate());
	
	// 示例：手动重写 Sa-Token 全局策略
	SaStrategy.instance.createToken = (loginId, loginType) -> {
		return SaFoxUtil.getRandomString(60);
	};
}
```


### 情况2：准备提交 pr 到 Sa-Token 仓库，让更多人使用。

这种情况稍微复杂一些，因为你基本上很难：通过在插件内部写一些代码，帮助“插件使用者”注册插件到项目中。

一种解决方案是：难办，那就别办了。

对，就是你只负责开发相对应的自定义组件，而将自定义组件的注册过程完全交给使用者，这并不是妥协的选择，反而会给插件使用者更大的自由度，
sa-token-jwt、sa-token-thymeleaf 等官方插件都是这样做的。

如果你觉得还是完成插件的自动注入比较好，也是有办法的，那就是利用 SPI 机制来注册组件。

（关于 java SPI 机制，网上教程众多，此处暂不详细介绍，不熟悉的同学可以直接向 deepseek 等 AI 工具提问，给你讲的明明白白的）

你需要考虑一点：这个插件是专门给 SpringBoot 项目使用的，还是面向 Solon、JFinal 等任意项目使用：

#### 如果是：SpringBoot 专用插件

如果这个插件只打算给 SpringBoot 项目使用，可以利用 SpringBoot 的 SPI 机制注册插件

SpringBoot2 格式：创建 `resources\META-INF\spring.factories` 文件：

``` txt
org.springframework.boot.autoconfigure.EnableAutoConfiguration=插件完全限定名
```

SpringBoot3 格式：创建 `resources\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件：

``` txt
插件完全限定名
```

这样在别人引入此插件时，便会根据 SPI 文件指定的地址去加载插件类，做到插件引入即注册的效果。


#### 如果是：通用型插件

通用型插件则不能使用 SpringBoot 的 SPI 机制去注册组件，因为其它项目是无法识别 SpringBoot SPI 文件的，
好在 Sa-Token 提供了自己的 SPI 机制，所有环境均可使用：

1、新建 `SaTokenPluginForXxx` 类，此类需要 `implements SaTokenPlugin` 接口，并且推荐定义在 `cn.dev33.satoken.plugin` 下：

``` java
/**
 * SaToken 插件安装：插件作用描述
 */
public class SaTokenPluginForXxx implements SaTokenPlugin {
    @Override
    public void install() {
        // 书写需要在项目启动时执行的代码，例如：
        // SaManager.setXxx(new SaXxxForXxx());
    }
}
```

2、新建 `resources\META-INF\satoken\cn.dev33.satoken.plugin.SaTokenPlugin` 文件，填写上插件类的完全限定名地址
``` txt
cn.dev33.satoken.plugin.SaTokenPluginForXxx
```

这样便可以在项目启动时，被 Sa-Token 插件管理器加载到此插件，执行自定义 `SaTokenPluginForXxx` 实现类的 `install` 方法，完成插件安装。


## 3、练练手

学废了吗？给你出个题练练手：

开发一个 `sa-token-hutool-json` 插件，要求引入该插件后，自动替换掉 Sa-Token 的 json 序列化方案为 hutool-json 模块。

如果没有思路，可以参考一下 `sa-token-fastjson` 的插件源码实现哦。

---

### Sa Token Doc New Docs Fun Refer Info (`sa-token-doc-new/docs/fun/refer-info.md`)

---

title: "Sa-Token 开发参考资料"
keywords: "Sa-Token,sa-token,satoken,参考资料,附录"
description: "Sa-Token 开发参考资料：自 2025-3-1 起整理框架开发中实际读过的文章与视频。一条讲 URL 过滤不全的风险点，三条讲 SpringBoot 热拔插 AOP（博客、CSDN、B 站），另有单元测试入门，便于对照源码里的路由过滤、切面与测试写法。阅读顺序建议先风险点、再 AOP 热拔插、最后单测。"
---

# 参考资料
记录 Sa-Token 框架开发中参考过的一些资料。（由 2025-3-1 开始整理）


- Sa-Token对url过滤不全存在的风险点 - https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA
- SpringBoot 热拔插 AOP 组件：
	- https://www.jb51.net/program/297714rev.htm
	- https://www.bilibili.com/video/BV1WZ421W7Qx
	- https://blog.csdn.net/Tomwildboar/article/details/139199801
- 单元测试 - https://www.cnblogs.com/flypig666/p/11505277.html

---

### Sa Token Doc New Docs Fun Sa Token Context  Backup (`sa-token-doc-new/docs/fun/sa-token-context--backup.md`)

---
description: "自定义 SaTokenContext（备份稿）：Web 框架不在官方 Starter 列表时的上下文整合参考。"
---

# 自定义 SaTokenContext 指南 

目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包，
如果我们使用的 Web 框架不在上述列表之中，则需要自定义 SaTokenContext 接口的实现完成整合工作。

---

### 1、SaTokenContext是什么，为什么要实现 SaTokenContext 接口？

在鉴权中，必不可少的步骤就是从 `HttpServletRequest` 中读取 Token，然而并不是所有框架都具有 HttpServletRequest 对象，例如在 WebFlux 中，只有 `ServerHttpRequest`，
在一些其它Web框架中，可能连 `Request` 的概念都没有。

那么，Sa-Token 如何只用一套代码就对接到所有 Web 框架呢？

解决这个问题的关键就在于 `SaTokenContext` 接口，此接口的作用是屏蔽掉不同 Web 框架之间的差异，提供统一的调用API：

<img class="s-w" src="/big-file/doc/fun/sa-token-context.svg" alt="Sa-Token 自定义 SaTokenContext 上下文示意图" />


SaTokenContext只是一个接口，没有工作能力，这也就意味着 SaTokenContext 接口的实现是必须的。
那么疑问来了，我们之前在 SpringBoot 中引用 Sa-Token 时为什么可以直接使用呢？

其实原理很简单，`sa-token-spring-boot-starter`集成包中已经内置了`SaTokenContext`的实现：[SaTokenContextForSpring](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java)，
并且根据 Spring 的自动注入特性，在项目启动时注入到 Sa-Token 中，做到“开箱即用”。

那么如果我们使用不是 Spring 框架，是不是就必须得手动实现 `SaTokenContext` 接口？答案是肯定的，脱离Spring 环境后，我们就不能再使用`sa-token-spring-boot-starter`集成包了，
此时我们只能引入 `sa-token-core` 核心包，然后手动实现 `SaTokenContext` 接口。

不过不用怕，这个工作很简单，只要跟着下面的文档一步步来，你就可以将 Sa-Token 对接到任意Web框架中。


### 2、实现 Model 接口
我们先来观察一下 `SaTokenContext` 接口的签名：
``` java
/**
 * Sa-Token 上下文处理器
 */
public interface SaTokenContext {

	/**
	 * 获取当前请求的 [Request] 对象
	 */
	public SaRequest getRequest();

	/**
	 * 获取当前请求的 [Response] 对象
	 */
	public SaResponse getResponse();

	/**
	 * 获取当前请求的 [存储器] 对象 
	 */
	public SaStorage getStorage();

	/**
	 * 校验指定路由匹配符是否可以匹配成功指定路径 
	 */
	public boolean matchPath(String pattern, String path);

}
```

你可能对 `SaRequest` 比较疑惑，这个对象是干什么用的？正如每个 Web 框架都有 Request 概念的抽象，Sa-Token 也封装了 `Request`、`Response`、`Storage`三者的抽象：

- `Request`：请求对象，携带着一次请求的所有参数数据。参考：[SaRequest.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaRequest.java)。
- `Response`：响应对象，携带着对客户端一次响应的所有数据。参考：[SaResponse.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaResponse.java)。
- `Storage`：请求上下文对象，提供 [一次请求范围内] 的上下文数据读写。参考：[SaStorage.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/context/model/SaStorage.java)。


因此，在实现 `SaTokenContext` 之前，你必须先实现这三个 Model 接口。

先别着急动手，如果你的 Web 框架是基于 Servlet 规范开发的，那么 Sa-Token 已经为你封装好了三个 Model 接口的实现，你要做的就是引入 `sa-token-servlet`包即可：

:::tabs
== Maven 方式

``` xml 

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-servlet</artifactId>
    <version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 权限认证（ServletAPI 集成包）
implementation 'cn.dev33:sa-token-servlet:${sa.top.version}'
```

:::



如果你的 Web 框架不是基于 Servlet 规范，那么你就需要手动实现这三个 Model 接口，我们可以参考 `sa-token-servlet` 是怎样实现的：
[SaRequestForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java)、
[SaResponseForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java)、
[SaStorageForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java)。


### 3、实现 SaTokenContext 接口

接下来我们奔入主题，提供 `SaTokenContext` 接口的实现，同样我们可以参考 Spring 集成包是怎样实现的：

``` java
/**
 * Sa-Token 上下文处理器 [ SpringMVC版本实现 ] 
 */
public class SaTokenContextForSpring implements SaTokenContext {

	/**
	 * 获取当前请求的Request对象
	 */
	@Override
	public SaRequest getRequest() {
		return new SaRequestForServlet(SpringMVCUtil.getRequest());
	}

	/**
	 * 获取当前请求的Response对象
	 */
	@Override
	public SaResponse getResponse() {
		return new SaResponseForServlet(SpringMVCUtil.getResponse());
	}

	/**
	 * 获取当前请求的 [存储器] 对象 
	 */
	@Override
	public SaStorage getStorage() {
		return new SaStorageForServlet(SpringMVCUtil.getRequest());
	}
	
	/**
	 * 校验指定路由匹配符是否可以匹配成功指定路径 
	 */
	@Override
	public boolean matchPath(String pattern, String path) {
		return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
	}

}
```

详细参考：
[SaTokenContextForSpring.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextForSpring.java)


### 4、将自定义实现注入到 Sa-Token 框架中

有了 `SaTokenContext` 接口的实现，我们还需要将这个实现类注入到 Sa-Token 之中，伪代码参考如下：
``` java
/**
 * 程序启动类
 */
public class Application {

	public static void main(String[] args) {
		// 框架启动
		XxxApplication.run(xxx);
		
		// 将自定义的 SaTokenContext 实现类注入到框架中 
		SaTokenContext saTokenContext = new SaTokenContextForXxx();
		SaManager.setSaTokenContext(saTokenContext);
	}
	
}
```

如果你使用的框架带有自动注入特性，那就更简单了，参考 Spring 集成包的 Bean 注入流程：
[注册Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java)、
[注入Bean](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java)


### 5、启动项目

启动项目，尝试打印一下 `SaManager.getSaTokenContext()` 对象，如果输出的是你的自定义实现类，那就证明你已经自定义 `SaTokenContext` 成功了，
快来体验一下 Sa-Token 的各种功能吧。

---

### Sa Token Doc New Docs Fun Sa Token Context (`sa-token-doc-new/docs/fun/sa-token-context.md`)

---
title: "自定义 SaTokenContext 指南"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,自定义 SaTokenContext 指南,插件"
description: "自定义 SaTokenContext：Starter 仅覆盖 SpringBoot、MVC、WebFlux、Solon。其他框架需封装 SaRequest、SaResponse、SaStorage，并 setContext。Servlet 可引入 sa-token-servlet；v1.42.0 起不推荐整接口重写。"
---

# 自定义 SaTokenContext 指南 

目前 Sa-Token 仅对 SpringBoot、SpringMVC、WebFlux、Solon 等部分 Web 框架制作了 Starter 集成包，
如果我们使用的 Web 框架不在上述列表之中，则需要自定义 SaTokenContext 相关接口完成整合工作。

我们需要关注的主要就是四个接口：

- SaTokenContext：上下文管理器。
- SaRequest：请求对象，携带着一次请求的所有参数数据。
- SaResponse：响应对象，携带着对客户端一次响应的所有数据。
- SaStorage：请求上下文对象，提供 [一次请求范围内] 的上下文数据读写。

---


### 上下文包装类 

在鉴权中，必不可少的步骤就是从 `HttpServletRequest` 中读取 Token，然而当我们调用 `StpUtil.isLogin()` 获取当前会话是否登录时，
我们并没有传递 `HttpServletRequest` 参数，框架是怎么读取出来 Token 的呢？

以 SpringBoot 项目为例，Sa-Token 框架会自动注册一个全局过滤器，在每次接收到请求时，将 `HttpServletRequest` 对象保存在 `ThreadLocal` 之中。

在后续的方法中，如果你调用了 `StpUtil.isLogin()` 等方法，框架便会从 `ThreadLocal` 中获取 `HttpServletRequest` 对象，从而进一步读取 token 等信息。

让我们来看一下具体的代码细节，全局上下文初始化过滤器：

``` java
/**
 * SaTokenContext 上下文初始化过滤器 (基于 Servlet)
 */
@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)
public class SaTokenContextFilterForServlet implements Filter {
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		try {
			SaTokenContextServletUtil.setContext((HttpServletRequest) request, (HttpServletResponse) response);
			chain.doFilter(request, response);
		} finally {
			SaTokenContextServletUtil.clearContext();
		}
	}
}
```

进一步追踪 `SaTokenContextServletUtil.setContext` 方法：

``` java
public static void setContext(HttpServletRequest request, HttpServletResponse response) {
	SaRequest req = new SaRequestForServlet(request);
	SaResponse res = new SaResponseForServlet(response);
	SaStorage stg = new SaStorageForServlet(request);
	SaManager.getSaTokenContext().setContext(req, res, stg);
}
```

此处有一个细节，为什么保存的不是原生 `HttpServletRequest` 与 `HttpServletResponse`，而是 `SaRequest`、`SaResponse`、`SaStorage` 三个包装对象？

因为并不是所有的 web 框架都具有 `HttpServletRequest` 对象，例如在 WebFlux 中，只有 `ServerHttpRequest`，
在一些其它Web框架中，可能连 `Request` 的概念都没有。

Sa-Token 为了一套代码对接所有的 Web 框架，就在原生请求对象的基础上又封装了一层 `SaTokenContext` 相关接口，用于屏蔽掉不同 Web 框架之间的差异，提供统一的调用API：

<img src="/big-file/doc/fun/sa-token-context-2.svg" alt="Sa-Token 上下文 SaTokenContext 架构示意图" />

因此，要对接不同的 Web 框架，就要针对不同的 Web 框架封装不同版本的 `SaRequest`、`SaResponse`、`SaStorage` 包装类对象。

如果你的 Web 框架是基于 Servlet 规范开发的，那么你可以直接引入 `sa-token-servlet`，这个包封装了针对 Servlet 规范的上下文包装类对象：

:::tabs
== Maven 方式

``` xml 

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-servlet</artifactId>
    <version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 权限认证（ServletAPI 集成包）
implementation 'cn.dev33:sa-token-servlet:${sa.top.version}'
```

:::



如果你的 web 框架不是基于 Servlet 规范开发的，也问题不大，手动实现一下即可，参考一下 Servlet 包是怎么做的：
[SaRequestForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java)、
[SaResponseForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaResponseForServlet.java)、
[SaStorageForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaStorageForServlet.java)。

封装好包装类对象之后，接下来要做的就是在这个 Web 框架中注册一个全局过滤器，将包装类对象保存到“全局上下文管理器”之中，以备调用：

``` java
SaRequest req = new SaRequestForXxx(request);
SaResponse res = new SaResponseForXxx(response);
SaStorage stg = new SaStorageForXxx(request);
SaManager.getSaTokenContext().setContext(req, res, stg);
```

这样我们即可在具体的 Controller 请求中，成功调用 `StpUtil.isLogin()` 的 API。

总结：整体的步骤并不复杂，就是先定义 `SaRequest`、`SaResponse`、`SaStorage` 的包装类，然后在全局过滤器保存在上下文管理器中。
可以参考具体实现 `sa-token-spring-boot-starter`（SpringBoot2 项目 starter 包）：
[SaTokenContextFilterForServlet.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaTokenContextFilterForServlet.java)



### 旧版本方案
在旧版本中（< v1.42.0）我们推荐的方案是自定义整个 `SaTokenCentext` 接口，目前此方案在新版本已不推荐，此处仅做留档备份：[自定义 SaTokenContext 指南](/fun/sa-token-context--backup.md)

---

### Sa Token Doc New Docs Fun Sa Token Test (`sa-token-doc-new/docs/fun/sa-token-test.md`)

---
title: "Sa-Token框架掌握度--在线考试"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Sa-Token框架掌握度--在线考试,附录"
description: "Sa-Token 框架掌握度在线考试：满分 100 分，测评登录认证、权限校验、SSO 等核心知识。考卷托管在问卷星，点正文链接即可开考。建议先通读官方文档再作答，做完后按错题回到对应章节复习，检验阅读与实操掌握程度。问卷星在线作答，此份考卷将测评您对 Sa-Token 框架的掌握程度，打开链接即可开始。"
---

# Sa-Token框架掌握度--在线考试

--- 

此份考卷将测评您对Sa-Token框架的掌握程度（满分100），链接：[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx)

---

### Sa Token Doc New Docs Fun Session Model (`sa-token-doc-new/docs/fun/session-model.md`)

---
title: "Sa-Token Session模型详解"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Session模型详解,附录"
description: "Sa-Token Session 模型：Account-Session 按账号 id 分配，PC 与 APP 同号可同步数据；Token-Session 按 token 隔离，适合单端无操作下线。另有 Custom-Session 按自定义 id 读写。仅登录时创建，支持 Cookie、Header、body 提交。"
---

# Sa-Token 中的 Session会话 模型详解

--- 

### 1、Account-Session 

提起Session，你脑海中最先浮现的可能就是 JSP 中的 HttpSession，它的工作原理可以大致总结为：

客户端每次与服务器第一次握手时，会被强制分配一个 `[唯一id]` 作为身份标识，注入到 Cookie 之中，
之后每次发起请求时，客户端都要将它提交到后台，服务器根据 `[唯一id]` 找到每个请求专属的Session对象，维持会话

这种机制简单粗暴，却有N多明显的缺点：

1. 同一账号分别在PC、APP登录，会被识别为两个不相干的会话 
2. 一个设备难以同时登录两个账号
3. 每次一个新的客户端访问服务器时，都会产生一个新的Session对象，即使这个客户端只访问了一次页面 
4. 在不支持Cookie的客户端下，这种机制会失效 


Sa-Token Session可以理解为 HttpSession 的升级版：

1. Sa-Token只在调用`StpUtil.login(id)`登录会话时才会产生Session，不会为每个陌生会话都产生Session，节省性能 
2. 在登录时产生的Session，是分配给账号id的，而不是分配给指定客户端的，也就是说在PC、APP上登录的同一账号所得到的Session也是同一个，所以两端可以非常轻松的同步数据  
3. Sa-Token支持Cookie、Header、body三个途径提交Token，而不是仅限于Cookie 
4. 由于不强依赖Cookie，所以只要将Token存储到不同的地方，便可以做到一个客户端同时登录多个账号 

这种为账号id分配的Session，我们给它起一个合适的名字：`Account-Session`，你可以通过如下方式操作它：
``` java
// 获取当前会话的 Account-Session 
SaSession session = StpUtil.getSession();

// 从 Account-Session 中读取、写入数据 
session.get("name");
session.set("name", "张三");
```

使用`Account-Session`在不同端同步数据是非常方便的，因为只要 PC 和 APP 登录的账号id一致，它们对应的都是同一个Session，
举个应用场景：在PC端点赞的帖子列表，在APP端的点赞记录里也要同步显示出来


### 2、Token-Session  

随着业务推进，我们还可能会遇到一些需要数据隔离的场景：

::: info 业务场景
指定客户端超过两小时无操作就自动下线，如果两小时内有操作，就再续期两小时，直到新的两小时无操作 
:::

那么这种请求访问记录应该存储在哪里呢？放在 Account-Session 里吗？

可别忘了，PC端和APP端可是共享的同一个 Account-Session ，如果把数据放在这里，
那就意味着，即使用户在PC端一直无操作，只要手机上用户还在不间断的操作，那PC端也不会过期！

解决这个问题的关键在于，虽然两个设备登录的是同一账号，但是两个它们得到的token是不一样的，
Sa-Token针对会话登录，不仅为账号id分配了`Account-Session`，同时还为每个token分配了不同的`Token-Session`

不同的设备端，哪怕登录了同一账号，只要它们得到的token不一致，它们对应的 `Token-Session` 就不一致，这就为我们不同端的独立数据读写提供了支持：

``` java
// 获取当前会话的 Token-Session 
SaSession session = StpUtil.getTokenSession();

// 从 Token-Session 中读取、写入数据 
session.get("name");
session.set("name", "张三");
```

### 3、Custom-Session

除了以上两种Session，Sa-Token还提供了第三种Session，那就是：`Custom-Session`，你可以将其理解为：自定义Session

Custom-Session不依赖特定的 账号id 或者 token，而是依赖于你提供的SessionId：

``` java
// 获取指定key的 Custom-Session 
SaSession session = SaSessionCustomUtil.getSessionById("goods-10001");

// 从 Custom-Session 中读取、写入数据 
session.get("name");
session.set("name", "张三");
```

只要两个自定义Session的Id一致，它们就是同一个Session 

Custom-Session的会话有效期默认使用`SaManager.getConfig().getTimeout()`, 如果需要修改会话有效期, 可以在创建之后, 使用对象方法修改

``` java
session.updateTimeout(1000); // 参数说明和全局有效期保持一致
```


### 4、Session模型结构图 

三种Session创建时机：

- `Account-Session`: 指的是框架为每个 账号id 分配的 Session 
- `Token-Session`: 指的是框架为每个 token 分配的 Session  
- `Custom-Session`: 指的是以一个 特定的值 作为SessionId，来分配的 Session 


**假设三个客户端登录同一账号，且配置了不共享token，那么此时的Session模型是：**

<img class="s-w" src="/big-file/doc/fun/session-model3.png" alt="Sa-Token Account-Session 与 Token-Session 模型示意图" />

简而言之：
- `Account-Session`  以账号 id 为主，只要 token 指向的账号 id 一致，那么对应的Session对象就一致
- `Token-Session` 以token为主，只要token不同，那么对应的Session对象就不同
- `Custom-Session` 以特定的key为主，不同key对应不同的Session对象，同样的key指向同一个Session对象

---

### Sa Token Doc New Docs Fun Sso Vs Oauth2 (`sa-token-doc-new/docs/fun/sso-vs-oauth2.md`)

---
title: "Sa-Token 技术选型：SSO 与 OAuth2 对比"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,技术选型：SSO 与 OAuth2 对比,附录"
description: "SSO 与 OAuth2 选型对比：统一认证两者都强。SSO 更擅长统一注销、会话强一致，适合企业内部多系统整合、集成也更简单；OAuth2 更适合第三方应用授权、Client 级权限校验，适合搭统一认证授权平台并对外开放服务。按 Sa-Token 实现列差异。注：对比按 Sa-Token 实现，其他框架可能略有不同。"
---

# 技术选型：[ 单点登录 ] VS [ OAuth2.0 ]

--- 

QQ群里经常有小伙伴提问：项目需要搭建统一认证中心，是用 SSO 方便还是 OAuth2.0 方便呢？针对这个问题，我们列出两者的主要区别以供大家参考：


| 功能点				| SSO单点登录			| OAuth2.0											|
| :--------			| :--------				| :--------											|
| 统一认证			| 支持度高				| 支持度高											|
| 统一注销			| 支持度高				| 支持度低											|
| 多个系统会话一致性	| 强一致					| 弱一致										|
| 第三方应用授权管理	| 不支持					| 支持度高									|
| 自有系统授权管理	| 支持度高				| 支持度低										|
| Client级的权限校验	| 不支持					| 支持度高										|
| 集成简易度			| 比较简单				| 难度中等									|
| 适合项目			| 企业内部项目整合		| 企业搭建统一认证授权平台，对外开放服务			|


注：以上仅为在 Sa-Token 中两种技术的差异度比较，不同框架的实现可能略有差异，但整体思想是一致的。

---

### Sa Token Doc New Docs Fun Team (`sa-token-doc-new/docs/fun/team.md`)

---
title: "Sa-Token 团队成员"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,团队成员,附录"
description: "Sa-Token 团队成员：开发组负责代码开发、社区维护、issue 处理与 PR 审核，成员为作者小风筝、AppleOfGray、ly-chn。提案讨论组负责提案新增、讨论与投票，另有茉莉、药水、呆某人、春困夏倦秋乏、淡墨；作者在讨论组记为刘潇。表内附头像与 Gitee 主页。开发三人；讨论组八人，其中三位交叉任职。"
---

# 团队成员 


### 开发 

负责：代码开发、社区维护、issue 处理、pr 审核等。

<table class="team-table">
    <tr>
        <th>头像</th>
        <th>昵称</th>
        <th>个人主页</th>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-xiaofengzheng.jpg" alt="Sa-Token 团队成员 小风筝" /></td>
        <td>小风筝（作者）</td>
        <td><a href="https://gitee.com/click33" target="_blank">https://gitee.com/click33</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-AppleOfGray.png" alt="Sa-Token 团队成员 AppleOfGray" /></td>
        <td>AppleOfGray</td>
		<td><a href="https://gitee.com/appleOfGray" target="_blank">https://gitee.com/appleOfGray</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-ly-chn.png" alt="Sa-Token 团队成员 ly-chn" /></td>
        <td>ly-chn</td>
		<td><a href="https://gitee.com/ly-chn" target="_blank">https://gitee.com/ly-chn</a></td>
    </tr>
</table>



### 提案讨论组成员 

负责：提案新增、讨论、投票。

<table class="team-table">
    <tr>
        <th>头像</th>
        <th>昵称</th>
        <th>个人主页</th>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-xiaofengzheng.jpg" alt="Sa-Token 团队成员 小风筝" /></td>
        <td>刘潇</td>
        <td><a href="https://gitee.com/click33" target="_blank">https://gitee.com/click33</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-AppleOfGray.png" alt="Sa-Token 团队成员 AppleOfGray" /></td>
        <td>AppleOfGray</td>
		<td><a href="https://gitee.com/appleOfGray" target="_blank">https://gitee.com/appleOfGray</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-ly-chn.png" alt="Sa-Token 团队成员 ly-chn" /></td>
        <td>ly-chn</td>
		<td><a href="https://gitee.com/ly-chn" target="_blank">https://gitee.com/ly-chn</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-moli.jpg" alt="Sa-Token 团队成员 茉莉" /></td>
        <td>茉莉</td>
		<td><a href="https://gitee.com/kidoldman" target="_blank">https://gitee.com/kidoldman</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-yaoshui.jpg" alt="Sa-Token 团队成员 药水" /></td>
        <td>药水</td>
		<td><a href="https://gitee.com/java_pioneer" target="_blank">https://gitee.com/java_pioneer</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-daimouren.png" alt="Sa-Token 团队成员 呆某人" /></td>
        <td>呆某人</td>
		<td><a href="https://gitee.com/zhubj0510" target="_blank">https://gitee.com/zhubj0510</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-chunkunqiufa.jpg" alt="Sa-Token 团队成员 春困夏倦秋乏" /></td>
        <td>春困夏倦秋乏</td>
		<td><a href="https://gitee.com/uncarbon97" target="_blank">https://gitee.com/uncarbon97</a></td>
    </tr>
    <tr>
        <td><img src="/big-file/doc/team/avatar-danmo.jpg" alt="Sa-Token 团队成员 淡墨" /></td>
        <td>淡墨</td>
		<td><a href="https://gitee.com/jinan-jimeng-network_0" target="_blank">https://gitee.com/jinan-jimeng-network_0</a></td>
    </tr>
</table>

---

### Sa Token Doc New Docs Fun Tech Stack (`sa-token-doc-new/docs/fun/tech-stack.md`)

---

title: "Sa-Token 源码技术栈"
keywords: "Sa-Token,sa-token,satoken,Spring Boot,Redis,JWT,OAuth2"
description: "Sa-Token 源码技术栈：Maven 多模块，内核用 Servlet、Cookie、Java8 默认方法与自定义注解；Starter 覆盖 SpringBoot2、AOP、yml 映射与拦截器。插件侧含 Redis、Jackson、Hutool、JWT、OAuth2 与 MD5、SHA、AES、RSA 加密。"
---

# Sa-Token 源码用到的所有技术栈

包括但不限于以下：

- Maven多模块项目
- Servlet API、临时Cookie与永久Cookie、Request参数获取
- SpringBoot2.0、Redis、Jackson、Hutool、jwt
- SpringBoot自定义starter、Spring包扫描 + 依赖注入、AOP注解切面、yml配置映射、拦截器
- Java8 接口与default实现、静态方法、枚举、定时器、异常类、泛型、反射、IO流、自定义注解、Lambda表达式、函数式编程
- package-info注释、Serializable序列化接口、synchronized锁
- java加密算法：MD5、SHA1、SHA256、AES、RSA
- OAuth2.0、同域单点登录、集群与分布式、路由Ant匹配

---

### Sa Token Doc New Docs Fun Three Scope (`sa-token-doc-new/docs/fun/three-scope.md`)

---
title: "Sa-Token 数据读写三大作用域"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,数据读写三大作用域,附录"
description: "Sa-Token 数据读写三大作用域：SaStorage 为请求域，一次请求结束即清，无需登录；SaSession 为会话域，须登录后经 StpUtil.getSession 读写；SaApplication 为全局域，应用或 Redis 关闭后清除，同样无需登录，三者 get/set/delete 用法一致。"
---

# 三大作用域 

--- 

Sa-Token 数据存储有三大作用域，分别是：
- `SaStorage` - 请求作用域：存储的数据只在一次请求内有效。
- `SaSession` - 会话作用域：存储的数据在一次会话范围内有效。
- `SaApplication` - 全局作用域：存储的数据在全局范围内有效。


### SaStorage - 请求作用域
在 SaStorage 中存储的数据只在一次请求范围内有效，请求结束后数据自动清除。使用 SaStorage 时无需处于登录状态。

``` java
SaStorage storage = SaHolder.getStorage();
storage.get("key");   // 取值
storage.set("key", "value");   // 写值 
storage.delete("key");   // 删值 
```


### SaSession - 会话作用域
在 SaSession 存储的数据在一次会话范围内有效，会话结束后数据自动清除。必须登录后才能使用 SaSession 对象。

``` java
SaSession session = StpUtil.getSession();
session.get("key");   // 取值
session.set("key", "value");   // 写值 
session.delete("key");   // 删值 
```


### SaApplication - 全局作用域
在 SaApplication 存储的数据在全局范围内有效，应用关闭后数据自动清除（如果集成了 Redis 那则是 Redis 关闭后数据自动清除）。使用 SaApplication 时无需处于登录状态。

``` java
SaApplication application = SaHolder.getApplication();
application.get("key");   // 取值
application.set("key", "value");   // 写值 
application.delete("key");   // 删值 
```

---

### Sa Token Doc New Docs Fun Timeline (`sa-token-doc-new/docs/fun/timeline.md`)

---
title: "Sa-Token开源大事记"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Sa-Token开源大事记,附录"
description: "Sa-Token 开源大事记：2020-02-04 GitHub 开源；2021 年加入 dromara、获 GVP，star 超过 Shiro 与 Spring Security；2022 年 GitHub 破万 star；2024 年 42.9k 登顶 Gitee；2026 年获 Gitee Web Top 2。"
---

# Sa-Token 开源大事记

--- 

- **2020-02-04：** 在 GitHub 提交第一个版本，正式开源。
- **2020-09-14：** GitHub star 数量破 100。
- **2020-10-26：** Gitee star 数量破 100。
- **2021-03-01：** 被 [HelloGitHub] 第 59 期收录推荐。
- **2021-03-26：** GitHub star 数量破 1k。
- **2021-03-30：** 受 TLog 作者邀请，Sa-Token 加入 dromara 社区。
- **2021-03-30：** 被 Gitee 列为推荐项目。
- **2021-03-31：** Gitee star 数量破1K。



- **2021-06-17：** Sa-Token star 数量 (3529) 超过 Shiro (3506)。

- **2021-07-26：** GitHub star 数量破5K。


- **2021-09-24：** Sa-Token star 数量 (6280) 超过 SpringSecurity (6247)。

- **2021-11-08：** 荣获开源中国 “码云 GVP 认证”。

- **2021-11-28：** Gitee star 数量破5K。
- **2021-12-27：** 荣获 OSC-2021 最佳软件 Top 30。
- **2022-05-20：** 成为 [可信开源社区共同体] 预备成员。
- **2022-08-01：** 加入 [中国开源社区 landscape]。
- **2022-08-18：** GitHub 第 10000 个 star 里程碑！
- **2023-01-09：** 荣获 OSC 2022 年度最热开源项目社区。
- **2023-11-21：** 被评为“开放原子基金会2023快速成长开源项目”。
- **2024-04-25：** 42.9k star 登顶 Gitee 开源项目推荐榜 Top 1。
- **2024-08-19：** 成为 GitCode G-Star 开源摘星计划毕业项目。
- **2024-11-22：** 所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛（开源）》二等奖。
- **2025-11-13：** 荣获 Gitee 5000 Star 专属定制奖杯。
- **2026-02-05：** 荣获《Gitee 2025年度开源项目 Web应用开发 Top 2》。

---

### Sa Token Doc New Docs Fun Token Info (`sa-token-doc-new/docs/fun/token-info.md`)

---
title: "Sa-Token TokenInfo参数详解"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,TokenInfo参数详解,附录"
description: "Sa-Token TokenInfo 参数：getTokenInfo() 返回 tokenName、tokenValue、isLogin、loginId、loginType、loginDevice，以及 tokenTimeout、sessionTimeout 等剩余秒数（-2 表示系统中不存在该缓存）。"
---

# SaTokenInfo 参数详解

token信息Model: 用来描述一个token的常用参数

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

---

### Sa Token Doc New Docs Fun Token Timeout (`sa-token-doc-new/docs/fun/token-timeout.md`)

---
title: "Sa-Token Token有效期详解"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Token有效期详解,附录"
description: "Sa-Token Token 有效期：timeout 为长久有效期，默认 30 天、-1 永不过期，v1.29+ 可用 renewTimeout 续期；active-timeout 为最低活跃频率，超时冻结抛 NotLoginException(-3)。二者可同时用，也可关 autoRenew 后手动续签。"
---

# Token有效期详解



Sa-Token 提供两种 Token 自动过期策略，分别是 `timeout` 与 `active-timeout`，配置方法如下：

:::tabs
== yaml 风格

``` yaml
sa-token: 
	# token 有效期（单位：秒），默认30天，-1代表永不过期 
	timeout: 2592000
	# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结
	active-timeout: -1
```

== properties 风格

``` properties
# token 有效期（单位：秒），默认30天，-1代表永不过期 
sa-token.timeout=2592000
# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结
sa-token.active-timeout=-1
```

:::



两者的区别，可以通过下面的例子体现：

::: tip 场景示例
1. 假设你到银行要存钱，首先就要办理一张卡 （要访问系统接口先登录）。
2. 银行为你颁发一张储蓄卡（系统为你颁发一个Token），以后每次存取钱都要带上这张卡（后续每次访问系统都要提交 Token）。
3. 银行为这张卡设定两个过期时间：
	- 第一个是 `timeout`，代表这张卡的长久有效期，就是指这张卡最长能用多久，假设 `timeout=3年`，那么3年后此卡将被银行删除，想要继续来银行办理业务必须重新办卡（Token 过期后想要访问系统必须重新登录）。
	- 第二个就是 `active-timeout`，代表这张卡的最低活跃频率限制，就是指这张卡必须每隔多久来银行一次，假设 `active-timeout=1月` ，你如果超过1月不来办一次业务，银行就将你的卡冻结，列为长期不动户（Token 长期不访问系统，被冻结，但不会被删除）。
4. 两个过期策略可以单独配置，也可以同时配置，只要有其中一个有效期超出了范围，这张卡就会变得不可用（两个有效期只要有一个过期了，Token就无法成功访问系统了）。
:::

下面是对两个过期策略的详细解释：

### timeout
1. `timeout`代表 Token 的长久有效期，单位/秒，例如将其配置为 2592000 (30天)，代表在30天后，Token必定过期，无法继续使用。
2. `timeout`~~无法续签，想要继续使用必须重新登录~~。v1.29.0+ 版本新增续期方法：`StpUtil.renewTimeout(100)`。
3. `timeout`的值配置为-1后，代表永久有效，不会过期。


### active-timeout
1. `active-timeout`代表最低活跃频率，单位/秒，例如将其配置为 1800 (30分钟)，代表用户如果30分钟无操作，则此Token会立即过期（被冻结，但不会删除掉）。
2. 如果在30分钟内用户有操作，则会再次续签30分钟，用户如果一直操作则会一直续签，直到连续30分钟无操作，Token才会过期。
3. `active-timeout`的值配置为-1后，代表永久有效，不会过期，此时也无需频繁续签。


### 关于active-timeout的续签
如果`active-timeout`配置了大于零的值，Sa-Token 会在登录时开始计时，在每次直接或间接调用`getLoginId()`、`getTokenSession()`时进行一次冻结检查与续签操作。
此时会有两种情况：
1. 一种是会话无操作时间太长，Token已经被冻结，此时框架会抛出`NotLoginException`异常(场景值=-3)，
2. 另一种则是会话在`active-timeout`有效期内通过检查，此时Token可以成功续签 


### 我可以手动续签 active-timeout 吗？
**可以！**
如果框架的自动续签算法无法满足您的业务需求，你可以进行手动续签，Sa-Token 提供两个API供你操作：
1. `StpUtil.checkActiveTimeout()`: 检查当前Token 是否已经被冻结，如果是则抛出异常
2. `StpUtil.updateLastActiveToNow()`: 续签当前Token：(将 [最后操作时间] 更新为当前时间戳) 

注意：在手动续签时，即使 Token 已经被冻结也可续签成功（解冻），如果此场景下需要提示续签失败，可采用先检查再续签的形式保证Token有效性 

例如以下代码：
``` java
// 先检查是否已被冻结
StpUtil.checkActiveTimeout();
// 检查通过后继续续签
StpUtil.updateLastActiveToNow();
```

同时，你还可以关闭框架的自动续签（在配置文件中配置 `autoRenew=false` ），此时续签操作完全由开发者控制，框架不再自动进行任何续签操作

如果你需要给其它 Token 续签：

``` java
// 为指定 Token 续签 
StpUtil.stpLogic.updateLastActiveToNow(tokenValue);
```


### timeout 与 active-timeout 可以同时使用吗？
**可以同时使用！** 
两者的认证逻辑彼此独立，互不干扰，可以同时使用。


### StpUtil 类中哪些方法支持自动续签 active-timeout? 
> 直接或间接调用过 `getLoginId()`、`getTokenSession()` 的方法

| 包括但不限于这些： |
|---|
| StpUtil.checkLogin() |
| StpUtil.getLoginId() |
| StpUtil.getLoginIdAsInt() |
| StpUtil.getLoginIdAsString() |
| StpUtil.getLoginIdAsLong() |
|---|
| StpUtil.getSession() |
| StpUtil.getTokenSession() |
|---|
| StpUtil.getRoleList() |
| StpUtil.hasRole() |
| StpUtil.hasRoleAnd() |
| StpUtil.hasRoleOr() |
| StpUtil.checkRole() |
| StpUtil.checkRoleAnd() |
| StpUtil.checkRoleOr() |
|---|
| StpUtil.getPermissionList() |
| StpUtil.hasPermission() |
| StpUtil.hasPermissionAnd() |
| StpUtil.hasPermissionOr() |
| StpUtil.checkPermission() |
| StpUtil.checkPermissionAnd() |
| StpUtil.checkPermissionOr() |
|---|
| StpUtil.openSafe() |
| StpUtil.isSafe() |
| StpUtil.checkSafe() |
| StpUtil.getSafeTime() |
| StpUtil.closeSafe() |

> 以下注解都间接调用过 getLoginId() 方法

| 支持自动续签的注解 |
|---|
| @SaCheckLogin      |
| @SaCheckRole       |
| @SaCheckPermission |
| @SaCheckSafe       |

---

### Sa Token Doc New Docs Fun Update Version Flow (`sa-token-doc-new/docs/fun/update-version-flow.md`)

---
title: "Sa-Token版本更新流程"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Sa-Token版本更新流程,附录"
description: "Sa-Token 版本更新流程：文内一张流程图串起从 issue 讨论、代码合并、发版到文档同步的维护步骤。贡献者可按图把握提 PR、审核与发版窗口，避免只改代码不更文档，或把文档更新甩到发版之后造成版本说明对不上。图中节点覆盖讨论、评审、发版与文档同步，贡献者可按图把握 PR 合入与文档同步时机。"
---

# Sa-Token 版本更新流程

---

### Sa-Token 版本更新流程：

<img src="/big-file/index/update-version-flow.svg" alt="Sa-Token 版本更新流程" style="width:100%;max-width:100%;" />

---

### Sa Token Doc New Docs Include Include Qa (`sa-token-doc-new/docs/include/include-qa.md`)

::: warning 更改了 hosts 但无法访问？
- 可能 1：你没保存。
- 可能 2：你后端项目没启动。
- 可能 3：你访问时端口写错了。
- 可能 4：你开了 VPN，关掉试试。
:::

---

### Sa Token Doc New Docs Micro Dcs Session (`sa-token-doc-new/docs/micro/dcs-session.md`)

---
title: "Sa-Token 分布式Session会话"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,分布式Session会话,微服务,微服务鉴权"

description: "微服务下的 Sa-Token 分布式 Session：单机 Session 无法跨节点，节点一登录后落到节点二会变未登录。推荐用 sa-token-redis-template 与 commons-pool2 把会话放到 Redis，各节点变无状态服务；jwt 无状态令牌适合简单业务但功能明显受限。"
---

# 微服务 - 分布式Session会话

--- 

### 需求场景 

微服务架构下的第一个难题便是数据同步，单机版的`Session`在分布式环境下一般不能正常工作，为此我们需要对框架做一些特定的处理。

首先我们要明白，分布式环境下为什么`Session`会失效？因为用户在一个节点对会话做出的更改无法实时同步到其它的节点，
这就导致一个很严重的问题：如果用户在节点一上已经登录成功，那么当下一次的请求落在节点二上时，对节点二来讲，此用户仍然是未登录状态。

### 解决方案 

要怎么解决这个问题呢？目前的主流方案有四种：
1. **Session同步**：只要一个节点的数据发生了改变，就强制同步到其它所有节点 
2. **Session粘滞**：通过一定的算法，保证一个用户的所有请求都稳定的落在一个节点之上，对这个用户来讲，就好像还是在访问一个单机版的服务
3. **建立会话中心**：将Session存储在专业的缓存中间件上，使每个节点都变成了无状态服务，例如：`Redis`
4. **颁发无状态token**：放弃Session机制，将用户数据直接写入到令牌本身上，使会话数据做到令牌自解释，例如：`jwt`


### 方案选择

该如何选择一个合适的方案？
- 方案一：性能消耗太大，不太考虑
- 方案二：需要从网关处动手，与框架无关
- 方案三：Sa-Token 整合`Redis`非常简单，详见章节：[集成 Redis](/up/integ-redis)
- 方案四：详见官方仓库中 Sa-Token 整合`jwt`的示例（示例压缩包：[Sa-Token 集成示例大全下载](/more/download-demos) ）

由于`jwt`模式不在服务端存储数据，对于比较复杂的业务可能会功能受限，因此更加推荐使用方案三


<button class="show-img" img-src="/big-file/doc/micro/g3--dcs-session.gif">加载动态演示图</button>


集成依赖示例：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 整合 RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'
implementation 'org.apache.commons:commons-pool2'
```

:::


详细参考：[集成 Redis](/up/integ-redis)

---

### Sa Token Doc New Docs Micro Gateway Auth (`sa-token-doc-new/docs/micro/gateway-auth.md`)

---
title: "Sa-Token 网关统一鉴权"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,网关统一鉴权,微服务,微服务鉴权"

description: "微服务网关统一鉴权：在 Spring Cloud Gateway 引入 sa-token-reactor-spring-boot-starter 与 Redis，注册 SaReactorFilter，用 SaRouter 做 checkLogin 并按 /user/** 等模块校验权限，业务服务不必各自鉴权。"
---

# 微服务 - 网关统一鉴权

微服务架构下的鉴权一般分为两种：
1. 每个服务各自鉴权
2. 网关统一鉴权 

方案一和传统单体鉴权差别不大，不再过多赘述，本篇介绍方案二的整合步骤：

--- 



### 1、引入依赖 

首先，根据 [依赖引入说明](/micro/import-intro) 引入正确的依赖，以`[SpringCloud Gateway]`为例：

:::tabs
== Maven 方式

``` xml 

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>${sa.top.version}</version>
</dependency>


<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。

== Gradle 方式

``` gradle
// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.com
implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'

// Sa-Token 整合 RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'
implementation 'org.apache.commons:commons-pool2'
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。

:::



注：Redis包是必须的，因为我们需要和各个服务通过Redis来同步数据 

### 2、实现鉴权接口
``` java
/**
 * 自定义权限验证接口扩展 
 */
@Component   
public class StpInterfaceImpl implements StpInterface {

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的权限列表 
        return ...;
    }

    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 返回此 loginId 拥有的角色列表
        return ...;
    }

}

```
关于数据的获取，建议以下方案三选一：
1. 在网关处集成ORM框架，直接从数据库查询数据
2. 先从Redis中获取数据，获取不到时走ORM框架查询数据库 
3. 先从Redis中获取缓存数据，获取不到时走RPC调用子服务 (专门的权限数据提供服务) 获取


### 3、注册全局过滤器 
然后在网关处注册全局过滤器进行鉴权操作 

``` java
/**
 * [Sa-Token 权限认证] 配置类 
 * @author click33
 */
@Configuration
public class SaTokenConfigure {
	// 注册 Sa-Token全局过滤器 
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
			// 拦截地址 
			.addInclude("/**")    /* 拦截全部path */
			// 开放地址 
			.addExclude("/favicon.ico")
			// 鉴权方法：每次访问进入 
			.setAuth(obj -> {
				// 登录校验 -- 拦截所有路由，并排除/user/doLogin 用于开放登录 
				SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
				
				// 权限认证 -- 不同模块, 校验不同权限 
				SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
				SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
				SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
				SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
				
				// 更多匹配 ...  */
			})
			// 异常处理方法：每次setAuth函数出现异常时进入 
			.setError(e -> {
				return SaResult.error(e.getMessage());
			})
			;
    }
}
```

详细操作参考：[路由拦截鉴权](/use/route-check)

---

### Sa Token Doc New Docs Micro Same Token (`sa-token-doc-new/docs/micro/same-token.md`)

---
title: "Sa-Token 内部服务外网隔离"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,内部服务外网隔离,微服务,微服务鉴权"
description: "Sa-Token Same-Token 内网隔离：防止绕过网关直连子服务。网关 ForwardAuthFilter 用 SaSameUtil.getToken 追加请求头，子服务 SaServletFilter.checkToken 校验；Feign 拦截器同样携带 Same-Token 做 RPC 鉴权。"
---

# 微服务 - 内部服务外网隔离 

--- 


### 一、需求场景  

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

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

这种鉴权需求牵扯到两个环节： **`网关转发鉴权`** 、 **`服务间内部调用鉴权`**

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

本篇主要讲解方案二 `Same-Token` 模块的整合步骤，其鉴权流程与 OAuth2.0 类似，不过使用方式上更加简洁（希望使用方案一的同学可参考Sa-OAuth2模块，此处不再赘述）

<img class="w-100" src="/big-file/doc/micro/micro-network-isolation.svg" alt="微服务内网隔离：网关转发与子服务鉴权流程" />


### 二、网关转发鉴权 

##### 1、引入依赖

在网关处引入的依赖为（此处以 SpringCloud Gateway 为例）：
:::tabs
== Maven 方式

``` xml 

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>${sa.top.version}</version>
</dependency>


<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。

== Gradle 方式

``` gradle
// Sa-Token 权限认证（Reactor响应式集成），在线文档：https://sa-token.com
implementation 'cn.dev33:sa-token-reactor-spring-boot-starter:${sa.top.version}'

// Sa-Token 整合 RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'
implementation 'org.apache.commons:commons-pool2'
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-reactor-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-reactor-spring-boot4-starter`。

:::


在下游子服务引入的依赖为：
:::tabs
== Maven 方式

``` xml 

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>${sa.top.version}</version>
</dependency>


<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。

== Gradle 方式

``` gradle
// Sa-Token 权限认证，在线文档：https://sa-token.com
implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'

// Sa-Token 整合 RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'
implementation 'org.apache.commons:commons-pool2'
```
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。

:::


##### 2、网关处添加Same-Token

为网关添加全局过滤器：
``` java
/**
 * 全局过滤器，为请求添加 Same-Token 
 */
@Component
public class ForwardAuthFilter implements GlobalFilter {
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		ServerHttpRequest newRequest = exchange
				.getRequest()
				.mutate()
				// 为请求追加 Same-Token 参数 
				.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken())
				.build();
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
        return chain.filter(newExchange);
	}
}
```
此过滤器会为 Request 请求头追加 `Same-Token` 参数，这个参数会被转发到子服务 


##### 3、在子服务里校验参数 

在子服务添加过滤器校验参数 
``` java
/**
 * Sa-Token 权限认证 配置类 
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
	// 注册 Sa-Token 全局过滤器 
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
        		.addInclude("/**")
        		.addExclude("/favicon.ico")
        		.setAuth(obj -> {
        			// 校验 Same-Token 身份凭证 	—— 以下两句代码可简化为：SaSameUtil.checkCurrentRequestToken(); 
        			String token = SaHolder.getRequest().getHeader(SaSameUtil.SAME_TOKEN);
        			SaSameUtil.checkToken(token);
        		})
        		.setError(e -> {
        			return SaResult.error(e.getMessage());
        		})
        		;
    }
}
```

启动网关与子服务，访问测试：

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


### 三、服务间内部调用鉴权 

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

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

##### 1、首先在调用方添加 FeignInterceptor
``` java
/**
 * feign拦截器, 在feign请求发出之前，加入一些操作 
 */
@Component
public class FeignInterceptor implements RequestInterceptor {
	// 为 Feign 的 RPC 调用 添加请求头Same-Token 
	@Override
	public void apply(RequestTemplate requestTemplate) {
		requestTemplate.header(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
		
		// 如果希望被调用方有会话状态，此处就还需要将 satoken 添加到请求头中
		// requestTemplate.header(StpUtil.getTokenName(), StpUtil.getTokenValue());
	}
}
```

##### 2、在调用接口里使用此 Interceptor 
``` java
/**
 * 服务调用 
 */
@FeignClient(
		name = "sp-home", 				// 服务名称 
		configuration = FeignInterceptor.class,		// 请求拦截器 （⚠️ 关键代码）
		fallbackFactory = SpCfgInterfaceFallback.class	// 服务降级处理 
		)	
public interface SpCfgInterface {

	// 获取server端指定配置信息 
	@RequestMapping("/SpConfig/getConfig")
	public String getConfig(@RequestParam("key")String key);
	
}
```

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


### 四、Same-Token 模块详解 

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

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

<img class="w-100" src="/big-file/doc/micro/micro-same-token.svg" alt="Same-Token 服务间身份校验流程" />




首先我们预览一下此模块的相关API：
``` java
// 获取当前Same-Token
SaSameUtil.getToken();

// 判断一个Same-Token是否有效
SaSameUtil.isValid(token);

// 校验一个Same-Token是否有效 (如果无效则抛出异常)
SaSameUtil.checkToken(token);

// 校验当前Request提供的Same-Token是否有效 (如果无效则抛出异常)
SaSameUtil.checkCurrentRequestToken();

// 刷新一次Same-Token (注意集群环境中不要多个服务重复调用) 
SaSameUtil.refreshToken();

// 在 Request 上储存 Same-Token 时建议使用的key
SaSameUtil.SAME_TOKEN;
```

##### 1、疑问：这个Token保存在什么地方？有没有泄露的风险？Token为永久有效还是临时有效？
Same-Token 默认随 Sa-Token 数据一起保存在Redis中，理论上不会存在泄露的风险，每个Token默认有效期只有一天

##### 2、如何主动刷新Same-Token，例如：五分钟、两小时刷新一次？
Same-Token 刷新间隔越短，其安全性越高，每个Token的默认有效期为一天，在一天后再次获取会自动产生一个新的Token

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

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

例如，我们可以专门起一个服务，使用定时任务来刷新Same-Token 
``` java
/**
 * Same-Token，定时刷新
 */
@Configuration
public class SaSameTokenRefreshTask {
	// 从 0 分钟开始 每隔 5 分钟执行一次 Same-Token  
	@Scheduled(cron = "0 0/5 * * * ? ")
	public void refreshToken(){
		SaSameUtil.refreshToken();
	}
}
```

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

##### 3、如果网关携带token转发的请求在落到子服务的节点上时，恰好刷新了token，导致鉴权未通过怎么办？
Same-Token 模块在每次刷新 Token 时，旧 Token 会被作为次级 Token 存储起来，
只要网关携带的 Token 符合新旧 Token 其一即可通过认证，直至下一次刷新，新 Token 再次作为次级 Token 将此替换掉。

---

### Sa Token Doc New Docs More Common Action (`sa-token-doc-new/docs/more/common-action.md`)

---
title: "Sa-Token 全局类、方法"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,全局类、方法,API手册"
description: "Sa-Token 全局类与方法：SaManager 管理配置、Dao、StpLogic 等组件；SaHolder 取当前 Request、Storage；SaRouter 路由鉴权；SaFoxUtil 提供内部工具。另含配置工厂、SpringMVCUtil 与 Reactor Holder，以及事件中心与参数签名入口。"
---

# 全局类、方法
本篇介绍 Sa-Token 中一些常用的全局对象、类

--- 

### SaManager
SaManager 负责管理 Sa-Token 所有全局组件。
```
/* Detailed source-code truncated for AI context efficiency. */
```

侦听器注册请使用 `SaTokenEventCenter.registerListener(listener)`，参数签名模块请使用 `SaSignManager.getSaSignTemplate()`（见 [参数签名](/plugin/api-sign) 章节）。


### SaHolder
Sa-Token上下文持有类，通过此类快速获取当前环境的相关对象 
``` java
SaHolder.getContext();           // 获取当前请求的 SaTokenContext
SaHolder.getRequest();           // 获取当前请求的 [Request] 对象 
SaHolder.getResponse();          // 获取当前请求的 [Response] 对象 
SaHolder.getStorage();           // 获取当前请求的 [Storage] 对象
SaHolder.getApplication();       // 获取全局 SaApplication 对象
```


### SaRouter
路由匹配工具类，详细戳：[路由拦截式鉴权](/use/route-check)


### SaFoxUtil
Sa-Token内部工具类，包含一些工具方法 
``` java
SaFoxUtil.printSaToken();           // 打印 Sa-Token 版本字符画
SaFoxUtil.getRandomString(8);       // 生成指定长度的随机字符串
SaFoxUtil.isEmpty(str);             // 指定字符串是否为null或者空字符串
SaFoxUtil.isNotEmpty(str);          // 指定字符串是否不是null或者空字符串
SaFoxUtil.equals(a, b);             // 比较两个对象是否相等 
SaFoxUtil.getMarking28();           // 以当前时间戳和随机int数字拼接一个随机字符串
SaFoxUtil.formatDate(date);         // 将日期格式化为yyyy-MM-dd HH:mm:ss字符串
SaFoxUtil.searchList(dataList, prefix, keyword, start, size, sortType);             // 从集合里查询数据
SaFoxUtil.searchList(dataList, start, size, sortType);       // 从集合里查询数据
SaFoxUtil.vagueMatch(patt, str);    // 字符串模糊匹配
SaFoxUtil.getValueByType(obj, cs);    // 将指定值转化为指定类型
SaFoxUtil.joinParam(url, parameStr);    // 在url上拼接上kv参数并返回 
SaFoxUtil.joinParam(url, key, value);    // 在url上拼接上kv参数并返回 
SaFoxUtil.joinSharpParam(url, parameStr);    // 在url上拼接锚参数 
SaFoxUtil.joinSharpParam(url, key, value);    // 在url上拼接锚参数 
SaFoxUtil.arrayJoin(arr);    // 将数组的所有元素使用逗号拼接在一起
SaFoxUtil.isUrl(str);    // 使用正则表达式判断一个字符串是否为URL
SaFoxUtil.encodeUrl(str);    // URL编码 
SaFoxUtil.decoderUrl(str);    // URL解码 
SaFoxUtil.convertStringToList(str);    // 将指定字符串按照逗号分隔符转化为字符串集合 
SaFoxUtil.convertListToString(list);    // 将指定集合按照逗号连接成一个字符串 
SaFoxUtil.convertStringToArray(str);    // String 转 Array，按照逗号切割 
SaFoxUtil.convertArrayToString(arr);    // Array 转 String，按照逗号切割 
SaFoxUtil.emptyList();    // 返回一个空集合
SaFoxUtil.toList(... strs);    // String 数组转集合 
```


### SaTokenConfigFactory
配置对象工厂类，通过此类你可以方便的根据 properties 配置文件创建一个配置对象 

1、首先在项目根目录，创建一个配置文件：`sa-token.properties`

``` properties
# token 名称 (同时也是 cookie 名称)
tokenName=satoken
# token 有效期（单位：秒） 默认30天，-1 代表永久有效
timeout=2592000
# token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结
activeTimeout=-1
# 是否允许同一账号多地同时登录 （为 true 时允许一起登录, 为 false 时新登录挤掉旧登录）
isConcurrent=true
# 在多人登录同一账号时，是否共用一个 token （为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token）
isShare=false
# token 风格（默认可取值：uuid、simple-uuid、random-32、random-64、random-128、tik）
tokenStyle=uuid
# 是否输出操作日志 
isLog=false
```

2、然后使用以下代码获取配置对象 
``` java
// 设置配置文件地址 
SaTokenConfigFactory.configPath = "sa-token.properties";

// 获取配置信息到 config 对象
SaTokenConfig config = SaTokenConfigFactory.createConfig();

// 注入到 SaManager 中
SaManager.setConfig(config);
```


### SpringMVCUtil
SpringMVC操作的工具类，位于包：`sa-token-spring-boot-starter`
``` java
SpringMVCUtil.getRequest();           // 获取本次请求的 request 对象 
SpringMVCUtil.getResponse();          // 获取本次请求的 response 对象 
SpringMVCUtil.isWeb();                // 判断当前是否处于 Web 上下文中  
```


### SaReactorHolder & SaReactorSyncHolder
Sa-Token集成Reactor时的 ServerWebExchange 工具类，位于包：`sa-token-reactor-spring-boot-starter`
``` java
// 异步方式获取 ServerWebExchange 对象 
SaReactorHolder.getMonoExchange().map(e -> {
	System.out.println(e);
	return e;
});

// 在 Mono 链中调用 Sa-Token 同步 API（返回 Mono<R>，Controller 中需 return）
return SaReactorHolder.sync(() -> {
	StpUtil.checkLogin();
	return SaResult.ok();
});

// 同步方式（WebFilter 等场景）
SaReactorSyncHolder.setContext(exchange);   // 写入当前请求的 ServerWebExchange
SaReactorSyncHolder.getExchange();          // 获取当前请求的 ServerWebExchange
SaReactorSyncHolder.clearContext();         // 清除当前上下文
SaReactorSyncHolder.setContext(exchange, () -> {});   // 在指定 exchange 上下文中执行代码块并返回结果
```

---

### Sa Token Doc New Docs More Common Questions (`sa-token-doc-new/docs/more/common-questions.md`)

---
title: "Sa-Token 常见问题排查"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,常见问题排查,附录"

description: "Sa-Token 常见问题排查：整理群聊高频报错与配置坑，覆盖 SaTokenContext 未初始化（异步、404 转发 /error、SSE/Flux）、NotLoginException 读不到 Token、注解与拦截器不生效、Redis 或 JWT 导致 Token 无效等对应解法。欢迎补充 PR。"
---

# 常见问题排查
本篇整理大家在群聊里经常提问的一些问题，如有补充，欢迎提交pr

[[toc]]

--- 



## 一、常见报错


### Q：报错：SaTokenContext 上下文尚未初始化

可能1：:你在 异步上下文 / 响应式上下文 里调用了 Sa-Token 的同步 API，解决方案参考：[异步 & Mock 上下文](/fun/async--mock)

可能2：访问了一个不存在的路由，而且 SaInterceptor 拦截器里有鉴权代码。

SpringBoot 默认会把 404 请求转发到 `/error`，如果恰好 SaInterceptor 里有鉴权代码，就会造成：

写入上下文 → 进入拦截器(有上下文，可调用鉴权代码) → 发现是404 → 清除上下文 → 
将请求转发至 /error -> 再次进入拦截器(无上下文，不可调用鉴权代码) → 报错：SaTokenContext 上下文尚未初始化。

解决方案：将 "/error" 地址排除在拦截器之外：

``` java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new SaInterceptor(handle -> {
			// 鉴权代码 ...
		}))
		.addPathPatterns("/**")
		.excludePathPatterns("/error");
	}
}
```

可能3：SpringMVC 项目里使用了 `SseEmitter` / `Flux` 等异步、流式返回，参考下面这条 QA。


### Q：SpringMVC 下使用 SSE / Flux 流式返回时报错：SaTokenContext 上下文尚未初始化？

常见于未引入 WebFlux、仅个别接口返回 `SseEmitter`、`Flux` 的场景。

- **v1.46.0 起**：框架已在 `SaTokenContextFilter` 注册时原生支持 `REQUEST` + `ASYNC` dispatch，一般无需额外配置。
- **v1.45.0 及以下**：参考 Issue 排查与临时规避：[升级后 SSE 请求报 SaTokenContext 上下文尚未初始化](https://gitee.com/dromara/sa-token/issues/IC4XFE)


### Q：报错：NotLoginException：xxx

这个错是说明调用接口的人没有通过登录校验，请注意：**通常情况下，异常提示语已经描述清楚了没有通过校验的具体原因：**
   
**如果是：未能读取到有效Token**
- 可能1：前端没有提交 Token（最好从前端f12控制台看看请求参数里有 token 吗）。
- 可能2：前端提交了 Token，但是参数名不对。默认参数名是 `satoken`，可通过配置文件 `sa-token.token-name: satoken` 来更改。
- 可能3：前端提交了 Token，但是你配置了框架不读取，比如说你配置了 `is-read-header=false`（关闭header读取），此时你再从 header 里提交token，框架就无法读取到。
- 可能4：前端提交了 Token，但是 Token前缀 不对，可参考：[自定义 Token 前缀](/up/token-prefix)
- 可能5：你的项目属于前后端分离架构，此时浏览器默认不自动提交 Cookie，参考：[前后端分离](/up/not-cookie) 
- 可能6：你使用了 Nginx 反向代理，而且配置了 自定义Token名称，而且自定义的名称还带有下划线（比如 shop_token），而且还是你的项目还是从 Header头提交Token的，此时 Nginx 默认会吞掉你的下划线参数，可参考：[nginx做转发时，带下划线的header参数丢失](https://blog.csdn.net/zfw_666666/article/details/124420828)
- 可能7：可能是跨域了，导致前端提交不上 token，看看前端浏览器有没有跨域的报错。

**如果是：Token无效：6ad93254-b286-4ec9-9997-4430b0341ca0**
- 可能1：前端提交的 token 是乱填的，或者从别的项目拷过来的，或者多个项目一起开发时彼此的 Token 串项目了。
- 可能2：前端提交的 token 已过期（timeout超时了）。
- 可能3：在不集成 Redis 的情况下：颁发 token 后，项目重启了，导致 token 无效。
- 可能4：在集成 Redis 的情况下：颁发 token 后，Redis重启了，导致 token 无效。
- 可能5：你提交的 token 和框架读取到的 token 不一致：
	- 可能5.1：比如说你配置了`is-read-header=false`（关闭header读取），然后你从header提交`token-A`，而框架从Cookie里读取`token-B`，导致鉴权不通过（框架读取顺序为`body->header->cookie`）
	- 可能5.2：比如说你配置了`token-name=x-token`（自定义token名称），此时你从header提交：`satoken:token-A`（参数名没对上），然后框架从header里读取不到你提交的token，转而继续从Cookie读取到了`token-B`。
- 可能6：在集成 jwt 插件的情况下：
	- 如果使用的是 Simple 模式：情况和不集成jwt一样。
	- 如果使用的是 Mixin 和 Stateless 模式：查看这个 token 颁发后是否更改了 `jwtSecretKey` 配置项。
- 可能7：同一账号登录数量超过12个，导致最先登录的被强制注销掉，这个值可以通过 `maxLoginCount` 来配置，默认值12，-1代表不做限制。
- 可能8：在配置了 `is-concurrent=true, is-share=true`的情况下，你和别人共同登录了同一账号，此时对方注销了登录，由于你们使用的是同一个token，导致你这边的会话也失效了。
- 可能9：可能是多账号鉴权的关系，在多账号模式下，如果是 `StpUserUtil.login()` 颁发的token，你从 `StpUtil.checkLogin()` 进行校验，永远都是无效token，因为账号体系没对上。

**如果是：Token已过期：6ad93254-b286-4ec9-9997-4430b0341ca0**
- 可能1：前端提交的 token 已被冻结（active-timeout超时了，比如配置了 active-timeout=120，但是超过了120秒没有访问接口）。
- 可能2：集成jwt，而且使用的是 Mixin 或 Stateless 模式，而且token过期了（timeout超时了）。

**如果是：Token已被顶下线：6ad93254-b286-4ec9-9997-4430b0341ca0**
- 可能1：在项目配置了 `is-concurrent=false` 的前提下，这个账号又被别人登录了，导致旧登录被挤了下去。
- 可能2：这个账号被 `StpUtil.replaced(loginId, device)` 方法强制顶下线了。

**如果是：Token已被踢下线：6ad93254-b286-4ec9-9997-4430b0341ca0**
- 可能1：这个账号被 `StpUtil.kickout(loginId)` 方法强制踢下线了。


### Q：加了注解进行鉴权认证，不生效？
1. 注解鉴权功能默认关闭，两种方式任选其一进行打开：注册注解拦截器、集成AOP模块，参考：[注解式鉴权](/use/at-check)
2. 在Spring环境中, 如果同时配置了`WebMvcConfigurer`和`WebMvcConfigurationSupport`时, 也会导致拦截器失效.
   - **常见场景**: 很多项目中会在`WebMvcConfigurationSupport`中配置`addResourceHandlers`方法开放Swagger等相关静态资源映射, 同时基于Sa-Token添加了`WebMvcConfigurer`配置`addInterceptors`方法注册注解拦截器, 这样会导致注解拦截器失效. 
   - **解决方案**: `WebMvcConfigurer`和`WebMvcConfigurationSupport`只选一个配置, 建议统一通过实现`WebMvcConfigurer`接口进行配置.
3. 如果以上步骤处理后仍然没有效果，加群说明一下复现步骤 



### Q：我加了拦截器鉴权，但是好像没有什么效果，请求没有被拦截住？
- 可能1：这个拦截器可能没有注册成功。
- 可能2：你访问的请求没有进入这个拦截器。

尝试按照下面的代码测试一下看看：

``` java
// 注册拦截器 
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		System.out.println("--------- flag 1");
		registry.addInterceptor(new SaInterceptor(handle -> {
			System.out.println("--------- flag 2，请求进入了拦截器，访问的 path 是：" + SaHolder.getRequest().getRequestPath());
			StpUtil.checkLogin();  // 登录校验，只有会话登录后才能通过这句代码 
		}))
		.addPathPatterns("/user/**")
		.excludePathPatterns("/user/doLogin");
	}
}
```

在启动时 `flag 1` 被打印出来，才证明拦截器注册成功了，在访问请求时 `flag 2` 被打印出来，才证明请求进入了拦截器。

如果拦截器没有注册成功，则：

- 可能1：`SaTokenConfigure` 配置类不在启动类的同包或者子包下，导致没有被 SpringBoot 扫描到。
- 可能2：你的项目启动类上加了 `@ComponentScan("com.xxx")` 注解，导致包扫描范围不正确，请将此注解删除或移动到其它配置类上。
- 可能3：项目属于 Maven 多模块项目，`SaTokenConfigure` 和启动类没有在一个模块，且启动类模块没有引入配置类的模块，导致加载不到。

如果拦截器已经注册成功，但请求没有进入拦截器：
- 可能1：你访问的 path，没有被 `.addPathPatterns("/user/**")` 拦截住，或者被 `.excludePathPatterns("/xxx/xx")` 排除掉了。
- 可能2：你访问的是另一个项目，请把当前项目停掉，看看你的请求还能不能访问成功。

如果请求进入拦截器也成功了，那可能是：
- 可能1：前端访问时提交了会话 Token，且这个 Token 是有效的，通过了拦截器的代码校验。
- 可能2：你访问的 path，和你预期不符，仔细观察一下打印出来的 path 信息，和你的预期相符吗。

注：以上的排查步骤，对过滤器不生效的情形一样适用。


### Q：我使用拦截器鉴权时，明明排除了某个路径却仍然被拦截了？
- 可能1：你的项目可能是跨域了，先把跨域问题解决掉，参考：[解决跨域问题](/fun/cors-filter)
- 可能2：你访问的接口可能是404了，SpringBoot环境下如果访问接口404后，会被转发到`/error`，然后被再次拦截。请确保你访问的 path 有对应的 Controller 承接！
- 可能3：可能拦截器这里并没有拦截，但是又被其他地方拦截了。请先把这个拦截器给注释掉，看看还会不会拦截，如果依然拦截，那说明不是这个拦截器的锅，请仔细查看一下控制台抛出的堆栈信息，定位一下到底是哪行代码拦截住这个请求的。
- 可能4：后端拦截的 path 未必是你前端访问的这个path（特别是经过网关转发后的path可能会有变化），建议先打印一下 path 信息，看看和你预想的是否一致，再做分析。
``` java
@Override
public void addInterceptors(InterceptorRegistry registry) {
	registry.addInterceptor(new SaInterceptor(handle -> {
		try {
			System.out.println("-------- 前端访问path：" + SaHolder.getRequest().getRequestPath());
			StpUtil.checkLogin();
			System.out.println("-------- 此 path 校验成功：" + SaHolder.getRequest().getRequestPath());
		} catch (Exception e) { 
			System.out.println("-------- 此 path 校验失败：" + SaHolder.getRequest().getRequestPath());
			throw e;
		}
	})).addPathPatterns("/**"); 
}
```
- 可能5：可能你只提交了一个请求，但是浏览器自动帮你提交了其它请求，举个例子：首次访问网站时，浏览器一般会自动提交 `/favicon.ico`，所以**你需要找出是哪个path被拦截了**，怎么找呢？用【可能4】的代码来测试找。
- 可能6：你的项目配置了 `context-path` 上下文地址，比如 `server.servlet.context-path=/shop`，注意这个地址是不需要加在拦截器上的：
``` java
// 这是错误示例，不需要把 context-path 上下文参数写在下面的 excludePathPatterns 地址上。
registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin()))
			.addPathPatterns("/**").excludePathPatterns("/shop/user/login");
// 这是正确示例，无论你的 context-path 上下文配置了什么样的值，下面的 excludePathPatterns 地址都不需要写上它
registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin()))
			.addPathPatterns("/**").excludePathPatterns("/user/login");
```
- 可能7：你写了多个匹配规则，请求越过了第一个规则，但又被其它规则拦下来了，例如以下代码：
``` java
// 以下代码，当你未登录访问 `/user/doLogin` 时，会被第1条规则越过，然后被第2条拦下，校验登录，然后抛出异常：`NotLoginException：xxx`
registry.addInterceptor(new SaInterceptor(handler -> {
	SaRouter.match("/**").notMatch("/user/doLogin").check(r -> StpUtil.checkLogin());  // 第1个规则 
	SaRouter.match("/**").notMatch("/article/getList").check(r -> StpUtil.checkLogin());  // 第2个规则 
	SaRouter.match("/**").notMatch("/goods/getList").check(r -> StpUtil.checkLogin());  // 第3个规则 
})).addPathPatterns("/**");
```
- 可能8：你自定义的封装方法，并没有按照你的预想情况执行：
``` java
public void addInterceptors(InterceptorRegistry registry) {
	registry.addInterceptor(new SaInterceptor(handle -> {
		// 调用自定义的 excludePaths() 方法获取数据排除鉴权  
		SaRouter.match("/**").notMatch(excludePaths()).check(r -> StpUtil.checkLogin());
	})).addPathPatterns("/**");
}
// 自定义查询排查鉴权的地址方法 
public static List<String> excludePaths() {
	List<String> list = ... // 从数据源查询...;
	return list;
}
```
如上方法， `excludePaths()` 可能并不会像你预想的一样正确执行返回相应的值，请在 `.notMatch()` 处 `一律先硬编码写固定死值来测试`，这时就有两种情况：
	- 情况1：写固定死值时，代码能正常执行了，那说明你自定义的 `excludePaths()` 方法有问题，执行结果不正确。
	- 情况2：写固定也不行，那说明不是 `excludePaths()` 的问题，那再从其它地方开始排查。



### Q：我在配置文件中加了一些关于 Sa-Token 的配置，但是没有生效。
首先，有没有生效的最佳判断方式是，在main方法中加一个打印，看看打印出来的和你配置文件的一致吗：

``` java
@SpringBootApplication
public class SaTokenApplication {
	public static void main(String[] args) {
		SpringApplication.run(SaTokenApplication.class, args); 
		System.out.println("\n启动成功：Sa-Token配置如下：" + SaManager.getConfig());
	}
}
```

如果不一致，请排查：
- 可能1：项目中还存在代码配置，而代码配置会覆盖 `application.yml` 中配置，详细参考：[框架配置](/use/config)。
- 可能2：你的配置文件名字错误，SpringBoot 项目正常情况下配置文件名称应该是：`application.yml` 或 `application.properties`。
- 可能3：可能是你的配置前缀不对，或者配置缩进不对：
``` yaml
# 错误示例，多加了 spring 前缀
spring:
	sa-token: 
		token-name: xxx-token
# 错误示例，缩进不对
sa-token: 
token-name: xxx-token
# 正确的应该是以 sa-token 开头
sa-token: 
	token-name: xxx-token
```


### Q：我自定义了组件，但是好像没有生效？
1、可能组件没有注入成功，排查方法为在 main 里打印这个组件，是否为自定义的class限定名：
``` java
@SpringBootApplication
public class SaTokenApplication {
	public static void main(String[] args) {
		SpringApplication.run(SaTokenApplication.class, args); 
		System.out.println(SaManager.getStpInterface());  // 打印全局的 StpInterface 实现类 
	}
}
```
如果打印出的是你的自定义实现类，则证明注入成功，如果不是，则证明没有注入成功，请排查：
- 自定义的组件实现类上是否加上了 `@Component` 注解，只有加上这个注解，组件才会被 Spring 自动实例化并注入。
- 自定义的组件实现类是否在启动类的同目录或者子目录上，如果不在则无法被 springboot 启动时扫描，扫描不到也就无法注入。
- 启动类上是否加了 `@ComponentScan` 注解，导致包扫描范围不正确，请将此注解删除或移动到其它配置类上。

2、这个组件注入成功了，但是还没到执行时机，比如 `StpInterface` 组件，只有在鉴权时才会触发，如果你的代码仅仅是登录校验，就不会执行到这个组件。


### Q：集成 Redis 后，明明 Redis 中有值，却还是提示无效Token？

根据以往的处理经验，发生这种情况 90% 的概率是因为你找错了Redis，即：代码连接的Redis和你用管理工具看到的Redis并不是同一个。

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

我的回答是：别光看配置文件，不一定准确，在启动时直接执行 `SaManager.getSaTokenDao().set("name", "value", 100000);`，
随便写入一个值，看看能不能根据你的预期写进这个Redis，如果能的话才能证明`代码连接的Redis` 和`你用管理工具看到的Redis` 是同一个，再进行下一步排查。


### Q：报错：无效Same-Token：xxxxxxxxxxx
与之类似的的报错还有：
- SSO模式二时，报错：无效ticket：xxxxxxxxxx
- OAuth2模块跨多个项目搭建Server时：报错无效 Access-Token：xxxxxx
- 微服务做分布式 Session 认证时，报错：无效 Token：xxxxxxxxx
- 等等等等.... 

这些功能有个统一的特点，就是需要多个项目连接同一个 Redis 才能搭建成功，如果连接的不是同一个 Redis，就会导致 Token / ticket 无法互相认证。

你可能会问：我看配置文件明明就是连接的同一个 Redis 啊？

别急，和上一个问题一样，**不要凭借肉眼检查下定论**，在你的两个服务之间，分别使用以下代码测试一下：

``` java
@SpringBootApplication
public class SaTokenApplication {
	public static void main(String[] args) {
		SpringApplication.run(SaTokenApplication.class, args);
		// 写值测试：注意一定要用下列方法测试，不要用自己封装的 RedisUtil 之类的测试 
		SaManager.getSaTokenDao().set("name", "value", 100000); 
	}
}
```

如果都能根据你的预期写进同一个 Redis，那才能证明两个服务确实连接的是同一个 Redis。

实际上，在交流群中提问这些问题的同学，90%的经过以上测试以后，都会发现两者连接的不是同一个 Reids，原因大多是：Redis配置没有生效、使用了 Alone-Redis 之类的……

如果你是剩下的 10%，那么继续排查：两边的 sa-token 配置是否完全一致，比如 token-name 配置不一致，也会导致数据无法相互认证。最好是把所有 sa-token 相关的配置都复制过去，试验一下看看。



### Q：我把 token 有效期设置为 30 天，但是总感觉不到 30 天的时候 token 就无效了，怎么回事？
- 可能1：你没有为 sa-token 集成 Redis，框架默认将会话数据保存在内存中，项目重启后数据会消失。
- 可能2：你为 sa-token 集成了 Redis，但是 Redis 重启了，导致会话消失。
- 可能3：你配置了 `is-concurrent=false`，不允许同一账号多端登录，有别人登录了这个账号把你顶下去了。
- 可能4：你配置了 `is-concurrent=true`，但是`is-share=false`，同一账号每次登录产生不同的 token，默认最高可以同时登录12个客户端，超过将自动注销最原先的会话。
- 可能5：你的这个账号，别人也登录了，别人调用了注销方法，把你这边的也注销了。`StpUtil.logout()` 为单 token 注销，`StpUtil.logout(10001)` 为账号所有 token 注销。
- 可能6：你虽然 `sa-token.timeout` 配置了 30 天，但是 `sa-token.active-timeout` 配置了较短的值，超过这个时间无操作，token 就过期了。
- 可能7：你换了浏览器，或者换了电脑，或者清空了浏览器最近缓存记录，自然而然需要重新登录。
- 可能8：你中途改了项目配置，比如改了 `sa-token.token-name` 配置项的值，会导致会话保存的 key 发生改变，效果等同于手动清空了 Redis 数据，需要重新登录。



### Q：有时候我不加 Token 也可以通过鉴权，请问是怎么回事？
- 可能1：你访问的这个接口，根本就没有鉴权的代码，所以可以安全的访问通过。
- 可能2：可能是 Cookie 帮你自动提交了 Token，在浏览器或 Postman 中会自动维护Cookie模式，如不需要可以在配置文件：`is-read-cookie: false`，然后重启项目再测试一下。


### Q：一个 User 对象存进 Session 后，再取出来时报错：无法从 User 类型转换成 User 类型？
- 可能1：你的 User 类中途换了包名，导致存进去时和取出来时对不上，无法成功创建实例。
- 可能2：你打开了代码热刷新模式，先存进去的对象，热刷新后再取出，会报错，关闭热刷新即可解决。


### Q：在 SaServletFilter 中调用 SpringMVCUtil.getRequest() 报错：非Web上下文无法获取Request？

- 可能1：项目中有配置类继承了： `extends WebMvcConfigurationSupport`。
- 可能2：项目中有配置类添加了注解： `@EnableWebMvc`。

解决方案：不要加 `@EnableWebMvc`，不要 `extends WebMvcConfigurationSupport`，要 `implements WebMvcConfigurer`

如果一定要 `extends WebMvcConfigurationSupport` ，可以通过手动注册 Spring 上下文初始化过滤器试试：

``` java
@Configuration
public class SaTokenConfigure extends WebMvcConfigurationSupport {

	// Spring 上下文初始化过滤器 可能由于各种原因没有被注册到，这里手动帮忙注册一下 
	@Bean
	@ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class })
	@ConditionalOnMissingFilterBean(RequestContextFilter.class)
	public static RequestContextFilter requestContextFilter() {
		System.out.println("--------------------------- 注册了"); // 加个打印语句或者断点确保这里注册到了
		return new OrderedRequestContextFilter();
	}
	
}
```


如果不是以上原因，可以加群提供复现demo。




### Q：我配置了 active-timeout 值，但是当我每次续签时 Redis 中的 ttl 并没有更新，是不是 bug 了？
不更新是正常现象，`active-timeout`不是根据 ttl 计算的，是根据value值计算的，value 记录的是该 Token 最后访问系统的时间戳，
每次验签时用：当前时间 - 时间戳 > active-timeout，来判断这个 Token 是否已经超时。 


### Q：整合 Redis 时先选择了默认jdk序列化，后又改成 jackson 序列化，程序开始报错，SerializationException？
两者的序列化算法不一致导致的反序列化失败，如果要更改序列化方式，则需要先将 Redis 中历史数据清除，再做更新。


### Q：报错：无法反序列化的类型：com.pj.model.SysUser，请先将其注册到 JSON 全局类型白名单？

说明 Session 或 Redis 中的 JSON 在反序列化时，遇到了 **未加入白名单** 的业务实体类型。  
常见于：`session.set("user", sysUser)` 后集成 Redis，或使用 `SaTokenDao.getObject()` 读回自定义 Model。

**原因与解决方案** 详见：[JSON 序列化扩展 - JSON 全局类型白名单](/plugin/json-extend#JSON-全局类型白名单机制)


### Q：升级 sa-token-redisson 后登录失效 / 读 Redis 报错？
`SaTokenDaoForRedisson` 已改为默认使用 `StringCodec`，与升级前跟随 Redisson 全局 codec（一般为 `Kryo5Codec`）不兼容。请清空 Redis 中的 Sa-Token 旧缓存，或构造时传入 `new Kryo5Codec()`。详见：[集成 Redis - 集成 Redisson](/up/integ-redis#5、集成-Redisson)。


### Q：Redis 6.0 以下版本集成报错：ERR syntax error
自 v1.46.0 起，当 Redis 6.0 以下集成时，可能会发生报错：

```
io.lettuce.core.RedisCommandExecutionException: ERR syntax error
org.redisson.client.RedisException: ERR syntax error. command: (SET), params: [key, value, KEEPTTL]
```

这是因为 Redis 集成插件使用了 `SET KEEPTTL` 高级特性（原子覆写 value 并保留原 TTL），如果你的 Redis 服务低于 6.0 版本，将会报错。

解决方案有两种：

1、将 Redis 升级到 6.0+（推荐）

2、重写相关组件，恢复旧写法：

如果你使用的 sa-token-redis-template 插件，请这样重写：

``` java
@Configuration
public class SaTokenDaoConfig {
	@Bean
	@Primary
	public SaTokenDao saTokenDao() {
		return new SaTokenDaoForRedisTemplate() {
			@Override
			public void update(String key, String value) {
				String finalKey = wrapKey(key);
				long expireMs = stringRedisTemplate.getExpire(finalKey, TimeUnit.MILLISECONDS);
				// -2 = 无此键
				if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) {
					return;
				}
				// -1 = 永不过期
				if (expireMs == SaTokenDao.NEVER_EXPIRE) {
					stringRedisTemplate.opsForValue().set(finalKey, value);
				} else {
					stringRedisTemplate.opsForValue().set(finalKey, value, expireMs, TimeUnit.MILLISECONDS);
				}
			}
		};
	}
}
```

如果你使用的是 sa-token-redis-template-jdk-serializer 插件，把上面的 `SaTokenDaoForRedisTemplate` 换成 `SaTokenDaoForRedisTemplateUseJdkSerializer`，并额外重写 `updateObject`（写法相同，把 `stringRedisTemplate` 换成 `objectRedisTemplate`）。

如果你使用的是 sa-token-redisson 相关插件，请这样重写：

``` java
@Configuration
public class SaTokenDaoConfig {
	@Bean
	@Primary
	public SaTokenDao saTokenDao(RedissonClient redissonClient) {
		return new SaTokenDaoForRedisson(redissonClient) {
			@Override
			public void update(String key, String value) {
				RBucket<String> bucket = redissonClient.getBucket(key, codec);
				long expireMs = bucket.remainTimeToLive();
				// -2 = 无此键
				if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) {
					return;
				}
				// -1 = 永不过期
				if (expireMs == SaTokenDao.NEVER_EXPIRE) {
					bucket.set(value);
				} else {
					bucket.set(value, Duration.ofMillis(expireMs));
				}
			}
		};
	}
}
```

> 旧写法会先读 TTL 再 SET，极端并发下可能有毫秒级偏差；能升级 Redis 的话，优先升级。


### Q：调用 `StpUtil.getExtra("name")` 报错：`this api is disabled`。
`StpUtil.getExtra(key)` 是给 sa-token-jwt 插件提供的，不集成这个插件就不能调用这个API，如果是普通模式需要存储自定义参数，请在 SaSession 上存储

``` java
// 在登录时缓存参数
StpUtil.getSession().set("name", "zhangsan");

// 然后我们就可以在任意处获取这个参数 
String name = StpUtil.getSession().getString("name");
```


### Q：我加了 Sa-Token 的全局过滤器，浏览器报错跨域了怎么办？
参考：[https://juejin.cn/post/7247376558367981627](https://juejin.cn/post/7247376558367981627)


### Q：前后端分离项目中，前端使用 vue，如果不打开 proxy 代理的话，调用 Sa-Token 登录不会将 token 自动注入到 Cookie 中，是因为跨域么？
是。

参考：[前后端分离](/up/not-cookie) 


### Q：集成redis后对象模型序列化异常
假设执行如下代码:
``` java
@Data
public class User implements Serializable {
    private Long userId;
    private String username;
    private String password;
}
```

``` java
User user = new User();
user.setUserId(10000L);
user.setUsername("oneName");
user.setPassword("onePass");        
StpUtil.getSession().set("userObjKey", user); // 这里报错
```
报错信息如下:
```
SerializationException: Could not read JSON: 
Cannot deserialize value of type `java.lang.Long` from Array value (token `JsonToken.START_ARRAY`)
```

Springboot 集成 Sa-Token Redis 后, 一旦 Springboot 切换版本就有可能出现此问题

原因是 Redis 里面有之前的 Sa-Token 会话数据, 清空 Redis 即可。



### Q：我实现了 StpInterface 接口，但是在登录时没有进入我的实现类代码？
不进入是正常现象， StpInterface 是鉴权接口，在执行鉴权代码时才会进入 StpInterface 实现类，登录认证时不会进入。


### Q：启动时报错，找不到 xx 类 xx 方法：
``` java
Caused by: java.lang.ClassNotFoundException: cn.dev33.satoken.same.SaSameTemplate
```

一般找不到类，或者找不到方法，都是版本冲突了，使用 Sa-Token 时一定要注意**版本对齐**，意思是所有和 Sa-Token 相关的依赖都需要版本一致。

比如说你如果一个依赖是 1.32.0，一个是 1.31.0，就会造成无法启动：

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-boot-starter</artifactId>
	<version>1.32.0</version>
</dependency>
<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-core</artifactId>
	<version>1.31.0</version>
</dependency>
```

请仔细排查你的 pom.xml 文件，是否有 Sa-Token 依赖没对齐，**请不要肉眼检查，用全局搜索 "sa-token" 关键词来找**，如果是多模块或者微服务项目，就整个项目搜索。


### Q：在多账号模式的注解鉴权时，报错：未能获取对应StpLogic，type=xxx

报这个错说明对应 type 的 StpLogic 尚未初始化到全局 StpLogicMap 中，一般会有两种原因造成这种情况：
1. 注解里的 loginType 拼写错误，请改正 （建议使用常量）。
2. 自定义 StpUtil 尚未初始化（静态类中的属性至少一次调用后才会初始化），解决方法两种：
	- (1) 从main方法里调用一次
	- (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 


### Q：使用拦截器鉴权，访问一个不存在的 path 时，springboot 会自动在控制台打印一下异常。
可尝试添加以下配置解决：
``` properties
spring.web.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true
```




### Q：开启了全局懒加载后，能启动项目，但是访问接口报“未能获取有效的上下文处理器”
开启了全局懒加载后，能启动项目，但是访问接口报异常 `InvalidContextException`: 未能获取有效的上下文处理器, 配置如下：
``` yaml
spring:
  main:
    lazy-initialization: true
```
原因是 Sa-Token 自动配置入口类 SaBeanInject 被延迟加载了，只需要手动指定懒加载排除掉 SaBeanInject 就可以了,实现代码如下:
``` java
@Configuration
class MyConfiguration {
    @Bean
    LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
        return LazyInitializationExcludeFilter.forBeanTypes(SaBeanInject.class);
    }
}
```
[经验来源](https://gitee.com/dromara/sa-token/issues/I7EXIU)


### Q：SpringBoot 3.x 路由拦截鉴权报错：No more pattern data allowed after {*...} or ** pattern element


报错原因：SpringBoot3.x 版本默认将路由匹配机制由 `ant_path_matcher` 改为了 `path_pattern_parser` 模式，
而此模式有一个规则，就是写路由匹配符的时候，不允许 `**` 之后再出现内容。例如：`/admin/**/info` 就是不允许的。

如果你的项目报了这个错，说明你写的路由匹配符出现了上述问题，有三种解决方案：
1. 等待 SpringMVC 官方增强 `path_pattern_parser` 模式能力，使之可以支持 `**` 之后再出现内容。
2. 在写路由匹配规则时，避免使 `**` 之后再出现内容。
3. 将项目的路由匹配机制改为 `ant_path_matcher`。

步骤1：先改项目的：
``` yml
spring:
    mvc:
        pathmatch:
            matching-strategy: ant_path_matcher
```

步骤2：再改 Sa-Token 的：
``` java
/**
 * 重写路由匹配算法，切换为 ant_path_matcher 模式，使之可以支持 `**` 之后再出现内容
 */
@PostConstruct
public void customRouteMatcher() {
	SaStrategy.instance.routeMatcher = (pattern, path) -> {
		return SaPatternsRequestConditionHolder.match(pattern, path);
	};
}
```

**注意点：**

SpringBoot2.x 的 `WebFlux`或 `SC Gateway` 项目，按照上述步骤改造，可能会报错 

``` html
java.lang.NoClassDefFoundError: org/springframework/web/servlet/mvc/condition/PatternsRequestCondition
```

只需要将“步骤2”中的代码 `return SaPatternsRequestConditionHolder.match(pattern, path);` 
更换为 `return SaPathMatcherHolder.getPathMatcher().match(pattern, path);` 即可，例如：

``` java
/**
 * 重写路由匹配算法，切换为 ant_path_matcher 模式，使之可以支持 `**` 之后再出现内容
 */
@PostConstruct
public void customRouteMatcher() {
	SaStrategy.instance.routeMatcher = (pattern, path) -> {
		return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
	};
}
```


### Q：Webflux 环境集成，或者 SpringCloud Gateway 环境集成后，过滤器里路由拦截鉴权报错：`java.lang.NoSuchFieldError: defaultInstance`

``` java
java.lang.NoSuchFieldError: defaultInstance
	at cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil.match(SaPathPatternParserUtil.java:40)
	at cn.dev33.satoken.reactor.spring.SaTokenContextForSpringReactor.matchPath(SaTokenContextForSpringReactor.java:34)
	at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:58)
	at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:72)
	... 
```

原因：SpringBoot 版本用的太低了，导致一些类不存在。

- 方案一：升级项目的 SpringBoot 版本至 `2.3.x` 以上
- 方案二：像上面的问题解决方案一样，重写一下相关类：

``` java
/**
 * 重写路由匹配算法，将 PathPatternParser.defaultInstance 改为 SaPathMatcherHolder.getPathMatcher()
 */
@PostConstruct
public void customRouteMatcher() {
	SaStrategy.instance.routeMatcher = (pattern, path) -> {
		return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
	};
}
```


### Q：过低的 SpringBoot 版本引入 Sa-Token 后报错

在低于 2.2.0 时 (不包含2.2.0本身) 的 SpringBoot 项目中引入 Sa-Token 后，项目启动时会报错：

``` txt
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.dev33.satoken.spring.SaBeanInject': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [cn.dev33.satoken.spring.SaBeanInject]: Constructor threw exception; nested exception is java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator
```

这是由于缺少 jackson 相关依赖导致的，可以手动添加以下依赖来解决：

``` xml

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-core</artifactId>
	<version>2.17.3</version>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-annotations</artifactId>
	<version>2.17.3</version>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.17.3</version>
</dependency>
```



### Q：在 idea 导入源码，运行报错：java: 程序包cn.dev33.satoken.oauth2不存在。

在项目根目录进入 cmd，执行 `mvn package`，然后重新运行试试。

如果不行，先执行 `maven clean` ，然后删除 .idea 文件夹里除 `icon.png` 外的所有文件，然后执行 `mvn package`，然后重新运行试试。

如果还不行，删除整个项目，重新从 git 地址拉取一遍，再运行。



### Q：报错：非 web 上下文无法获取 HttpServletRequest。

报错原因解析：

Sa-Token 的部分 API 只能在 Web 上下文中才能调用，例如：`StpUtil.getLoginId()` 获取当前用户Id，这个方法第一步需要先从前端提交的参数里获取 token 值，
当你在 main 方法里调用这个 API 时，由于 main 方法本质上不是一个 Controller 请求，所以框架无法完成 *“从前端提交的参数里获取 token 值”* 这一步骤，框架就只能抛出异常。

按照此标准，Sa-Token 的 API 可粗浅的分为两大类：
- 必须在 Web 上下文中才能调用的 API，例如：`StpUtil.getLoginId()`、`StpUtil.getTokenValue()` 等等。
- 无需 Web 上下文也能调用的 API，例如：`StpUtil.getLoginType()`、`SaManager.getConfig()` 等等。

此处无法逐一列出到底哪些 API 属于 *“必须依赖 Web 上下文的 API”*，因为太多了，你只需要记住关键的一点：
**当一个 API 执行的代码需要先从前端请求中获取一些数据时，这个 API 就属于 *“必须依赖 Web 上下文的 API”*。**

如果你的代码报这个错，说明你在不是 Web 上下文中的地方，调用了 *“必须依赖 Web 上下文的 API”*，请排查：

1. 是否在 main 方法中调用了 *“必须依赖 Web 上下文的 API”*。
2. 是否在带有 `@Async` 注解的方法中调用了 *“必须依赖 Web 上下文的 API”*。
3. 是否在一些丢失 web 上下文的子线程中调用了 *“必须依赖 Web 上下文的 API”*，例如 `MyBatis-Plus` 的 `insertFill` 自动填充。
4. 是否在一些非 Http 协议的 RPC 框架中（例如 Dubbo）调用了 *“必须依赖 Web 上下文的 API”*。
5. 是否在 SpringBoot 启动初始化的方法中调用了 *“必须依赖 Web 上下文的 API”*，例如 `@PostConstruct` 修饰的方法。
6. 是否在定时任务中调用了 *“必须依赖 Web 上下文的 API”*。


### Q：报错：未能获取有效的上下文处理器。

报错原因解析：

在 sa-token-core 核心包中，Sa-Token 底层不能确认最终运行的 web 容器，所以抽象了 `SaTokenContext` 接口，对接不同容器时需要注入不同的实现，
通常这个注入工作都是框架自动完成的，你只需要按照文档开始部分集成相应的依赖即可。例如：

- 如果你使用的 `SpringBoot 2.x`，请引入 `sa-token-spring-boot-starter`。
- 如果你使用的 `SpringBoot 3.x`，请引入 `sa-token-spring-boot3-starter`。
- 如果你使用的 `SpringBoot 4.x`，请引入 `sa-token-spring-boot4-starter`。
- 如果你在基于 WebFlux 架构的网关中使用 Sa-Token，请引入 `sa-token-reactor-spring-boot-starter`（3.x 用 `sa-token-reactor-spring-boot3-starter`，4.x 用 `sa-token-reactor-spring-boot4-starter`）。
- 你要在 Solon 中使用 Sa-Token，就引入：`sa-token-solon-plugin`。
- 等等等等……

如果你的代码报 *“未能获取有效的上下文处理器”* 这个错，大概率是因为你没有正确引入所需的包，导致框架没有注入正确的 `SaTokenContext` 上下文实现，请排查：

1. 如果你的项目是微服务项目，请直接参考：[微服务-依赖引入说明](/micro/import-intro)，如果是单体项目，请往下看：
2. 请判断你的项目是 SpringMVC 环境还是 WebFlux 环境：
	- 如果是 SpringMVC 环境就引入 `sa-token-spring-boot-starter` 依赖，参考：[在SpringBoot环境集成](/start/example)
	- 如果是 WebFlux 环境就引入 `sa-token-reactor-spring-boot-starter` 依赖，参考：[在WebFlux环境集成](/start/webflux-example)
3. 如果你还无法分辨你是哪个环境，就看你的 pom.xml 依赖：
	- 如果引入了`spring-boot-starter-web`就是 SpringMVC 环境。
	- 如果引入了 `spring-boot-starter-webflux` 就是WebFlux环境。
	- 什么？你说你两个都引入了？那你的项目能启动成功吗？
4. 如果是 WebFlux 环境而且正确引入了依赖，依然报错，**请检查是否注册了 SaReactorFilter 全局过滤器，在 WebFlux 下这一步是必须的**，具体还是请参考上面的 [ 在WebFlux环境集成 ] 章节。
5. 需要仔细注意，如果你使用的是 `SpringBoot 3.x` 或 `SpringBoot 4.x`，请分别引入 `sa-token-spring-boot3-starter` 或 `sa-token-spring-boot4-starter`，不要错误引入 `sa-token-spring-boot-starter`，不然会导致框架报错。
6. 如果你的项目开启了全局懒加载(spring.main.lazy-initialization=true)后，能启动项目，但是访问接口报异常，请直接参考：[Q：开启了全局懒加载后，能启动项目，但是访问接口报未能获取有效的上下文处理器](/more/common-questions#Q：开启了全局懒加载后，能启动项目，但是访问接口报“未能获取有效的上下文处理器”)
7. 如果以上步骤排除无误后依然报错，请直接提 issue 或者加入QQ群求助。







## 二、常见疑问

### Q：登录方法需要我自己实现吗？
是的，不同于`shiro`等框架，`Sa-Token`不会在登录流程中强插一脚，开发者比对完用户的账号和密码之后，只需要调用`StpUtil.login(id)`通知一下框架即可

``` java
// 会话登录接口 
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
    // 第一步：比对前端提交的账号名称、密码
    if("zhang".equals(name) && "123456".equals(pwd)) {
		
        // 第二步：比对成功后，调用通知框架，xxx账号登录成功 
        StpUtil.login(10001);
        return SaResult.ok("登录成功");
    }
    return SaResult.error("登录失败");
}
```

### Q：框架抛出的权限不足异常，我想根据自定义提示信息，可以吗？
可以，在全局异常拦截器里捕获`NotPermissionException`，可以通过`getPermission()`获取没有通过认证的权限码，可以据此自定义返回信息

``` java
@RestControllerAdvice
public class GlobalExceptionHandler {
    // 全局 NotPermissionException 异常捕获 
    @ExceptionHandler(NotPermissionException.class)
    public SaResult handlerException(NotPermissionException e) {
        e.printStackTrace();
        return SaResult.error("缺少权限：" + e.getPermission());
    }
}
```

### Q：在 SaInterceptor 中，注解鉴权总是先于路由拦截鉴权执行，能调整一下顺序吗？
框架没有提供直接的 API，但你有以下两种方式可以做到这一点：
- 方式1：将 SaInterceptor 里的代码复制出来一份，按照你的需求改一下，然后使用你这个自定义的拦截器，不再使用官方的。
- 方式2：注册两次 SaInterceptor 拦截器，例如：

``` java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
	
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// 路由拦截鉴权
		registry.addInterceptor(new SaInterceptor(r -> {
			// 路由拦截鉴权的代码 ...
		}).isAnnotation(false)).addPathPatterns("/**");

		// 打开注解鉴权
		registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
	}
}
```
如上，第一个完成路由拦截鉴权功能，第二个完成注解鉴权功能。


### Q：我的项目权限模型不是RBAC模型，很复杂，可以集成吗？
无论什么模型，只要能把一个用户具有的所有权限塞到一个List里返回给框架，就能集成


### Q：StpInterface 接口的  方法，在什么时候执行？
每次鉴权时执行，例如你调用了 `StpUtil.checkgetPermission("xxx")` 方法，框架就会调用底层的 `StpInterface#getPermissionList` 方法来获取权限数据。

如果你的 `getPermissionList` 里有读数据库的代码，那么你每鉴一次权，系统将访问一次数据库。如果要减小性能消耗，可以把权限数据放在缓存中，参考：[把权限放在缓存里](/fun/jur-cache)。


### Q：当我配置不并发登录时，每次登陆都会产生一个新的 Token，旧 Token 依然被保存在 Redis 中，框架为什么不删除呢？
首先，不删除旧 Token 的原因是为了在旧 Token 再次访问系统时提示他：已被顶下线。

而且这个 Token 不会永远留在 `Redis` 里，在其 TTL 到期后就会自动清除，如果你想让它立即消失，可以：

- 方法一：配置文件把 `is-concurrent` 和 `is-share` 都打开，这样每次登陆都会复用以前的旧 Token，就不会有废弃 Token 产生了。 
- 方法二：每次登录前把先调用注销方法 `StpUtil.logout(10001)` ，把这个账号的旧登录都给清除了。
- 方法三：写一个定时任务查询 Redis 值进行删除。


### Q：我使用过滤器鉴权 or 全局拦截器鉴权，结果 Swagger 不能访问了，我应该排除哪些地址？
尝试加上排除 `"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ,"/doc.html/**","/error","/favicon.ico"`

不同版本可能会有所不同，其实在前端摁一下 `F12` 看看哪个 url 报错排除哪个就行了（另附：注解鉴权是不需要排除的，因为 `Swagger` 本身也没有使用 Sa-Token 的注解）


### Q：SaRouter.match 有多个路径需要排除怎么办？
可以点进去源码看一下，`SaRouter.match`方法有多个重载，可以放一个集合, 例如：
``` java
SaRouter.match("/**").notMatch("/login", "/reg").check(r -> StpUtil.checkLogin());
```


### Q：为什么StpUtil.login() 不能直接写入一个User对象？
`StpUtil.login()`只是为了给当前会话做个唯一标记，通常写入`UserId`即可，如果要存储User对象，可以使用`StpUtil.getSession()`获取Session对象进行存储。 


### Q：前后端分离模式下和普通模式有何不同？
主要是失去了`Cookie`无法自动化保存和提交`token秘钥`，可以参考章节：[前后端分离](/up/not-cookie)


### Q：前后端分离时，前端提交的 header 参数是叫 token 还是 satoken 还是 tokenName？
默认是satoken，如果想换一个名字，更改一下配置文件的`tokenName`即可。


### Q：一个账号拥有哪些权限，可以做成动态的吗？
权限本来就是动态的，框架预留的 `StpInterface` 接口，就是为了让你可以写任意代码来获取数据


### Q：路由拦截鉴权，可以做成动态的吗？
参考：[把路由拦截鉴权动态化](/fun/dynamic-router-check)


### Q：我不想让框架自动操作Cookie，怎么办？
在配置文件将`isReadCookie`值配置为`false`


### Q：怎么关掉每次启动时的字符画打印？
在配置文件将`isPrint`值配置为`false`


### Q：StpUtil.getSession()必须登录后才能调用吗？如果我想在用户未登录之前存储一些数据应该怎么办？
`StpUtil.getSession()`获取的是`Account-Session`，必须登录后才能使用，如果需要在未登录状态下也使用Session功能，请使用`Token-Session` <br>
步骤：先在配置文件里将`tokenSessionCheckLogin`配置为`false`，然后通过`StpUtil.getTokenSession()`获取Session 。或者直接调用 `StpUtil.getAnonTokenSession()` 获取匿名 Token-Session。


### Q：我只使用header来传输token，还需要打开Cookie模式吗？
不需要，如果只使用header来传输token，可以在配置文件关闭Cookie模式，例：`isReadCookie=false`


### Q：我想让用户修改密码后立即掉线重新登录，应该怎么做？
框架内置 [强制指定账号下线] 的APi，在执行修改密码逻辑之后调用此API即可: `StpUtil.logout()`


### Q：代码鉴权、注解鉴权、路由拦截鉴权，我该如何选择？
这个问题没有标准答案，这里只能给你提供一些建议，从鉴权粒度的角度来看：
1. 路由拦截鉴权：粒度最粗，只能粗略的拦截一个模块进行权限认证
2. 注解鉴权：粒度较细，可以详细到方法级，比较灵活
3. 代码鉴权：粒度最细，不光可以控制到方法级，甚至可以if语句决定是否鉴权

So：从鉴权粒度的角度来看，需要针对一个模块鉴权的时候，就用路由拦截鉴权，需要控制到方法级的时候，就用注解鉴权，需要根据条件判断是否鉴权的时候，就用代码鉴权 


### Q：Sa-Token的全局过滤器我应该怎么指定它的优先级呢？
为了保证相关组件能够及时初始化，框架默认给过滤器注册的优先级为-100，如果你想更改优先级，直接在注册过滤器的方法上加上 `@Order(xxx)` 即可覆盖框架的默认配置


### Q：timeout 过期了，获取到的 NotLoginException 场景值是-2，按照文档说的应该是-3吧。是我理解的不对还是操作有误？
你的理解是对的，但是框架现在只能做到返回-2，因为 token 过期后，就从 Redis 中消失了，框架没法分辨这个 token 是曾经有过然后过期的，还是从来就没有在Redis中存在过，
所以只能统一抛出-2，这个行为也和具体使用的 SaTokenDao 有关联，例如集成 sa-token-jwt 插件后，框架就能分辨出来是 token 过期了，抛出-3。


### Q：Sa-Token 是否提供类似 RefreshToken 的概念，与 AccessToken 相互配合刷新令牌鉴权。
关于长短 token，Sa-Token 没有提供直接的 API 支持，但是你可以利用 “临时 token 认证模块” 轻易的达到这一点：

1. 把 `sa-token.timeout` 的值配置小一点，然后把 `StpUtil.login(10001)` 生成的 token 作为短 token ，用来鉴权。
2. 用 “临时 token 认证模块” 生成长 token， `String refreshToken = SaTempUtil.createToken(10001, 2592000);`。
3. 把这两个 token 一起返回到前端。
4. 你再开个接口，可以让前端通过长 token，刷新短 token，参考代码：

``` java
@RequestMapping("/refreshToken")
public SaResult refreshToken(String refreshToken) {
	// 1、验证
	Object userId = SaTempUtil.parseToken(refreshToken);
	if(userId == null) {
		return SaResult.error("无效 refreshToken");
	}

	// 2、为其生成新的短 token
	String accessToken = StpUtil.createLoginSession(userId);

	// 3、返回
	return SaResult.data(accessToken);
}
```


### Q：前后端一体项目下，在拦截未登录进入登录页面时，如何登录完成后原路返回？
可以在拦截跳转登录页面时，把原 url 作为 back 参数挂载到登录页后方，登录完成后读取 back 参数并跳转
``` java
@RestControllerAdvice
public class GlobalException {
	// 未登录异常拦截 
	@ExceptionHandler(NotLoginException.class)
	public Object handlerException(NotLoginException e) {
		e.printStackTrace();
		return SaHolder.getResponse().redirect("/login?back=" + SaHolder.getRequest().getUrl());
	}
}
```



### Q：怎么改变请求返回的 http 状态码？
``` java
SaHolder.getResponse().setStatus(401)
```



### Q：Sa-Token 集成 Redis 如何集群？
以 `sa-token-redis-template` 为例：Sa-Token 底层使用的是 RedisTemplate 对象来操作数据的，也就是说，你只要给 RedisTemplate 配置上集群模式，Sa-Token 自动就是集群模式了。


### Q：多个项目共用同一个 redis，怎么防止冲突？

详见：[集成 Redis - 多个项目共用同一个 Redis，怎么防止冲突？](/up/integ-redis#4、多个项目共用同一个-Redis，怎么防止冲突？)


### Q：是否启用 Redis，可以通过配置文件来决定吗？

框架没有提供直接的配置开关。不过你可以自己注册 `SaTokenDao`，根据配置返回不同实现：

``` yaml
# 自定义开关（非框架内置配置项）
sa-token:
    my-use-redis: true
```

``` java
@Configuration
public class SaTokenDaoConfig {

	@Value("${sa-token.my-use-redis:false}")
	private boolean useRedis;

	@Bean
	@Primary
	public SaTokenDao saTokenDao() {
		// 开启 Redis：使用 RedisTemplate 持久化
		if (useRedis) {
			return new SaTokenDaoForRedisTemplate();
		}
		// 关闭 Redis：回落到内存实现
		return new SaTokenDaoDefaultImpl();
	}
}
```

也可以按 SpringBoot 多环境配置文件拆分（如 `application-dev.yml` / `application-prod.yml` 配不同的 `sa-token.my-use-redis`），达到「开发用内存、生产用 Redis」的效果。


### Q：如何防止 CSRF 攻击？
CSRF 攻击的核心在于利用浏览器自动提交 Cookie 的特性，代替用户发送自己不想发送的请求。

**方案一：关闭 Cookie模式。**

在配置文件里配置 `sa-token.is-read-cookie=false` 关闭 Cookie 读取模式，采用 localStorage 存储 token + header 头提交，即可避免 CSRF 攻击。

**方案二：增加 csrf-token 验证**

如果项目必须采用 Cookie 模式验证，可以在请求中增加 csrf-token 验证的环节：

1、在登录时，生成一个 `csrf_token` 返回到前端：
``` java
// 测试登录 
@RequestMapping("/login")
public SaResult login() {
	StpUtil.login(10001);
	String csrfToken = StpUtil.getSession().get("csrf_token", () -> SaFoxUtil.getRandomString(60));
	return SaResult.ok().set("csrf_token", csrfToken);
}
```

2、前端将 csrf_token 存储在 localStorage 中（注意一定要存储在 localStorage 而非 Cookie 中，存储在 Cookie 中还是可能会被浏览器自动提交）
``` java
localStorage.setItem('csrf_token', csrf_token);
```
每次请求将 csrf_token 塞到 Header 中。

3、在需要防止 CSRF 攻击的接口验证 csrf_token：
``` java
@RequestMapping("/test")
public SaResult test() {

	// 先验证 csrfToken 
	String csrfToken = SaHolder.getRequest().getHeader("csrf_token");
	if (csrfToken == null || ! csrfToken.equals(StpUtil.getSession().get("csrf_token")) ) {
		throw new SaTokenException("csrf_token 不匹配");
	}

	// 通过后再处理具体业务
	// ...

	return SaResult.ok();
}
```

也可以将验证代码写到全局拦截器中，为所有接口提供校验。



### Q：如何自定义框架读取 token 的方式？
**方式一：通过 StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx") 自定义 token 值**

如果你可以在框架读取 token 之前写一些代码，那么你可以通过如下代码自定义当前请求的 token 值：
``` java
@RequestMapping("/test")
public SaResult test() {
	System.out.println(StpUtil.getTokenValue()); // 此时读取到的是前端提交的: cebcc930-c0f5-4009-8eb0-1b6aee63b4aa
	StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx");
	System.out.println(StpUtil.getTokenValue()); // 此时读取到的是我们自定义的: abcdefgxxxxxxxx
	return SaResult.ok();
}
```

**方式二：重写 StpLogic 读取 token 的方法**

``` java
@Component
public class MyStpLogic extends StpLogic {
    public MyStpLogic() {
        super("login");
    }
	// 自定义 token 读取方式，例如此处改为读取请求头为 my-token 的值 
    @Override
    public String getTokenValue() {
        String token = SaHolder.getRequest().getHeader("my-token");
        return token;
    }
}
```




### Q：文档是否能下载？是否有离线版？
文档已完整开源，请访问 Sa-Token 官方仓库，根目录下的 sa-token-doc 文件夹就是文档。




### Q：还是有不明白到的地方?
请在`gitee` 、 `github` 提交 `issues`，或者加入qq群交流，[群链接](/more/join-group)

---

### Sa Token Doc New Docs More Content Cooperation (`sa-token-doc-new/docs/more/content-cooperation.md`)

---
title: "Sa-Token 内容合作群"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,内容合作群,其它"
description: "Sa-Token 内容合作群：面向写公众号、掘金、CSDN 或录 B 站视频的创作者。相关文章可转发至 30+ 微信粉丝群（约 500 人）与 10+ QQ 群，并挂到月 PV 20 万+ 的文档博客；官方文案开放转载无需申请、无 KPI 要求。加微信时请务必备注申请加入 Sa-Token 内容合作群。"
---

# Sa-Token 内容合作群 

**好内容值得被看见！**

为感谢 Sa-Token 的内容创作者们，我们特别创建了「Sa-Token 内容合作群」，帮助大家的内容触达更多 Sa-Token 的使用者 (加群方式在最下方)。

--- 


### 📖 1、一些碎碎念，想和写公众号/录视频的朋友们聊聊

前几天，我在公众号上搜索 “Sa-Token”，想看看有没有人写过相关的教程或者踩坑心得。

说实话，当时没抱太大期望。毕竟我们开发团队这几年来几乎将所有精力都放在了代码开发，而一直疏于内容运营建设。

但结果让我挺意外的 —— **我发现了不少公众号都在写 Sa-Token 的文章，而且其中不少都写得很用心**。

有从零开始的入门教程，有深入源码的解析文章，有对框架各个功能的介绍，还有一些结合真实业务场景的实战案例 …… 解决的都是实实在在的问题。

但这些文章的阅读量… 很多都只有几百，有些甚至只有几十。

我知道这很正常。**技术公众号起步非常不容易，粉丝少的时候，再好的内容也很难被看到**。我自己也经历过这个阶段，知道那种 [ 写了一整天，发出来没人看 ] 的感觉。

为了不让这些有价值的内容埋没，我连夜将这些文章整理到了 Sa-Token 官网：[博客](/blog/index.html)。

在整理这些博客的过程中，我突然有了一个想法。


### ✨ 2、一个可能 [三赢] 的想法

我在想，我为什么不拉一个群聊，把这些为 Sa-Token 写文章的博主们，给聚集起来呢？

只要有朋友写了 Sa-Token 相关文章，都可以转发到群里，我们团队会把这个文章转发到 Sa-Token 所有粉丝群里：

这可能是一个三赢的合作：

- **对于 Sa-Token 来说**：能获得更多的优质内容，帮助新用户更快上手，生态也能更丰富。
- **对写文章的朋友来说**：你的好文章能被更多人看到，公众号能涨涨粉，付出的时间更有价值。
- **对 Sa-Token 的用户来说**：能看到更多的技术干货，学到更多知识，找到各种场景的解决方案，不用重复踩坑。

听起来好像…还不错？

所以我打算建个群，名字就叫 「Sa-Token 内容合作群」。不是什么正式的组织，就是一群对技术内容感兴趣的朋友，凑在一起互相帮帮忙。


### 🤝 3、这个群主要用来做什么？

1、如果你写了 Sa-Token 相关的文章(或录制了视频课程)，可以分享到群里，我们团队会把文章：

- **转发到 Sa-Token 所有粉丝群里**：Sa-Token 目前拥有 30+ 微信粉丝群 (500人)，10+ QQ粉丝群 (1000人 or 2000人)。
- **挂载到 Sa-Token 在线文档博客栏目**：Sa-Token 目前在线文档访问量月PV 20万+。

相信这一定可以大大提高文章的曝光量。

2、我们团队偶尔也会为 Sa-Token 撰写技术文章，发到群里：

- 如果你觉得内容不错，想转载到自己的公众号，**直接转就行**。
- 不用专门申请授权，Sa-Token 官方订阅号所有内容均开放版权，任何人都可以自由转载。


### ❤️ 4、几个你可能关心的小问题

#### Q：我没写过 Sa-Token 的文章，可以加入吗？
可以。完全没问题。哪怕你之前从来没写过 Sa-Token 的文章，但只要你有公众号，想试试写相关的内容，都欢迎。

如果你没有公众号，但是在别的平台，比如掘金、CSDN有写过文章，也可以加入。如果你在 B站/抖音录制过视频，也可以加入。

总之：只要你有意向在任意平台创作 Sa-Token 相关内容，就可以加入。


#### Q：有 KPI 吗？是不是进了群就要为 Sa-Token 写文章？
没有。 想写就写，不想写就不写。哪怕加群后一篇都不写，也没关系。**这就是个「互助群」，不是「任务群」**。

#### Q：文章有什么要求吗？
就一点：认真写。 
可以是入门教程、源码解析、实战心得、bug排查、常见踩坑、对比测评…什么形式都行。不需要多长的篇幅，能把一个知识点讲清楚就好。唯一的要求是：不要是纯 AI 生成的粗制滥造文，不要写明显错误的技术观点。

#### Q：对粉丝量有要求吗？
没有要求。 我自己也是从小号做起来的，完全理解起步的难处。群里不分大号小号，只看内容用不用心。

#### Q：除了转发，还有别的吗？
有。 我偶尔会分享一些 Sa-Token 的更新动态、设计思路，或者我发现的其他好的技术文章。大家也可以互相**聊聊技术写作、视频制作的心得，分享好用的工具**。就是个普通的交流群，只不过主题稍微聚焦一点。

说到底，我希望这个群不只是一个内容分发渠道，更是一个 「Sa-Token内容共创伙伴」的聚集地。我们一起，让好的技术方案被更多人看见和使用。


### 👋 5、怎么加群？

你可以在 [加入讨论群](/more/join-group) 处，添加我们的微信账号， **请在添加时备注或者加好友成功后发送以下信息：[申请加入 Sa-Token 内容合作群]** 。

一定要备注以上信息，否则我们团队人员只会把你拉入到普通粉丝交流群。

<img class="s-w" src="/big-file/contact/show/wx-sthzq-show.png" style="max-width: 50%;" alt="Sa-Token 生态合作群" />

---

### Sa Token Doc New Docs More Demand Commit (`sa-token-doc-new/docs/more/demand-commit.md`)

---
title: "Sa-Token 需求提交"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,需求提交,其它"
description: "Sa-Token 需求提交：文档写不清、功能不完善或有改进 idea，都可通过腾讯问卷在线提交需求。比起浮夸赞美我们更重视批评建议；简单特性会尽快开发，复杂项排期，超出鉴权范畴则视人数决定。也可走 Gitee、GitHub、AtomGit issue 或交流群反馈。请大胆提交、大胆咨询，请不必害羞。"
---

# 需求提交

文档不清晰？功能不完善？脑袋里有好 idea？提！都可以提！

比起浮夸的赞美，Sa-Token 更希望收到您的批评与建议。

我们深知一个优秀的项目不能闭门造车，它需要海纳百川：[点我在线提交需求](https://wj.qq.com/s2/10852322/0d8b/)



我们将慎重对待每一位粉丝的珍贵意见 ❤️ ❤️ ❤️：

- 对框架新增特性功能且比较简单，会在第一时间进行开发。
- 对框架新增特性功能但比较复杂，会延后几个版本制定相应的计划后进行开发。
- 与框架设计理念不太相符，或超出权限认证范畴，将会视需求人数决定是否开发。


### 其它反馈途径
除了问卷提交，你还可以从以下渠道向我们提交反馈：
- Gitee：[issue 提交](https://gitee.com/dromara/sa-token/issues)  
- GitHub：[issue 提交](https://github.com/dromara/sa-token/issues)  
- AtomGit：[issue 提交](https://atomgit.com/dromara/sa-token/issues)
- 交流群：[加群链接](/more/join-group)

请大胆提交、大胆咨询，请在交流群中大胆艾特我们，请不要有任何害羞 🤭。就算我们不实现，你也不会损失什么，对吧！

---

### Sa Token Doc New Docs More Download Demos (`sa-token-doc-new/docs/more/download-demos.md`)

---
title: "Sa-Token 集成示例大全下载"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,集成示例大全下载"
description: "Sa-Token 集成示例大全下载：为几乎所有技术点均单独制作对应 Demo，压缩包共计 60+ 个工程，覆盖登录认证、权限认证、SSO、OAuth2、微服务鉴权、API Key、JWT 集成与跨系统参数签名。加入 QQ 群 1098917026 或微信群（备注 sa）后，可在群公告查看网盘领取地址。"
---

# Sa-Token 集成 Demos 示例大全下载

**我们为框架几乎所有技术点均单独制作了对应的集成示例，此压缩包共计 60+ Demo**：涵盖 Sa-Token 登录认证、权限认证、SSO 单点登录、OAuth2 统一认证、微服务鉴权、API Key 认证、JWT集成、跨系统调用参数签名校验 等鉴权认证的方方面面。

<img class="s-w-sh" src="/big-file/contact/show/sa-token-demos-pre.png" alt="Sa-Token 集成示例大全下载页预览" />


### 下载方式

加入 Sa-Token 交流群，即可在群公告查看下载地址：

加入QQ交流群：1098917026 [点击加入](https://qm.qq.com/q/26OFBGd2Zy) 

加入微信交流群：

<img class="s-w" src="/big-file/contact/i-wx-qr2.jpg" style="width: 180px;" alt="微信群" />

PS：扫码添加微信 (备注：sa)，邀您加入群聊。

<br>

<img class="s-w" src="/big-file/contact/show/sa-token-demos-notice.png" style="max-width: 70%;" alt="Sa-Token 示例大全网盘领取说明" />

---

### Sa Token Doc New Docs More Join Group (`sa-token-doc-new/docs/more/join-group.md`)

---
title: "Sa-Token 加入讨论群"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,加入讨论群,其它"
description: "加入 Sa-Token 讨论群：QQ 群 1098917026，微信扫码备注 sa 入群，可第一时间收到版本、Bug 与开源案例通知。群规禁止擦边图与代理广告，违规将移出（7 天后可再申请）。写过 5 篇以上博客或开发过插件者可进内部群 939849926；友联商务请备注来意加微信 kite2020_。"
---

# 加入讨论群

加入 Sa-Token 专属讨论群，与众多大佬一起努力 (huá shuǐ) 成长 (mō yú)。

---

### 1、加入QQ交流群


 


QQ交流群：1098917026 [点击加入](https://qm.qq.com/q/26OFBGd2Zy) 

### 2、加入微信交流群：



<img class="s-w" src="/big-file/contact/i-wx-qr2.jpg" style="width: 180px;" alt="微信群" />

PS：扫码添加微信 (备注：sa)，邀您加入群聊。

<br>


<img class="s-w" src="/big-file/contact/show/wx-group-show3.png" style="max-width: 50%;" alt="微信群" />


加入群聊的好处：
- 第一时间收到框架更新通知。
- 第一时间收到框架 bug 通知。
- 第一时间收到新增开源案例通知。
- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú) 🖐️🐟️。



### 3、群规（碎碎念）：
有同学质问我们，我加了群，为什么被踢了？我们很少踢人，一般只有严重违反群规了我们才会选择踢人。
- 不要在群里发擦边图、视频。轻度我们会警告，重度我们会选择移出群聊。
- 不要在群里聊代理、魔法上网等话题，如有需求请各自互相私聊，不要在群里聊。
- 不要发和程序员无关的广告，和程序员有关的比如开源项目、IT网站等我们一般不管。

请体谅我们，我们拉一个群也不容易，辛辛苦苦几个月才能拉满一个500人群，结果因为一些违规消息就导致封群，很难受的！

被踢了还能再次加群吗？可以，只要你想加，并保证不再发布违规消息，就可以在被踢7天之后再次申请加群。




### 4、内部群：

为感谢对 Sa-Token 生态做出贡献的同学，我们特创建了内部群：【Sa-Token 生态共享与合作】

加入群聊条件，以下满足其一即可：
- 写过5篇以上有关 Sa-Token 的原创博客。
- 为 Sa-Token 开发过插件。
- 有开源项目集成了 Sa-Token，并在 [Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token) 完成提交。
- 有为 Sa-Token 录制过教程视频，发表在公共平台（总时长>30分钟，且播放量>2000）。
- 其它一些您认为有对 Sa-Token 生态做出贡献的行为，可以直接联系我们，经内部投票评审通过即可加入（不要害羞，大胆联系我们哦 😊 ）

加入群聊的好处：

- 更及时的获知 Sa-Token 下一步更新计划。
- 在 Sa-Token 遇到的任何疑问都可以当面与作者沟通，可协助解决问题。
- 可提出未来版本更新需求，将具有更高的优先级进行评审与开发。

QQ群聊号码：939849926 

注：此为专属内部群聊，不满足上述条件的同学请勿过分申请打扰，谢谢合作。满足条件者可以在申请加入时备注上您的项目名称
（例如：xx开源项目作者集成了 sa-token，申请加入群聊），如果字数太多无法写完，也可在开源交流群里@管理员协助交流。


### 5、Sa-Token 内容合作群
专门为 Sa-Token 内容创作者们准备的交流群：[Sa-Token 内容合作群](/more/content-cooperation) 


### 6、商务合作

友联互换、商务合作请添加我的个人微信号：`kite2020_`，添加时请备注来意。

我的个人微信号好友数量已 9000+，马上满 1 万好友上限，所以请务必备注来意，否则不通过。如果仅是为了加入 Sa-Token 交流群请扫描上方二维码，会拉你进群。

---

### Sa Token Doc New Docs More Recommended (`sa-token-doc-new/docs/more/recommended.md`)

---
title: "Sa-Token 推荐阅读"
keywords: "Sa-Token,sa-token,satoken,推荐阅读,博客"
description: "Sa-Token 推荐阅读：完整列表已迁至 /blog/index.html，本页列表不再更新。仍保留阮一峰《理解 OAuth 2.0》与四种授权方式、RBAC 从 ACL 到 RBAC3 实战（2026），以及掘金上 2018 年前端 XSS、CSRF 防护系列，方便对照权限模型与浏览器安全要点。"
---

# 推荐阅读

> 推荐文章已迁移至  <a href="/blog/index.html">Sa-Token 博客</a>，本页不再更新。

---

- [RBAC 权限模型实战：从 ACL 踩坑到 RBAC3 落地（2026）](https://cloud.tencent.com/developer/article/2702469) （2026-07-02）

- [OAuth 2.0 的四种方式](https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html) （2019-04-09）

- [前端安全系列之二：如何防止 CSRF 攻击？](https://juejin.cn/post/6844903689702866952) （2018-10-12）

- [前端安全系列（一）：如何防止 XSS 攻击？](https://juejin.cn/post/6844903685122703367) （2018-09-28）

- [理解 OAuth 2.0](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) （2014-05-12）

---

### Sa Token Doc New Docs More Sa Token Donate (`sa-token-doc-new/docs/more/sa-token-donate.md`)

---
title: "赞助 Sa-Token"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,赞助 Sa-Token,其它"
description: "赞助 Sa-Token：框架采用 Apache-2.0，承诺源码与在线文档永久免费开放，但日常更新与社区运营靠爱发电难以长久。若框架帮到你，可用页面顶部微信赞赏码友情支持。开源永不收费，赞助纯属自愿。已捐赠列表展示赞助人、金额、留言与时间，支持按日期或金额排序并翻页查看，感谢每一位热心小伙伴的支持。"
---

# 赞助 Sa-Token

--- 

Sa-Token 采用 Apache-2.0 开源协议，<green>**承诺框架本身与在线文档永久免费开放**</green>，
但是框架的日常更新与社区运营需要付出大量的精力，靠爱发电难以长久，如果 Sa-Token 帮助到了您，您可以友情支持一下 Sa-Token。


### 友情赞助



<img src="/big-file/contact/wx-zsm.png" alt="微信赞赏码" style="width: 280px;" />


**已捐赠列表：**

<div class="zanzhu-box">
<div class="zanzhu-sort-box">
	<span class="zanzhu-sort-btn zz-sort-native" sort-value="1">日期排序</span>
	<span> | </span>
	<span class="zanzhu-sort-btn" sort-value="2">赞助额排序</span>
</div>
<table class="zanzhu-table" cellspacing="0" border="1" bordercolor="e9e9e9">
	<thead>
		<tr>
			<th>赞助人</th>
			<th>赞助金额</th>
			<th>留言</th>
			<th>时间</th>
		</tr>
	</thead>
	<tbody>
	</tbody>
</table>

<div class="zz-btn-box">
	<button onclick="prevPageRDT()"> < </button>
	<span class="zz-pageInfo">第 1/1 页</span>
	<button onclick="nextPageRDT()"> > </button>
</div>
</div>

感谢每一位小伙伴的热心支持 ❤️ ❤️ ❤️ ！

---

### Sa Token Doc New Docs More Tj Gzh Hz (`sa-token-doc-new/docs/more/tj-gzh-hz.md`)

---
title: "Sa-Token 公众号合作"
keywords: "Sa-Token,sa-token,satoken,公众号,互推合作"
description: "与 Sa-Token 做公众号互推：Java 运营者无需专门撰文，复制仓库 README 并留下官网或 GitHub 链接即可发表，阅读量需 1000+。作为回报，文档推荐公众号页按日期倒序放二维码，交流群 @全体推广一次；新推文每周最多三次发到群里，群友多为 Java 工程师。示例可参考仓库介绍推文。"
---

# 公众号合作

--- 

### 推荐须知：
Sa-Token作为一个新兴项目，迫切需要一定的途径进行项目推广<br>
如果您也是java公众号运营者，欢迎您将Sa-Token框架推荐给您的粉丝：

1. 您无需为Sa-Token专门撰写文案，只需要复制项目仓库的 Readme 内容即可，可参考：[链接](https://mp.weixin.qq.com/s/xMCedNj6Nti2BwGzS9A0mg)
2. 在文章底部或内容中留下项目官网或者GitHub仓库链接
3. 文章需至少 1000+ 的阅读量

作为回报，Sa-Token将：
1. 在框架官方文档 [[推荐公众号]](/more/tj-gzh) 处留下您的公众号二维码（按照推荐日期倒叙排列）
2. 在框架官方交流群里@全体成员推广您的公众号一次
3. 您的公众号所有新推文章都可以将链接发送到Sa-Token交流群中，增加阅读量（为避免频繁推送连接，请不要超过一周三次）

<br>
如果您还有除公众号以外的其它途径可以与Sa-Token相互推荐，欢迎加群交流……（群链接在首页）

---

### Sa Token Doc New Docs More Tj Gzh (`sa-token-doc-new/docs/more/tj-gzh.md`)

---
title: "Sa-Token 推荐公众号"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,推荐公众号,其它"
description: "Sa-Token 推荐公众号：收录 Java 技术、架构设计、源码分享与开源工具类账号，如 JavaGuide、Hollis、程序猿DD、macrozheng、TJ君、HelloGitHub 等，表格展示二维码与简介，便于关注 Spring 与权限认证内容。互推请转载仓库介绍并达阅读量门槛，见合作页。"
---

# 推荐公众号

--- 

### Java技术公众号：


<table class="gzh-table" style="text-align: center;">
    <tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzg2Mzg0NjEwOA==&mid=2247485024&idx=1&sn=11883396c1844fd2c4bd208c299bb075&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java大飞哥</b>
			<span>专注软件开发、技术架构设计、源码分享、JAVA技术。</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=Mzg4Nzg2MjQzNg==&mid=2247484545&idx=1&sn=c6101effb31b639fe2699b56bcc090c7&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>小简聊开发</b>
			<span>偶尔更新前后端技术文章，偶尔发布生活散文，更新随缘。浏览器搜索JanYork了解我更多。</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU2MTI4MjI0MQ==&mid=2247508620&idx=1&sn=500448fce310a6012aa58616c304dec2&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java笔记虾</b>
			<span>专注于Java技术栈，推送 Spring全家桶，Dubbo等相关技术知识</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzAxODcyNjEzNQ==&mid=2247541818&idx=2&sn=2b0e7190ed40fa07196de26be51bb432&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>程序猿DD</b>
			<span>一线技术工作者的学习、生活与见闻</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU5Mjc5NTIzMA==&mid=2247485007&idx=1&sn=d3bc0cc74a4efbc88b52cc5812c6573a&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>TJ君</b>
			<span>一个励志推荐10000款开源项目与免费工具的程序猿</span>
		</td>
    </tr>
    <tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzg2OTA0Njk0OA==&mid=2247511204&idx=2&sn=e1c689f459474fcc1b8ee2ca20a52d3a&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>JavaGuide</b>
			<span>专注Java后端学习和大厂面试的公众号！</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI3NzE0NjcwMg==&mid=2650166553&idx=3&sn=c8457e76c3fea17235fb7370cd2279d8&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Hollis</b>
			<span>《Java工程师成神之路》系列作者</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU1Nzg4NjgyMw==&mid=2247493156&idx=1&sn=571db231d274a90ae9a47ac9c4f0a034&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>macrozheng</b>
			<span>专注Java技术分享，作者Github开源项目mall（40K+Star）</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU2OTMyMTAxNA==&mid=2247496684&idx=2&sn=be1520743589ca43c129fde828af16ef&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>终码一生</b>
			<span>分享Java开发技术（JVM，多线程，高并发，性能调优）</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzU4ODI1MjA3NQ==&mid=2247502759&idx=2&sn=87ef078b86f5a0015d97807e76691b09&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>CodeSheep</b>
			<span>一只爱技术的程序羊，想把分享变成一种习惯！</span>
		</td>
    </tr>
    <tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzI4Njk5OTg1MA==&mid=2247489188&idx=1&sn=0098f458660d194817d4fc40bca55e4d&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java开发宝典</b>
			<span>分享Java基础、Java框架、数据库、微服务、中间件、分布式、架构等技术干货</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIwODkzOTc1MQ==&mid=2247489717&idx=1&sn=1c295f070123c84ee3791ac4d4898ae9&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>MarkerHub</b>
			<span>专注于梳理java知识，解析开源项目。 </span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzI3MDE0NzYwNA==&mid=2651444525&idx=3&sn=e266f94ada851a5ba6a61c1d71e9a62f&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>架构师必备</b>
			<span>分享干货文章，做一个有逼格的架构师社区！ </span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzk0MjE5MTk5Mw==&mid=2247485576&idx=1&sn=d955d60ab193d895fbd15c37bedebcb8&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Github导航站</b>
			<span>分享好玩有趣、新奇、实用的开源项目 </span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI1MTA3Mzk4Mg==&mid=2651026326&idx=1&sn=3244e18a9a799cd8e0ec375c6c730670&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java爱好者</b>
			<span>分享Java开发编程资源和Java技术文章 </span>
		</td>
    </tr>
    <tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIxODQxMjc0MA==&mid=2247511029&idx=1&sn=42fbb1b8180dc2c995429dabc6f97500&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>IT大咖说</b>
			<span>大咖干货，不再错过。 </span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIxNjA5MTM2MA==&mid=2652444329&idx=1&sn=2a040e87f49a977a116c90e1107a9fdc&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java编程</b>
			<span>专注Java技术分享，Java基础知识/数据结构/算法</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzA4MDMyODg4OQ==&mid=2649482871&idx=2&sn=b376585faaf814d9072af539efda68fe&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>大侠学JAVA</b>
			<span>道阻且长，行则将至，专注分享JAVA领域的干货</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=Mzg2MDIxNjAzNg==&mid=2247485810&idx=1&sn=afd46d5924afbc1030a87b5d56265fdf&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java大厂面试官</b>
			<span>一名一直奋斗在一线的程序员, 记录工作用遇到的问题和解决方案</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI2ODQwMTI0MA==&mid=2247486194&idx=2&sn=3fef462f77bdf0354a223e2e6d9d9136&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>程序员大神</b>
			<span>有一个在程序员圈混了10年的老程序员, 分享程序员相关的精选内容教程</span>
		</td>
    </tr>
    <tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzUyMDk4OTU5OA==&mid=2247499610&idx=6&sn=bc12403d060cba70be5739b808057d14&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>码农学习联盟</b>
			<span>码农学习联盟，程序员码农学习第一站！Java、Python、大数据、机器学习、人工智能</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MjM5NzA1MTcyMA==&mid=2651175957&idx=3&sn=93343b003a8a3c2c600b909499cb554a&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>程序猿</b>
			<span>传播编程经验，挖掘程序员优秀的学习资源。</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI3MDE0NzYwNA==&mid=2651443022&idx=2&sn=65db50021c95493d2df03dc58f6dae49&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>架构师必备</b>
			<span>分享干货文章，做一个有逼格的架构师社区！</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI4MjI1MTI0Mw==&mid=2247493681&idx=1&sn=4257550a48bf3dfc5c312b0750171c61&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>GitHuboy</b>
			<span>专注于分享 Python、Java、Web、AI、数据分析等多个领域的优质学习资源</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzA3NzA2MDMyNA==&mid=2650359032&idx=1&sn=79eea2f0cb16d5054bbb731885c310e5&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>开源最前线</b>
			<span>推荐热门开源软件，介绍新开源项目，报导开源资讯！</span>
		</td>
    </tr>
    <tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzUzNTY2NjAzMg==&mid=2247484321&idx=1&sn=52e7e5e0dc03437e94908b6a67985500&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Dromara开源组织</b>
			<span>Dromara开源组织官方公众号</span><br>
			<a href="https://gitee.com/dromara" target="_blank">Gitee</a>
			<a href="https://github.com/dromara" target="_blank">GitHub</a>
			<a href="https://dromara.org/zh/projects/" target="_blank">官网</a>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzAxNjk4ODE4OQ==&mid=2247503088&idx=3&sn=f7e82b05d8f155b1fa79601393c437dc&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>方志朋</b>
			<span>主要分享Java、Python等技术，用大厂程序员的视角来探讨技术进阶、面试指南、职业规划等。</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzkyNzExODM3OA==&mid=2247485166&idx=1&sn=fe7ff42336d050a7fbbe6b06fdd8c3ec&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java仓库</b>
			<span>专注Java全栈开发，分享实用技术干货。</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzU3MDc3OTI1NA==&mid=2247490668&idx=1&sn=cd9efecdf1ac34cc8cac04902a9f8319&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java技术江湖</b>
			<span>一位阿里 Java 工程师的技术小站, 分享技术干货和学习经验</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MjM5MTM0NjQ2MQ==&mid=2650152326&idx=2&sn=62643fd0987a56095663b12a2ec622c5&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>java那些事</b>
			<span>分享java中各种新技术的应用方法，做一个潮流的java技术人！</span>
		</td>
    </tr>
    <tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzIwNTk5NjEzNw==&mid=2247494012&idx=1&sn=378001dabae76b2df4de9a0dadf5842d&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java研发军团</b>
			<span>Java系列文章个人博客，MySQL、SSM、Redis、Spring</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzIwMTY0NDU3Nw==&mid=2651952104&idx=1&sn=315a840285b4f5b243d68e31cd0f2008&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java团长</b>
			<span>分享些技术干货，致力于Java全栈开发！</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzAwMjk5Mjk3Mw==&mid=2247496042&idx=1&sn=3c246a6feea74d24b92d49b564509fe8&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>武哥聊编程</b>
			<span>你若对得起时间，时间便对得起你~ 我是武哥！谢谢你的关注~每天进步一点点！</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzU2MTI4MjI0MQ==&mid=2247501218&idx=3&sn=599c40e5cd1acf597c8f392f1c5bd150&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java笔记虾</b>
			<span>专注于Java技术栈，推送 Spring全家桶，Dubbo，Zookeeper，Redis，Linux，多线程</span>
		</td>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzI1MDQwMDE3MQ==&mid=2247489606&idx=1&sn=d215fce776c5f56fc439cfdc024de504&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>Java项目学习</b>
			<span>关注我，我来带你从零开始做Java项目！</span>
		</td>
    </tr>
	<tr>
        <td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000005&size=102&__biz=MzIwNjg4MzY4NA==&mid=2247490744&idx=2&sn=2cea21ce873cae0cc4a1c5097265e678&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>程序员追风</b>
			<span>专注于分享Java各类学习笔记、面试题以及IT类资讯。</span>
		</td>
		<td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzU2OTYxNjk0Mg==&mid=2247489042&idx=1&sn=3526217770b7ec0661dc53b18eb98500&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>程序员编程</b>
			<span>每天下午13：30分发文，主要发布开源项目、面试题、最新技术资讯、干货学习资源～</span>
		</td>
		<td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzUzMDE4NjE4Mg==&mid=2247486739&idx=1&sn=339aed2b872c5c2f291e83b7ab107b4d&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>写代码的渣渣鹏</b>
			<span>关注我，学好Java。Spring Boot、 微服务、高并发、多线程、JVM、Spring Cloud</span>
		</td>
		<td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzA3MzE4ODY0Mg==&mid=2455988316&idx=1&sn=6d70a967f1ff757cdda320472c1deb87&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>GitHub精选</b>
			<span>专注于分享优质的开源项目、学习资源，Java、Python、Go、Web 前端、AI、数据分析</span>
		</td>
		<td>
			<img src="https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzA5MzYyNzQ0MQ==&mid=2247498873&idx=1&sn=8cb67e3057f81fc68ec5fd407076c7eb&send_time=" alt="Sa-Token 推荐公众号二维码" />
			<b>HelloGitHub</b>
			<span>分享 GitHub 上有趣、入门级的开源项目。</span>
		</td>
	</tr>
</table>

<br>

感谢以上公众号对 Sa-Token 项目的推荐

---

### Sa Token Doc New Docs More Update Log (`sa-token-doc-new/docs/more/update-log.md`)

---
title: "Sa-Token 更新日志"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,更新日志,其它"
description: "Sa-Token 更新日志：按发布时间倒序记录各版本新增特性、缺陷修复与不向下兼容变更。近期 v1.46.0（2026-8-18）含注销前钩子、JSON 反序列化白名单、SSE 异步上下文修复，以及 SSO/OAuth2 redirect 绕过 allow-url 的加固，升级前建议对照本页逐条评估。"
---

# 更新日志 

### v1.46.0 @2026-8-18
- core：
	- 新增：侦听器新增注销前钩子 `doBeforeLogout` / `doBeforeKickout` / `doBeforeReplaced`。 **[重要]**
	- 新增：`StpInterface.isDisabled` 补充 `loginType` 参数。  **[不向下兼容]**
	- 新增：新增 `allowLoginIdColon` 配置，默认禁止 loginId 包含冒号。  **[不向下兼容]**
	- 修复：Jackson DefaultTyping 多态反序列化 RCE，新增 `SaJsonStrategy` 全局类型白名单。  **[重要]**  **[漏洞修复]**
	- 新增：`getSaTokenConfig` 策略支持自定义配置来源，方便从数据库中读取配置。
	- 新增：新增 `SaRequest` / `SaResponse` / `SaStorage` 创建策略。  fix: [#841](https://github.com/dromara/Sa-Token/issues/841)
	- 新增：Session 新增 `getList` / `getSet` / `getMap` 类型安全集合读取。
	- 修改：随机字符串/数字改用 CSPRNG（`SecureRandom`）生成。
	- 修复：`isLastingCookie` 为 null 时 `getCookieTimeout` 不再 NPE。
	- 修复：`isUrl` 支持 IPv6 方括号地址并拒绝裸 IPv6。
	- 修复：JSON 反序列化容错优化。  fix: [#ICOTM8](https://gitee.com/dromara/sa-token/issues/ICOTM8)
- 插件：
	- 新增：新增 `sa-token-fory-json` JSON 序列化插件。  **[重要]**
	- 新增：新增 HTTP 请求扩展插件 `sa-token-rest-client` / `sa-token-rest-template`。  **[重要]**
	- 新增：新增 `sa-token-alone-redisson` 独立连接插件及 Spring Boot 3/4 示例。  **[重要]**  fix: [#710](https://github.com/dromara/Sa-Token/issues/710)、[#ICE2V0](https://gitee.com/dromara/sa-token/issues/ICE2V0)
	- 新增：新增 `sa-token-alone-redis-by-spring-boot4` 插件及示例，实现 Spring Boot 4 下权限缓存与业务缓存分离。  **[重要]**
	- 新增：`SaTokenDaoForRedisson` 支持指定 Codec（默认 `StringCodec`），update 方法改用 `setAndKeepTTL` 原子保留原过期时间。  **[重要]**  fix: [#791](https://github.com/dromara/Sa-Token/issues/791)
	- 修复：`sa-token-redis-template` 及 jdk-serializer 在 update key 时 TTL 偏移的问题。  **[重要]**  fix: [#I80P5O](https://gitee.com/dromara/sa-token/issues/I80P5O)、[#ICWJOQ](https://gitee.com/dromara/sa-token/issues/ICWJOQ)
	- 修复：redis-template `searchData` 由 KEYS 改为 SCAN。  **[重要]**  fix: [#702](https://github.com/dromara/Sa-Token/issues/702)
	- 新增：RedisTemplate Dao 支持重写 `wrapKey` 自定义键前缀。  fix: [#956](https://github.com/dromara/Sa-Token/issues/956)
	- 修复：`sa-token-redis-template-jdk-serializer` KEEPTTL 泛型编译错误。
	- 修复：Snack3 序列化不再写入类型信息，对齐 fastjson 安全策略。  **[漏洞修复]**
	- 修复：集成 JWT 时 `extraData` 禁止包含保留字段。  **[不向下兼容]**
	- 优化：升级 fastjson / fastjson2 至 2.0.64。
	- 优化：`sa-token-dependencies` 不再锁定 Reactor 版本，避免与 Spring Cloud Gateway 冲突。  fix: [#622](https://github.com/dromara/Sa-Token/issues/622)
- starter：
	- 修复：Context Filter 注册 REQUEST+ASYNC，修复 SSE/Flux 流式返回上下文未初始化。  **[重要]**  fix: [#IC4XFE](https://gitee.com/dromara/sa-token/issues/IC4XFE)、[#ICB9OJ](https://gitee.com/dromara/sa-token/issues/ICB9OJ)
	- 修复：Boot3+ WebFlux/Gateway `setStatus` NoSuchMethodError。  fix: [#IIAW1A](https://gitee.com/dromara/sa-token/issues/IIAW1A)、[#916](https://github.com/dromara/Sa-Token/issues/916)、[#IHRIBY](https://gitee.com/dromara/sa-token/issues/IHRIBY)
	- 新增：新增 SSO Client Spring Boot 4 示例。
	- 新增：新增 `sa-token-demo-first-run` 首次运行引导示例。
	- 修复：demo-ssm 适配 v1.42.0+ 上下文机制。
- Solon：
	- 优化：`sa-token-solon-plugin` 由 PathAnalyzer 适配改为 PathMatcher，优化 Config 示例。  merge: [pr 364](https://gitee.com/dromara/sa-token/pulls/364)  fix: [#IJTNJK](https://gitee.com/dromara/sa-token/issues/IJTNJK)
	- 优化：升级依赖 noear-redisx 至 1.8.5、noear-snack4 至 4.0.59。
- SSO：
	- 修复：OAuth2/SSO `redirect` 参数绕过 `allow-url` 校验的安全漏洞。  **[重要]**  **[漏洞修复]**
	- 新增：SSO 客户端新增指定账号单点注销 API。
	- 补全：补全最新版 SSO NoSdk 模式实现，新增 `SsoSignUtil`。
	- 修复：`buildServerAuthUrl` 识别 URL 编码后的 back 参数，back 为空时跳过追加，并增强重定向编码/解码健壮性。  fix: [#842](https://github.com/dromara/Sa-Token/issues/842)
	- 修复：`getClient` 统一通过 `getClients` 查找以支持子类覆盖。
	- 修复：NoSdk demo 签名校验补充时间窗与 nonce 防重放。  **[漏洞修复]**
	- 修复：订正 SSO-Client 接收消息推送地址的路由说明。
- OAuth2：
	- 修复：`redirect` 参数绕过 `allow-url` 校验的安全漏洞。  **[重要]**  **[漏洞修复]**
	- 修改：`expiresTime` 为 -1 时按永久有效存储 Token。
	- 重构：`doConfirm` 确认授权流程统一校验。
	- 重构：`SaOAuth2Dao` 改用带 Class 参数的 `getObject` 反序列化。  fix: [#IK5GYU](https://gitee.com/dromara/sa-token/issues/IK5GYU)
	- 修复：OAuth2 demo 拒绝授权时回传 state。
	- 新增：OAuth2 章节补充数据互通后的 token 过期策略说明。
- 文档：
	- 修改：全仓库域名 sa-token.cc 统一迁移至 sa-token.com。  **[重要]**
	- 新增：上线独立博客系统，收录原创文章。  **[重要]**
	- 新增：文档站点多语言切换（基于 translate.js）。  **[重要]**
	- 新增：README 多语言版本（英文、日文、韩文、俄文、繁体中文）与切换入口。  **[重要]**
	- 新增：新增 `SECURITY.md` 安全策略及联系邮箱。  **[重要]**  merge: [pr 933](https://github.com/dromara/Sa-Token/pull/933)  fix: [#932](https://github.com/dromara/Sa-Token/issues/932)、[#877](https://github.com/dromara/Sa-Token/issues/877)
	- 新增：文档站点暗色主题切换，支持 5 种 IDE 风格。
	- 新增：文档版本选择器改为动态加载（`all-version-common.js`）。  merge: [pr 928](https://github.com/dromara/Sa-Token/pull/928)  fix: [#900](https://github.com/dromara/Sa-Token/issues/900)
	- 新增：新增源码运行指南文档。
	- 新增：新增「Sa-Token 集成 Demos 示例大全下载」文档。
	- 新增：导航栏增加安全推荐与案例下拉菜单。
	- 优化：文档站点整体优化（侧边栏、首页、捐赠页、导航交互、API 手册等）。
	- 同步：同步公众号文章列表、博客列表、赞助者名单、企业登记案例。
	- 补充：补充 SSE/Flux 流式返回常见问题；补充按配置决定是否启用 Redis 的示例。  fix: [#I9I4ZG](https://gitee.com/dromara/sa-token/issues/I9I4ZG)
	- 修复：订正 API 手册、SSO/OAuth2/WebFlux/Dubbo 等文档错漏。
- 其它：
	- 新增：新增乐之者java B站视频教程链接。  **[重要]**
	- 优化：统一日志文案用词，「帐号」改为「账号」。
	- 优化：统一 token 续期相关注释措辞。
	- 修复：修正 ConcurrentHashMap 的错误 import 路径。  merge: [pr 359](https://gitee.com/dromara/sa-token/pulls/359)


### v1.45.0 @2026-3-8
- core：
	- 新增：新增重复登录处理策略，当同一账号不允许多客户端同时登录时支持选择踢人下线或拦截本次登录。  **[重要]** merge: [pr 349](https://gitee.com/dromara/sa-token/pulls/349)
	- 修复：修复 `StpUtil.getLoginIdByTokenNotThinkFreeze` 方法缺少 `static` 修饰符的问题。
	- 优化：优化路由匹配 pattern 缓存算法，消除魔法值。merge: [pr 907](https://github.com/dromara/Sa-Token/pulls/907)
	- 优化：移除冗余导包。
- 插件：
	- 新增：新增 `sa-token-jackson3` 插件，用于 Jackson 3 的 JSON 操作。  **[重要]**
	- 新增：新增 `sa-token-jackson3-test` 单元测试。
	- 新增：新增 `sa-token-snack4` 插件。  **[重要]** merge: [pr 356](https://gitee.com/dromara/sa-token/pulls/356)
	- 修复：修复 Dubbo 上下文清理问题。 merge: [pr 889](https://github.com/dromara/Sa-Token/pulls/889)
	- 新增：loveqq-framework 版本更新。merge: [pr 351](https://gitee.com/dromara/sa-token/pulls/351)
- starter：
	- 新增：新增 `sa-token-spring-boot4-starter` 集成包，支持 Spring Boot 4 环境集成。  **[重要]**
	- 新增：新增 `sa-token-reactor-spring-boot4-starter` 集成包，支持 Reactor + Spring Boot 4 环境集成。  **[重要]**
	- 新增：新增 `sa-token-demo-springboot4`、`sa-token-demo-webflux-springboot4` 示例。
	- 新增：新增 Spring Boot 4 整合 demo 示例。
- 重构：
	- 重构：重构 `sa-token-dependencies` 相关模块，优化依赖关系。  **[重要]**
	- 重构：重构 Spring Boot WebMVC/Reactor 相关集成包，优化依赖关系。 **[重要]**
	- 优化：优化整体模块依赖关系。
- Solon：
	- 优化：`sa-token-solon-plugin` 优化 Gateway 接口的处理，避免使用路由接口。merge: [pr 348](https://gitee.com/dromara/sa-token/pulls/348)
- SSO：
	- 新增：sso-server 前后端分离模式下 平台中心模式 demo 示例。
	- 修复：SSO 模块 msgType 参数说明、API 说明修正。
	- 新增：SSO 模块视频讲解链接：B站 王清江唷 SSO篇（29集）。  **[重要]**
	- 补全：SSO 模块内置消息处理器相关文档。
	- 新增：文档为 `sa-token-sso` 模块定义 STS 协议。  **[重要]**
- OAuth2：
	- 修复：修复 `sa-token-oauth2` 组件使用 `sa-token-fastjson2` 序列化导致的类型转换问题。merge: [pr 355](https://gitee.com/dromara/sa-token/pulls/355)
	- 优化：修改 `ClientIdSecretModel` 的读取构建逻辑。merge: [pr 346](https://gitee.com/dromara/sa-token/pulls/346)
- 文档：
	- 同步：同步公众号文章列表、博客列表、赞助者名单、企业登记案例。
	- 新增：新增 Sa-Token 内容合作者群。  **[重要]**
	- 新增：新增《Gitee 2025 年度开源项目 Web 应用开发 Top 2》证书展示。
	- 新增：新增赞赏码展示、文档首页 stars 对比图。
	- 新增：新增解决跨域专题文章。
	- 新增：增加微信群聊信息展示。
	- 优化：优化框架 Slogan。
	- 优化：优化 README、案例库展示。
	- 优化：文档主题切换增加水滴特效，调整主题色块顺序。
	- 优化：文档优化 [登录认证]、[权限认证]、[路由拦截鉴权] 篇。
	- 优化：补全全局策略说明、数据结构说明。
	- 新增：目录树增加专门栏目记录项目架构设计。
	- 优化：功能结构图增加点击事件跳转到对应功能文档。
	- 优化：子服务外网隔离章节增加示意图。
	- 优化：Same-Token 同源系统认证图示说明。
	- 修复：更换 GitCode logo 为 AtomGit。
	- 修复：更换 QQ 群链接、微信群聊展示图。
	- 修复：文档图片地址更换为本地文件。
	- 修复：错别字修复。
	- 修复：maven-pull.md 文档，解决父子项目依赖下载问题。
	- 新增：Maven 父子项目无法下载依赖的问题解决方案。merge: [pr 358](https://gitee.com/dromara/sa-token/pulls/358)
	- 修复：订正文档错别字。merge: [pr 354](https://gitee.com/dromara/sa-token/pulls/354)
	- 修复：文档内代码示例修正。merge: [pr 347](https://gitee.com/dromara/sa-token/pulls/347)
- AI: 
	- 新增：新增 organize-update-log SKILL，用于格式化整理版本更新日志信息。
	- 新增：新增 commit-message SKILL，用于整理 git commit 日志信息。
	- 新增：新增 upgrade-version SKILL，用于统一升级修改版本号。
	- 新增：新增 remove-redundancy-import SKILL，用于检查 Java 类中无效冗余导包并移除。
- 其它：
	- 新增：readme 增加快问快答区域。
	- 新增：增加忽略 .vscode 目录。
	- 优化：注释优化。
	- 重构：备忘录重构为专门的文件夹。
	- 重构：调整项目发布配置至 Maven Central Portal。merge: [pr 792](https://github.com/dromara/Sa-Token/pull/792)
	- 优化：部分构建配置升级到最新版。






### v1.44.0 @2025-6-7

- 修复：修复 sso-server 前后端分离示例无法正常登录的问题。
- 修复：修复 SSO 模式三全端注销失效的问题。
- 修复：修复 SSO `SaSsoClientModel` 部分场景下无法序列化的问题。
- 新增：OAuth2 模块新增支持从 `SaOAuth2DataLoader` 接口获取高级权限与低级权限的方法。merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/339) 
- 修复：修复 `sa-token-dubbo` 与 `sa-token-dubbo3` 每次调用都强制需要上下文的问题。
- 文档：新增 `sa-token-dubbo3` 的说明。
- 文档：更新赞助者名单。
- 文档：新增 `loveqq-framework` 框架集成包。 **[重要]**  merge: [pr 339](https://gitee.com/dromara/sa-token/pulls/340) 



### v1.43.0 @2025-5-17
- core: 
	- 新增：`SaLogoutParameter` 新增 `deviceId` 参数，用于控制指定设备 id 的注销。  **[重要]**
	- 新增：新增 `SaHttpTemplate` 请求处理器模块。
	- 新增：TOTP 增加 `issuer` 字段。  merge: [pr 329](https://gitee.com/dromara/sa-token/pulls/329) 
	- 修复：修复 `Http Digest` 认证时 url 上带有查询参数时认证无法通过的问题。merge: [pr 334](https://gitee.com/dromara/sa-token/pulls/334) 
	- 新增：@SaCheckOr 注解添加 `append` 字段，用于抓取未预先定义的注解类型进行批量注解鉴权。
	- 新增：侦听器 `doRenewTimeout` 方法添加 loginType 参数。
	- 新增：`SaInterceptor` 新增 `beforeAuth` 认证前置函数。
- SSO：
	- 新增：单点注销支持单设备注销。   **[重要]**  fix: [#IA6ZK0](https://gitee.com/dromara/sa-token/issues/IA6ZK0) 、[#747](https://github.com/dromara/Sa-Token/issues/747)
	- 新增：新增消息推送机制。  **[重要]**   fix: [#IBGXA7](https://gitee.com/dromara/sa-token/issues/IBGXA7) 
	- 新增：配置项 clients 用于单独配置每个 client 的授权信息。  **[重要]** 
	- 新增：配置项 `allowAnonClient` 决定是否启用匿名 client。
	- 新增：SSO 模块新增配置文件方式启用“不同 client 不同秘钥”能力。
	- 重构：sso-client 封装化获取 client 标识值。
	- 新增：新增 SSO Strategy 策略类。
	- 新增：sso-client 新增 `convertCenterIdToLoginId`、`convertLoginIdToCenterId` 策略函数，用于描述本地 LoginId 与认证中心 loginId 的转换规则。
	- 新增：sso-server 新增 `jumpToRedirectUrlNotice` 策略，用于授权重定向跳转之前的通知。
	- 优化：调整整体 SSO 示例代码。
	- 新增：新增 ReSdk 模式对接示例：`sa-token-demo-sso3-client-resdk`。  **[重要]** 
	- 新增：新增匿名应用模式对接示例：`sa-token-demo-sso3-client-anon`。  **[重要]** 
- OAuth2：
	- 新增：`SaClientModel` 新增 `isAutoConfirm` 配置项，用于决定是否允许应用可以自动确认授权。 **[重要]** 
	- 新增：多 `Access-Token` 并存、多 `Refresh-Token` 并存、多 `Client-Token` 并存能力。 **[重要]**  fix: [#IBHFD1](https://gitee.com/dromara/sa-token/issues/IBHFD1) 、 [#IBLL4Q](https://gitee.com/dromara/sa-token/issues/IBLL4Q) 、[#724](https://github.com/dromara/Sa-Token/issues/724) 
	- 新增：Scope 分割符支持加号。merge: [pr 333](https://gitee.com/dromara/sa-token/pulls/333) 
	- 修复：修复 oidc 协议下，当用户数据变动后，id_token 仍是旧信息的问题。
	- 优化：对 `OAuth2 Password` 认证模式需要重写处理器添加强提醒。
	- 优化：将认证流程回调从 `SaOAuth2ServerConfig` 转移到 `SaOAuth2Strategy`。
	- 新增：新增 `SaOAuth2Strategy.instance.userAuthorizeClientCheck` 策略，用于检查指定用户是否可以授权指定应用。fix: [#553](https://github.com/dromara/Sa-Token/issues/553) 
	- 优化：优化调整 `sa-token-oauth2` 模块代码结构及注释。
	- 新增：`currentAccessToken()`、`currentClientToken()`，简化读取 `access_token`、`client_token` 步骤
- 插件：
	- 新增：新增 `sa-token-forest` 插件，用于在 Http 请求处理器模块整合 Forest。
	- 新增：新增 `sa-token-okhttps` 插件，用于在 Http 请求处理器模块整合 OkHttps。
	- 拆分：API Key 模块拆分独立插件包：`sa-token-apikey`。
	- 拆分：API Sign 模块拆分独立插件包：`sa-token-sign`。
	- 修复：修复 `sa-token-dubbo` 插件部分场景上下文控制出错的问题。
	- 修复：修复 `sa-token-sanck3` `SaSessionForSnack3Customized:getModel` 接收 map 值时会出错的问题。 merge: [pr 330](https://gitee.com/dromara/sa-token/pulls/330) 
	- 修复：修复使用 `sa-token-redis-template-jdk-serializer` 时反序列化错误。merge: [pr 331](https://gitee.com/dromara/sa-token/pulls/331) 
	- 修复：`sa-token-snack3` 优化 `objectToJson` 序列化处理（增加类名，但不增加根类名）。
	- 重构：重构 `sa-token-redis-template`、`sa-token-redis-template-jdk-serializer` 插件中 update 方法 ttl 获取方式改为毫秒，以减少 update 时的 ttl 计算误差。  **[重要]** 
- 示例：
	- 新增：新增 SSE 鉴权示例。
- 文档：
	- 新增：新增文档离线版下载。
	- 新增：新增框架功能列表插图。
	- 新增：新增示例：如何在响应式环境下的 Filter 里调用 Sa-Token 同步 API。
	- 新增：新增 QA：在 idea 导入源码，运行报错：java: 程序包cn.dev33.satoken.oauth2不存在。
	- 新增：新增 QA：新增QA：报错：SaTokenContext 上下文尚未初始化。
	- 新增：新增 QA：在 idea 导入源码，运行报错：java: 程序包cn.dev33.satoken.oauth2不存在。
	- 新增：重写路由匹配算法修正为最新写法。
	- 新增：修复 OAuth2 UnionId 章节相关不正确描述。
	- 优化：完善 QA：访问了一个不存在的路由，报错：SaTokenContext 上下文尚未初始化。   fix: [#771](https://github.com/dromara/Sa-Token/issues/771)
	- 优化：补充 sso 模块遗漏的配置字段介绍。
	- 优化：OAuth2-Server 示例添加真正表单。
	- 新增：文档新增重写 `PasswordGrantTypeHandler` 处理器示例。
	- 新增：sso 章节和 oauth2 章节文档增加可重写策略说明。
- 其它：
	- 新增：readme 新增框架功能介绍图。
	- 新增：SSO 模块新增思维导图说明。
	- 新增：readme 新增 Forest 的友情链接。
									
								


### v1.42.0 @2025-4-11
更新导读：[视频](https://www.bilibili.com/video/BV1h85izzEe8/)、[文字版](https://juejin.cn/post/7491971657201451062)

- core: 
	- 新增: 新增 `API Key` 模块。   **[重要]**
	- 新增: 新增 `TOTP` 实现。   **[重要]**
	- 重构：重构 `TempToken` 模块，新增 value 反查 token 机制。   **[重要]**
	- 升级: 重构升级 `SaTokenContext` 上下文读写策略。   **[重要]**
	- 新增: 新增 Mock 上下文模块。   **[重要]**
	- 删除: 删除二级上下文模块。
	- 新增: 新增异步场景使用 demo。   **[重要]**
	- 新增: 新增 `Base32` 编码工具类。
	- 新增：新增 `CORS` 跨域策略处理函数，提供不同架构下统一的跨域处理方案。
	- 新增：`renewTimeout` 续期方法增加 token 终端信息有效性校验。
	- 新增: 全局配置项 `cookieAutoFillPrefix`：cookie 模式是否自动填充 token 前缀。
	- 新增: 全局配置项 `rightNowCreateTokenSession`：在登录时，是否立即创建对应的 `Token-Session`。
	- 优化：优化 `Token-Session` 获取算法，减少缓存读取次数。
	- 新增：`SaLoginParameter` 支持配置 `SaCookieConfig`，以配置 Cookie 相关参数。
	- 修改：防火墙校验过滤器的注册顺序 修改为 -102。
	- 新增：防火墙 `hook` 注册新增 `registerHookToFirst`、`registerHookToSecond` 方法，以便更灵活的控制 hook 顺序。
- 插件：
	- 新增: `sa-token-quick-login` 插件支持 `Http Basic` 方式通过认证。
- 单元测试：
	- 补全：补全 `Temp Token` 模块单元测试。
- 文档：
	- 补全：补全赞助者名单。
	- 修复：修复 `Thymeleaf` 集成文档不正确的依赖示例说明。
	- 修复：修复 `unionid` 章节错误描述。
	- 优化：采用更细致的描述优化SSO模式三单点注销步骤。
	- 新增：登录认证文档添加 Cookie 查看步骤演示图。
	- 新增：多账号模式新增注意点：运行时不可更改 `LoginType`。
	- 新增: 多账号模式QA：在一个接口里获取是哪个体系的账号正在登录。
	- 新增：新增QA：解决低版本 `SpringBoot (<2.2.0)` 引入 Sa-Token 报错的问题。
	- 新增：新增QA：前后端一体项目下，在拦截未登录进入登录页面时，如何登录完成后原路返回？
	- 新增：新增QA：Sa-Token 集成 Redis 如何集群？
	- 新增：新增QA：如何自定义框架读取 token 的方式？
	- 新增：新增QA：修改 `hosts` 文件无效可能原因排查。
	- 新增：新增QA：如何防止 CSRF 攻击。
	- 新增: “异步 & Mock 上下文” 章节。
	- 升级：升级“自定义 SaTokenContext 指南”章节文档。



### v1.41.0 @2025-3-21
更新导读：[视频](https://www.bilibili.com/video/BV1aNo4YCEM1/)、[文字版](https://juejin.cn/post/7484191942358499368)

- core: 
	- 修复：修复 `StpUtil.setTokenValue("xxx")`、`loginParameter.getIsWriteHeader()` 空指针的问题。 fix: [#IBKSM0](https://gitee.com/dromara/sa-token/issues/IBKSM0)
	- 修复：将 `SaDisableWrapperInfo.createNotDisabled()` 默认返回值封禁等级改为 -2，以保证向之前版本兼容。
	- 新增：新增基于 SPI 的插件体系。   **[重要]** 
	- 重构：JSON 转换器模块。   **[重要]** 
	- 新增：新增 serializer 序列化模块，控制 `Object` 与 `String` 的序列化方式。   **[重要]** 
	- 重构：重构防火墙模块，增加 hooks 机制。   **[重要]** 
	- 新增：防火墙新增：请求 path 禁止字符校验、Host 检测、请求 Method 检测、请求头检测、请求参数检测。重构目录遍历符检测算法。
	- 重构：重构 `SaTokenDao` 模块，将序列化与存储操作分离。   **[重要]**
	- 重构：重构 `SaTokenDao` 默认实现类，优化底层设计。
	- 新增：`isLastingCookie` 配置项支持在全局配置中定义了。
	- 重构：`SaLoginModel` -> `SaLoginParameter`。    **[不向下兼容]** 
	- 重构：`TokenSign` -> `SaTerminalInfo`。    **[不向下兼容]** 
	- 新增：`SaTerminalInfo` 新增 `extraData` 自定义扩展数据设置。
	- 新增：`SaLoginParameter` 支持配置 `isConcurrent`、`isShare`、`maxLoginCount`、`maxTryTimes`。
	- 新增：新增 `SaLogoutParameter`，用于控制注销会话时的各种细节。  **[重要]**
	- 新增：新增 `StpLogic#isTrustDeviceId` 方法，用于判断指定设备是否为可信任设备。
	- 新增：新增 `StpUtil.getTerminalListByLoginId(loginId)`、`StpUtil.forEachTerminalList(loginId)` 方法，以更方便的实现单账号会话管理。
	- 升级：API 参数签名配置支持自定义摘要算法。
	- 新增：新增 `@SaCheckSign` 注解鉴权，用于 API 签名参数校验。
	- 新增：API 参数签名模块新增多应用模式。 fix: [#IAK2BI](https://gitee.com/dromara/sa-token/issues/IAK2BI), [#I9SPI1](https://gitee.com/dromara/sa-token/issues/I9SPI1), [#IAC0P9](https://gitee.com/dromara/sa-token/issues/IAC0P9)   **[重要]**
	- 重构：全局配置 `is-share` 默认值改为 false。    **[不向下兼容]** 
	- 重构：踢人下线、顶人下线默认将删除对应的 token-session 对象。
	- 优化：优化注销会话相关 API。
	- 重构：登录默认设备类型值改为 DEF。   **[不向下兼容]** 
	- 重构：`BCrypt` 标注为 `@Deprecated`。
	- 新增：`sa-token-quick-login` 支持 `SpringBoot3` 项目。 fix: [#IAFQNE](https://gitee.com/dromara/sa-token/issues/IAFQNE)、[#673](https://github.com/dromara/Sa-Token/issues/673)
	- 新增：`SaTokenConfig` 新增 `replacedRange`、`overflowLogoutMode`、`logoutRange`、`isLogoutKeepFreezeOps`、``isLogoutKeepTokenSession`` 配置项。
- OAuth2：
	- 重构：重构 sa-token-oauth2 插件，使注解鉴权处理器的注册过程改为 SPI 插件加载。
- 插件：
	- 新增：`sa-token-serializer-features` 插件，用于实现各种形式的自定义字符集序列化方案。
	- 新增：`sa-token-fastjson` 插件。
	- 新增：`sa-token-fastjson2` 插件。
	- 新增：`sa-token-snack3` 插件。
	- 新增：`sa-token-caffeine` 插件。
- 单元测试：
	- 新增：`sa-token-json-test` json 模块单元测试。
	- 新增：`sa-token-serializer-test` 序列化模块单元测试。
- 文档：
	- 新增：QA “多个项目共用同一个 redis，怎么防止冲突？” 
	- 优化：补全 OAuth2 模块遗漏的相关配置项。
	- 优化：优化 OAuth2 简述章节描述文档。
	- 优化：完善 “SSO 用户数据同步 / 迁移” 章节文档。
	- 修正：补全项目目录结构介绍文档。
	- 新增：文档新增 “登录参数 & 注销参数” 章节。
	- 优化：优化“技术求助”按钮的提示文字。
	- 新增：新增 `preview-doc.bat` 文件，一键启动文档预览。
	- 完善：完善 Redis 集成文档。
	- 新增：新增单账号会话查询的操作示例。
	- 新增：新增顶人下线 API 介绍。
	- 新增：新增 自定义序列化插件 章节。
- 其它：
	- 新增：新增 `sa-token-demo/pom.xml` 以便在 idea 中一键导入所有 demo 项目。
	- 删除：删除不必要的 `.gitignore` 文件
	- 重构：重构 `sa-token-solon-plugin` 插件。
	- 新增：新增设备锁登录示例。


### v1.40.0 @2025-2-1
更新导读：[视频](https://www.bilibili.com/video/BV1uNATeeEvg/)、[文字版](https://juejin.cn/post/7467969744307306505)

- core: 
	- 新增：新增 `Cookie` 自定义属性支持。  fix: [#693](https://github.com/dromara/Sa-Token/issues/693)   **[重要]** 
	- 新增：`SaFirewallStrategy` 防火墙策略：请求 path 黑名单校验、非法字符校验、白名单放行。  **[重要]** 
	- 修复：新增对分号字符的 path 路径校验。   参考：[Sa-Token对url过滤不全存在的风险点](https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA)   **[漏洞修复]** 
	- 修复: 修复部分场景下登录后已存在的 `token-session` 没有被续期的问题。  fix: [#IA8U1O](https://gitee.com/dromara/sa-token/issues/IA8U1O)
	- 优化：优化 `active-timeout` 的检查与续期操作，同一请求内只会检查与续期一次。
	- 修复：`SaFoxUtil.joinSharpParam` 方法中不正确的注释。
	- 新增：封禁模块新增支持实时从数据库查询数据。
- SSO：
	- 优化：SSO 示例代码的跨域处理由原生方式改为 Sa-Token 过滤器模式。
	- 新增：文档新增 “SSO整合 - NoSdk 模式与非 java 项目” 章节。
	- 新增：“不同 SSO Client 配置不同秘钥” 章节增加部分异常的处理方案提示，fix: [#IAFZXL](https://gitee.com/dromara/sa-token/issues/IAFZXL)
	- 删除：sso demo 示例中部分不必要的代码内容。
- OAuth2：
	- 新增：OAuth2 Client 前端测试页。   **[重要]**
	- 新增：`UnionId` 联合id 实现。   **[重要]** 
	- 新增：`oauth2-server` 端前后台分离示例与文档。 fix: [#I9DQGA](https://gitee.com/dromara/sa-token/issues/I9DQGA)、[#I9W2RU](https://gitee.com/dromara/sa-token/issues/I9W2RU)    **[重要]**
	- 新增：`OIDC` 模式 `nonce` 随机数响应校验。 merge: [pr311](https://gitee.com/dromara/sa-token/pulls/311)
	- 修复：错误方法名 `deleteGrantScope(String state)` -> `deleteState(String state)`。
	- 修复：全局配置项 `sa-token.oauth2-server.oidc.iss` 无效的问题。
	- 新增：回收 Refresh-Token 方法: `revokeRefreshToken`、`revokeRefreshTokenByIndex`。
	- 新增：为 `CodeModel`、`AccessTokenModel`、`RefreshTokenModel`、`ClientTokenModel` 添加 `createTime` 字段，以记录该数据的创建时间。
	- 新增：为 Access-Token、Client-Token 添加 `grantType` 字段，以记录该数据的授权类型。
	- 新增：`SaOAuth2Util.getCode` 等方法，以更方便的获取、校验授权码。
- 插件：
	- 新增：新增 `sa-token-freemarker` 插件，整合 `Freemarker` 视图引擎。 fix: [#651](https://github.com/dromara/sa-token/issues/651)   **[重要]**
	- 新增：新增 `sa-token-spring-el` 插件，用于支持 SpEL 表达式注解鉴权。 fix: [#IB3GBB](https://gitee.com/dromara/sa-token/issues/IB3GBB)、fix: [#IAIXSL](https://gitee.com/dromara/sa-token/issues/IAIXSL)、fix: [#I9P24F](https://gitee.com/dromara/sa-token/issues/I9P24F)   **[重要]**
- 文档：
	- 新增：新增 `MongoDB` 集成示例。 感谢 `@lilihao` 提供的示例。 merge: [pr322](https://gitee.com/dromara/sa-token/pulls/322)、[pr667](https://github.com/dromara/Sa-Token/pull/667)   **[重要]**
	- 新增：“fox说技术” 视频教程链接。
	- 新增：“API接口参数签名”章节 视频讲解链接（B站抓蛙师）。
	- 优化：文档首页首屏增加需求提交按钮。
	- 其它：补全赞助者名单、`Dromara` 项目链接等信息。
	- 新增：`SpringBoot3.x` 版本配置 Redis 注意事项。fix: [#688](https://github.com/dromara/Sa-Token/issues/688)
	- 新增：`gitcode` g-star badge 展示。
	- 修复：`OAuth2` 滞后的配置信息示例。
	- 新增：新增视频账号链接。
	- 新增：新增团队成员展示。




### v1.39.0 @2024-8-28
- 核心：
	- 升级：重构注解鉴权底层，支持自定义鉴权注解了。  **[重要]**
	- 修复：修复前端提交同名 `Cookie` 时的框架错读现象。
	- 更名：`NotBasicAuthException` -> `NotHttpBasicAuthException`。
- 插件：
	- 修复：修复 `sa-token-quick-login` 插件无法正常拦截的问题。
- SSO：
	- 优化：优化 sso-server 前后端分离 demo 代码。
	- 优化：优化 sso-server 前后端分离时的跳转流程。
- OAuth2：
	- 重构：`sa-token-oauth2` 模块整体重构。   **[重要]**  **[不向下兼容]**
	- 新增：新增支持自定义 `scope` 处理器。	 **[重要]**
	- 新增：新增支持自定义 `grant_type`。	 **[重要]**
	- 新增：新增 `scope` 划分等级。		 **[重要]**
	- 新增：新增 `oidc` 协议支持。		 **[重要]**
	- 新增：新增支持默认 `openid` 生成算法。	 **[重要]**
	- 新增：新增 `OAuth2` 注解鉴权支持。		 **[重要]**
	- 修复：`redirect_url` 参数校验增加规则：不允许出现@字符、*通配符只能出现在最后一位。关联 [issue](https://github.com/dromara/Sa-Token/issues/529) **[重要]**
	- 优化：优化代码注释、异常提示信息。
	- 升级：兼容 `Http Basic` 提交 `client` 信息的场景。感谢 github `@CuiGeekYoung` 提交的pr。
	- 升级：兼容 `Bearer Token` 方式提交 `access_token` 和 `client_token`。
	- 升级：适配拆分式路由。
	- 新增：将 `scope` 字段改为 List 类型。
	- 重构：抽离 `SaOAuth2Strategy` 全局策略接口，定义一些创建 token 的算法策略。
	- 新增：新增 `addAllowUrls` `addContractScopes` 方法，简化 `SaClientModel` 构建代码。
	- 重构：抽离 `SaOAuth2Dao` 接口，负责数据持久。
	- 重构：抽离 `SaOAuth2DataLoader` 数据加载器接口。
	- 重构：抽离 `SaOAuth2DataGenerate` 数据构造器接口。
	- 重构：抽离 `SaOAuth2DataConverter` 数据转换器接口。
	- 重构：抽离 `SaOAuth2DataResolver` 数据解析器接口。
	- 重构：重构 `SaOAuth2Handle` -> `SaOAuth2ServerProcessor` 更方便的逻辑重写。
	- 重构：重构 `PastToken` -> `LowerClientToken`。
	- 新增：新增 `state` 值校验，同一 `state` 参数不可重复使用。
	- 优化：完善 `SaOAuth2Util` 相关方法，更方便的二次开发。
	- 新增：新增部分异常类，细分异常 `ClassType`。
	- 优化：优化 `sa-token-oauth2` 异常细分状态码。
- 文档：
	- 新增：新增数据结构说明。
	- 新增：新增不同 `client` 不同登录页说明。
	- 优化：优化文档 [将权限数据放在缓存里] 示例。
	- 新增：新增 从 Shiro、SpringSecurity、JWT 迁移 示例。  **[重要]**



### v1.38.0 @2024-5-12
- sa-token-core：
	- 修复：修复 `StpUtil.getSessionByLoginId(xx)` 参数为 null 时创建无效 `SaSession` 的 bug。
	- 优化：在 `SpringBoot 3.x` 版本下错误的引入依赖时将得到启动失败的提示。 （感谢`Uncarbon`提交的pr）
	- 优化：进一步优化权限校验算法，hasXxx API 只会返回 true 或 false，不再抛出异常。
	- 重构：`InvalidContextException` 更名为 `SaTokenContextException`。 **[已做向下兼容处理]**
	- 重构：彻底删除 `NotPermissionException` 异常中的 `getCode()` 方法。 **[过期API清理]**
	- 重构：重构 `SaTokenException` 类方法 `throwBy-`>`notTrue`、`throwByNull-`>`notEmpty`。**[已做向下兼容处理]**
	- 重构：`StpUtil.getSessionBySessionId` 提供的 `SessionId` 为空时将直接抛出异常，而不是再返回null。**[不向下兼容]**
	- 新增：新增 `Http Digest` 认证模块简单实现。	**[重要]**
	- 重构：更换 `HttpBasic` 认证模块包名。	**[已做向下兼容处理]**
	- 新增：新增 `StpUtil.getLoginDeviceByToken(xxx)` 方法，用于获取任意 token 的登录设备类型。
	- 新增：新增 `StpUtil.getTokenLastActiveTime()` 方法，获取当前 token 最后活跃时间。
	- 修复：修复“当登录时指定 timeout 小于全局 timeout 时，`Account-Session` 有效期为全局 timeout”的问题。
	- 优化：首次获取 `Token-Session` 时，其有效期将保持和 token 有效期相同，而不是再是全局 timeout 值。
	- 移除：移除 `SaSignConfig` 的 `isCheckNonce` 配置项。 **[不向下兼容]**
	- 新增：`SaSignTemplate#checkRequest` 增加“可指定参与签名参数”的功能。
	- 重构：将部分加密算法设置为过期。
	- 重构：优化 token 读取策略，空字符串将视为没有提交token。
	- 修复：`sa-token-bom` 补全缺失依赖。
	- 优化：二级认证校验之前必须先通过登录认证校验。
	- 修复：修复 `StpUtil.getLoginId(T defaultValue)` 传入 null 时无法正确返回值的bug。
- sa-token-sso：
	- 优化：SSO 模式三，API 调用签名校验时，限定参与签名的参数列表，更安全。
	- 新增：新增 `autoRenewTimeout` 配置项：是否在每次下发 ticket 时，自动续期 token 的有效期（根据全局 timeout 值）
	- 新增：`SaSsoConfig` 新增配置 `isCheckSign`（是否校验参数签名），方便本地开发时的调试。
	- 新增：`SaSsoConfig` 新增配置 `currSsoLogin`，用于强制指定当前系统的 sso 登录地址。
	- 重构：整体重构 `sa-token-sso` 模块，将 `server` 端和 `client` 端代码拆分。 **[重要]** **[不向下兼容]**
	- 新增：`SaSsoConfig` 配置项 `ssoLogoutCall` 重命名为 `currSsoLogoutCall`。**[已做向下兼容处理]**
	- 重构：模式三在校验 Ticket 时，也将强制校验签名才能调通请求。**[不向下兼容]**
	- 新增：新增 `maxRegClient` 配置项，用于控制模式三下 client 注册数量。
	- 新增：新增不同 SSO Client 配置不同 `secret-key` 的方案。 **[重要]**
	- 重构：匿名 client 将不再能解析出所有应用的 ticket。**[不向下兼容]**
	- 新增：新增 `homeRoute` 配置项：在 ``/sso/auth`` 登录后不指定 redirect 参数的情况下默认跳转的路由。
	- 优化：优化登录有效期策略，SSO Client 端登录时将延续 SSO Server 端的会话剩余有效期。
	- 新增：新增 `checkTicketAppendData` 策略函数，用于在校验 ticket 后，给 sso-client 端追加返回信息。
	- 新增：SSO章节文档新增用户数据同步/迁移方案的建议。
	- 修复：修复利用@字符可以绕过域名允许列表校验的漏洞。 **[漏洞修复]**
	- 修复：禁止 `allow-url` 配置项 * 符号出现在中间位置，因为这有可能导致校验被绕过。 **[漏洞修复]**
- 新增插件/示例：
	- 新增：新增插件 `sa-token-hutool-timed-cache`，用于整合 Hutool 缓存插件 TimedCache。 **[重要]**
	- 新增：新增 SSM 架构整合 Sa-Token 简单示例。 	**[重要]**
	- 新增：新增 beetl 整合 Sa-Token 简单示例。 	**[重要]**
- 文档：
	- 部分章节将 `@Autowired` 更换为更合适的 `@PostConstruct`
	- 新增过滤器执行顺序更改示例。
- 其它：
	- 优化：将跨域处理demo拆分为独立仓库。
	- 优化：解决 springboot 集成 sa-token 后排除 jackson 依赖无法成功启动的问题。
	- 优化：解决 `sa-token-jwt` 模块重复设置 keyt 秘钥问题。（感谢`KonBAI`提交的pr）
	- 优化：jwt模式 token 过期后，抛出的异常描述是 token 已过期，而不再是 token 无效。
	- 修复：补齐 `sa-token-spring-aop` 模块中遗漏监听的注解。


### v1.37.0 @2023-10-18
- 修复：修复路由拦截鉴权可被绕过的问题。 **[漏洞修复]**
- 重构：未登录时调用鉴权 API 抛出未登录异常而不再是无权限异常。
- 优化：优化 SaTokenDao 组件更换时的逻辑。
- 文档：提供 SpringBoot3.x 路由匹配出错的解决方案。


### v1.36.0 @2023-9-22
- sa-token-core：
	- 修复：API接口签名校验参数接口NPE问题，增加必须参数的非空校验处理。
	- 新增：加密工具类新增 sha384、sha512 实现。 感谢 `@若初995` 提交的pr。   **[重要]**
	- 修复：`SaFoxUtil.vagueMatch()` 正则匹配的一些问题。  **[漏洞修复]**
	- 修复：`SaRouter.match()` 路由匹配的一些问题。  **[漏洞修复]**
- 其它：
	- 优化：`sa-token-alone-redis` 去掉不必要的配置项判断。
	- 新增：`sa-token-solon-plugin` 增加对 solon 网关的支持。
	- 新增：新增第三方插件专用仓库：`sa-token-three-plugin` 。
	- 升级：`sa-token-solon-plugin` 增加对 solon 网关的支持。
- 文档：
	- 新增：新增开启全局懒加载时不能注入上下文处理器的处理方案 。
	- 新增：新增 RefreshToken 示例。 **[重要]**
	- 新增：文档新增 sa-token 小助手，可在线实时技术提问。 **[重要]**
	- 优化：其它一些优化。
- 新增插件：
	- `sa-token-redisson-jackson2`：通用 redisson 集成方案 （spring, solon, jfinal 等都可用）



### v1.35.0 @2023-6-23
- sa-token-core：
	- 优化：前端未提供 token 时，`getTokenSession()` 将抛出未登录异常，而不是返回 null。 **[不向下兼容]**
	- 新增：SaSession 新增字段：`type`、`loginType`、`loginId`、`token`。
	- 重构：全局过滤器抽离 SaFilter 统一接口。
	- 重构：全局过滤器 `includeList`、`excludeList` 改为 public，同时移除对应的 getter 方法。 **[不向下兼容]** 
	- 重构：将全局过滤器的 BeforeAuth 认证设为不受 `includeList` 与 `excludeList` 的限制，所有请求都会进入。 **[不向下兼容]** 
	- 新增：新增循环生成 token 的算法，用于确保 Token 的唯一性。 **[重要]** 
	- 重构：API 接口签名所有方法均迁移至 core 核心模块。 **[重要]** 
	- 新增：新增彩色日志打印，更方便的分辨不同日志等级。 **[重要]** 
	- 重构：重构概念：临时有效期 -> token 最低活跃频率，过期后 token 冻结。
	- 重构：重构概念：`User-Session` -> `Account-Session`。
	- 新增：新增 `getTokenTimeout(String token)` 方法，获取任意 token 剩余有效期。
	- 优化：在登录时增加判断当前 StpLogic 是否支持 extra 扩展参数模式，如果不支持则打印警告信息。
	- 新增：NotLoginException 增加新场景值 -6、-7，提供更精确的未登录异常描述信息。
	- 新增：TokenSign 新增 tag 挂载参数，可在登录时方便的存储一些客户端特有数据。  **[重要]** 
	- 新增：新增 `SaStrategy#createStpLogic`，用于指定动态创建 StpLogic 时的算法策略。
	- 新增：新增 `@SaCheckOr` 批量注解鉴权：只要满足其中一个注解即可通过验证。  **[重要]** 
	- 重构：重命名：`SaStrategy.me` -> `SaStrategy.instance`。
	- 重构：在登录时强制性检查账号 id 是否为异常值，如果是则登录失败。
	- 重构：重构概念：`activity-timeout` -> `active-timeout`。  **[重要]** 
	- 新增：新增动态 `active-timeout` 能力，可在每次登录时指定 `active-timeout` 值。  **[重要]** 
	- 优化：将 `SaStrategy` 所有策略声明抽离为单独的函数式接口。
	- 新增：增加为 StpLogic 单独配置 `SaTokenConfig` 参数的能力。
	
- sa-token-sso：
	- 修复：在 SSO 模式三中 `ticket` 校验地址配错时，会出现 NPE 的问题 
	- 新增：新增 `getData` 接口配置，在模式三拉取数据时可以传递任意参数。 **[重要]** 
	- 重构：模式三秘钥配置更改：`sa-token.sso.secretkey=xxx` -> `sa-token.sign.secret-key=xxx`。 **[不向下兼容]**
	- 重构：模式三校验签名方法更改：`SaSsoUtil.checkSign(req)` -> `SaSignUtil.checkRequest(req)`。 **[不向下兼容]**
	- 新增：新增 `sa-token.sso.mode` 配置项，用于约定此系统使用的 SSO 模式。
	- 优化：优化校验 ticket 的逻辑。
	
- sa-token-jwt：
	- 修复：jwt 令牌的签名类型可以被篡改的问题。 **[重要]** 
	
- 其它：
	- 优化：所有模块优化注释，更方便开发者阅读源码。
	- 优化：在所有 .java 文件中添加 license 头说明。
	- 优化：修复大部分代码警告。
	- 新增：新增 thymeleaf 标签方言命名空间，增强 ide 代码提示。 **[重要]** 
	- 新增：定义 `sa-token-bom` 包，方便引入 sa-token 时对齐版本。
	- 新增：sa-token-dubbo3 插件新增代码示例。
	- 新增：新增跨域文章和示例：Header 参数版和第三方 Cookie 版。 **[重要]** 
	- 修复：修复 `sa-token-alone-redis` 在低版本 springboot 下无法启动成功(缺少 `username` 属性)的问题。
	
- 新增插件：
	- 新增：新增 `sa-token-context-dubbo3` 插件。 感谢 `@qiudaozhang` 提交的 pr。 **[重要]** 

- 文档：
	- 新增：部分常见报错排查。
	- 新增：首页图片增加懒加载效果，节省流量。
	- 新增：增加 Cookie 配置示例。
	- 修复：整理 demo 结构目录结构，修复不正确的路径说明。
	- 新增：新增 api-sign 模块文档。  **[重要]**  
	
- 简化包名  **[重要]**  **[不向下兼容]** 
	- `sa-token-dao-redis` -> `sa-token-redis`
	- `sa-token-dao-redis-jackson` -> `sa-token-redis-jackson`。
	- `sa-token-dao-redis-fastjson` -> `sa-token-redis-fastjson`。
	- `sa-token-dao-redis-fastjson2` -> `sa-token-redis-fastjson2`。
	- `sa-token-dao-redisson-jackson` -> `sa-token-redisson-jackson`。
	- `sa-token-dao-redisx` -> `sa-token-redisx`。
	- `sa-token-context-dubbo` -> `sa-token-dubbo`。
	- `sa-token-context-dubbo3` -> `sa-token-dubbo3`。
	- `sa-token-context-grpc` -> `sa-token-grpc`。


### v1.34.0 @2023-1-11

新增插件：
- 新增：新增 `SpringBoot3.x` 集成插件，感谢 `@jry` 提供的参考思路。   **[重要]**
- 新增：新增 `sa-token-dao-redisson-jackson` 插件，感谢 `@疯狂的狮子Li` 提交的pr。   **[重要]**

sa-token-core 核心包：
- 升级：升级 Sign 签名模块，增加部分重载方法。
- 重构：`SaSignTemplate#joinParams` 更名为 `joinParamsDictSort`。  **[不向下兼容]**
- 升级：升级临时 Token 认证模块，可指定 service 参数。
- 删除：彻底删除过期类 `SaAnnotationInterceptor` 和 `SaRouteInterceptor`。
- 修复：修复源码注释和文档的部分不合适之处。

sa-token-sso 单点登录：
- 删除：SSO 模块移除过期类 `SaSsoHandle` 类。
- 新增：SSO 模块增加 ticket 的 client 锁定功能，解决部分场景下的 ticket 劫持问题。  **[重要]**
- 修复：修复 SSO 模式2，在 client 端配置 `is-share=false` 时无法单点注销的问题。
- 修复：修复 SSO 模式3 部分场景下注销时无法正常回退页面的问题。

其它模块：
- sa-token-oauth2：修复 OAuth2 模块示例 getClientModel 方法 clientId 写错的问题。
- sa-token-alone-redis：新增：Alone-Redis 新增集群配置能力，感谢 `@appleOfGray` 提交的pr。   **[重要]**
- sa-token-jwt：重构：使用 jwt-simple 模式后 is-share 恒等于 false，无论是否有设定 `setExtra` 数据。



### v1.33.0 @2022-11-16
- 重构：重构异常状态码机制。   **[重要]**
- 重构：重构 sa-token-sso 模块异常码改为 300 开头，sa-token-jwt 异常码改为 302 开头。  **[不向下兼容]**
- 新增：新增全局 Log 模块。   **[重要]**
- 重构：`SaTokenListenerForConsolePrint` 改名 `SaTokenListenerForLog`。   **[不向下兼容]**
- 修复：修复多线程下 `SaFoxUtil.getRandomString()` 随机数重复问题。
- 修复：修复 sa-token-demo-sso3-client-nosdk 项目中单点注销 url 配置错误的问题
- 文档：文档优化。



### v1.32.0 @2022-10-28
- 修复：修复 sa-token-dao-redis-fastjson 插件多余序列化 `timeout` 字段的问题。
- 修复：修复 sa-token-dao-redis-fastjson 插件 `session.getModel` 无法反序列化实体类的问题。
- 修复：修复 `sa-token-quick-login` 插件指定拦截排除路由不生效的问题。
- 修复：修复 `sa-token-alone-redis` + `sa-token-dao-redis-fastson` 时 Redis 无法分离的问题。
- 修复：修复在配置了 cookie.path 后，注销时无法彻底清除 Cookie 的问题。
- 升级：`SaFoxUtil.getValueByType()` 新增对 char 类型的转换。
- 新增：新增 `sa-token-dao-redis-fastjson2` 插件。 **[重要]** 
- 新增：新增全局配置 `is-write-header`，控制登录后是否将 Token 写入响应头。 **[重要]** 
- 新增：二级认证模块新增指定业务标识能力。  **[重要]** 
- 重构：Id-Token 模块更名为 Same-Token。 **[重要]** **[不向下兼容]**
- 重构：重构会话查询参数作用：由`start=-1`时查询全部会话，改为 `start=0,size=-1` 时查询全部。 **[不向下兼容]** 
- 重构：`SaManager.getStpLogic("type")` 默认当对应type不存在时不再抛出异常，而是自动创建并返回。
- 重构：重构SSO模块，静态式API改为实例式：SaSsoHandle -> SaSsoProcessor。 **[重要]** **[不向下兼容]** 
- 重构：SSO-Server 端单点注销地址修改 `/sso/logout` -> `/sso/signout`，避免与 SSO-Client 端同 path 的冲突。 **[不向下兼容]** 
- 新增：文档新增 SSO 平台中心模式示例，跳连接进入子系统。 **[重要]** 
- 新增：新增SSO前后端分离集成示例 vue2 & vue3 版本。  **[重要]** 
- 重构：SSO 示例项目 http 请求工具改为 Forest。
- 新增：SSO模块文档新增单个项目同时搭建 `sso-server` 和 `sso-client` 的示例。 **[重要]** 
- 新增：SSO模块文档新增一个项目同时搭建两个 `sso-server` 服务 的示例。 **[重要]** 
- 文档：在线文档新增代码示例。
- 文档：在线文档增加全局调色功能。
- 文档：[自定义 SaTokenContext 指南] 章节新增对三种模型的解释。
- 文档：新增多账号体系混合鉴权代码示例。
- 文档：文档增加 `Gradle` 依赖方式和 `properties` 风格配置。
- 新增：新增 `sa-token-dependencies`，统一定义依赖版本。 **[重要]** 

##### 已知问题：

> 部分场景下 Token 重复问题，受影响版本 `=v1.32.0`
> - 受影响模块：
> 	- sa-token-core 切换了 Token 风格：tik、random-32、random-64、random-128，如果使用 默认uuid、simple-uuid 风格则不受影响。
> 	- sa-token-core 使用了临时 Token 认证模块，如果集成了 sa-token-temp-jwt 则不受影响。
> 	- sa-token-core 使用了 Same-Token 模块。
> 	- sa-token-jwt 全模块
> 	- sa-token-oauth2 全模块
> 	- sa-token-sso 模式二和模式三



### v1.31.0 @2022-9-8
- 文档：新增优秀开源案例展示。
- 文档：新增博客展示，欢迎大家投稿。 
- 新增：新增 `SaInterceptor` 综合拦截器。   **[重要]**  **[不向下兼容]**
- 新增：新增 新增 `@SaIgnore` 忽略鉴权注解。   **[重要]** 
- 新增：新增插件 `sa-token-dao-redis-fastjson`，感谢 `@sikadai` 提交的pr。  **[重要]**
- 新增：新增插件 `sa-token-context-grpc`，感谢 `@LiYiMing666` 提交的pr。  **[重要]**
- 重构：SaSession 取消 `tokenSignList` 的 final 修饰符。
- 新增：SaSession 添加 `setTokenSignList` 方法。
- 重构：TokenSign 新增 `setValue` 和 `setDevice` 方法。
- 修复：修复多账号模式下不能正确重置 `StpLogic` 的问题。
- 修复：修复 SaSession 对象中 TokenSign 判断有可能空指针的问题。 
- 修复：解决当权限码为 null 时可能带来的空指针问题。
- 新增：新增 `StpUtil.getExtra(tokenValue, key)` 方法，用于获取任意 token 的扩展参数。 
- 优化：优化 `StpLogic#logoutByTokenValue` 方法逻辑，精简代码。
- 重构：`SaTokenConfig` 配置类字段 `isReadHead` 改为 `isReadHeader`。 **[不向下兼容]** 
- 修复：修复部分场景下踢人下线会抛出异常 `非Web上下文无法获取Request` 的问题。 
- 新增：新增方法 `StpLogic#getAnonTokenSession`，可在未登录情况下安全的获取 Token-Session。  **[重要]** 
- 新增：新增 `SaApplication` 对象，用于全局作用域存取值。   **[重要]** 
- 重构：将 `SaTokenListener` 改为事件发布订阅模式，允许同时注册多个侦听器。  **[重要]**  **[不向下兼容]**
- 重构：**StpUtil.login(id) 不再强制校验账号是否禁用，需要手动校验。** **[不向下兼容]** 
- 重构：`DisableLoginException` 更换名称为 `DisableServiceException`。 **[不向下兼容]** 
- 新增：新增对账号限制、分类封禁、阶梯封禁功能。	 **[重要]** 
- 新增：会话查询API增加反序获取会话方式。
- 新增：SSO模块增加 server-url 属性，用于简化各种 url 配置。  **[重要]**
- 修复：修复单点登录模块 `ssoLogoutCall` 配置项无效的问题。 
- 优化：优化 `SaSsoHandle.checkTicket(ticket, currUri);` 方法，使其不提供 currUri 参数时将不再注册单点注销回调。
- 修复：修复 `SaOAuth2Handle` 类中 `doLogin` 方法没有使用 `Param.pwd` 常量的问题。 
- 新增：新增 `SaOAuth2Util.checkClientTokenScope(clientToken, scopes)` 方法，校验 Client-Token 是否含有指定 Scope。
- 删除：删除 `sa-token-jwt` 模块过期 class。
- 重构：`sa-token-jwt` 模块依赖改为 `hutool-jwt`，并升级版本为 5.8.5。
- 重构：`sa-token-jwt` 模块改为 `Util + Template` 形式，方便针对部分代码重写。  **[重要]** 
- 新增：在线文档添加API手册。
- 重构：`sa-token-oauth2` 模块密码模式新增 `client_secret` 参数校验。**[不向下兼容]** 
- 新增：集成 `jacoco` 插件，核心包单元测试覆盖率提高至 90% 以上。
- 优化：开源案例分离专属仓库：[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token)



### v1.30.0 @2022-05-9
- 新增：新增集成 Web-Socket 鉴权示例。 **[重要]**
- 新增：新增集成 Web-Socket（Spring封装版） 鉴权示例。
- 新增：新增 jfinal 集成包 `sa-token-jfinal-plugin`  **[重要]**
- 新增：新增 jboot 集成包 `sa-token-jboot-plugin` （感谢 @nxstv 提交的pr）
- 修复：修复整合 sa-token-jwt Style 模式时，`StpUtil.getExtra("key")` 无效的bug  
- 升级：升级 `sa-token-context-dubbo` dubbo版本：`2.7.11` -> `2.7.15`
- 升级：借助 `flatten-maven-plugin` 统一版本号定义 （感谢 @ruansheng8 提交的pr）   **[重要]**
- 修复：修复在 `springboot 2.6.x` 下 `quick-login` 插件循环依赖无法启动的问题 
- 优化：`sa-token-spring-aop` 依赖改为 `sa-token-core`，避免在webflux环境下启动报错的问题 
- 优化：源码注释 设备标识 改为 设备类型 更符合语义 
- 修复：解决部分协议下 dubbo 参数变为小写导致 `Id-Token` 鉴权无效的问题 
- 升级：单元测试升级为 JUnit5 
- 新增：新增 `maxLoginCount` 配置，指定同一账号可同时在线的最大数量   **[重要]** 
- 升级：彻底删除 SaTokenAction 接口，完全由 SaStrategy 代替 
- 新增：新增 `sa-token-dao-redisx` 插件，感谢 @noear 提交的pr  **[重要]** 
- 优化：增加 parseToken 未配置 jwt 密钥时的异常提示，感谢 @BATTLEHAWK00 提交的pr 
- 优化：sso,oauth2 插件中调用配置类使用 getter 方法，感谢 @Naah 提交的pr 
- 新增：新增 json 转换器模块 
- 重构：SaTokenListener#doLogin 方法新增 tokenValue 参数  **[不向下兼容]** 
- 升级：SpringBoot 相关组件依赖版本升级至 `2.5.12` 
- 文档：在线文档所有 `AjaxJson` 改为 `SaResult` 
- 文档：“多账号认证” -> 改为 “多账户认证” 
- 文档：部分章节新增动态演示图  **[重要]** 
- 升级：顶级异常类 `SaTokenException` 增加 code 异常细分状态码。[详见](/fun/exception-code) **[重要]** 
- **注意升级：受异常细分状态码影响，`NotPermissionException` 类中 `getCode()` 方法改为 `getPermission()`。** **[不向下兼容]**
- SSO 模块升级：
	- 重构：SSO 模块从核心包拆分为独立插件 `sa-token-sso` **[重要]** 
	- 优化：SSO模式三单点注销回调方法中，注销语句改为：`stpLogic.logout(loginId)` 更符合情景  
	- 修复：解决 sso 构建认证地址时，部分 Servlet 版本内部实现不一致带来的双 back 参数问题。
	- 升级：SSO 模块提供精细化异常处理 
	- 重构：SSO 模式三接口 `/sso/checkTicket`、`/sso/logout`，更改响应体格式   **[不向下兼容]** 
	- 优化：SSO 模式三单点注销搭建示例增加 `try-catch`，提高容错性  
	- 优化：`SsoUtil.singleLogout` 改为 `SsoUtil.ssoLogout`，且无需再提供 secretkey 参数   **[不向下兼容]** 
	- 升级：将 SSO 模式三的接口调用改为签名式校验。  **[重要]**  **[不向下兼容]** 
	- 新增：新增 SSO 模式三下无 sdk 的对接示例， 感谢 @Sa-药水 的建议反馈 	**[重要]** 
- sa-token-jwt 模块升级：
	- 重构：`sa-token-jwt` 的创建，强制校验loginType  **[不向下兼容]** 
	- 重构：`StpLogicJwtForStateless` 由重写 login 方法改为重写 createLoginSession 
	- 重构：`SaJwtUtil` 工具类不再吞并异常消息，且提供精细化异常 code 码。
	- 重构：改名：StpLogicJwtForStyle -> StpLogicJwtForSimple
	- 重构：改名：StpLogicJwtForMix -> StpLogicJwtForMixin
	- 修复：修复 `StpLogicJwtForSimple` 模式下 Extra 数据可能受到旧 token 影响的bug


### v1.29.0 @2022-02-10
- 升级：sa-token-jwt插件可在登录时添加额外数据。
- 重构：优化Dubbo调用时向下传递Token的规则，可避免在项目启动时由于Context无效引发的bug。
- 重构：OAuth2 授权模式开放由全局配置和Client单独配置共同设定。
- 重构：OAuth2 模块部分属性支持每个 Client 单独配置。
- 重构：OAuth2 模块部分方法名修复单词拼写错误：converXxx -> convertXxx。
- 重构：修复 OAuth2 模块 `deleteAccessTokenIndex` 回收 token 不彻底的bug。
- 新增：OAuth2 模块新增 `pastClientTokenTimeout`，用于指定 PastClientToken 默认有效期。
- 文档：常见报错章节增加目录树，方便查阅。
- 文档：优化文档样式。
- 新增：新增 BCrypt 加密。
- 修复：修复StpUtil.getLoginIdByToken(token) 在部分场景下返回出错的bug。
- 重构：优化OAuth2模块密码式校验步骤。
- 新增：新增Jackson定制版Session，避免timeout属性的序列化。
- 新增：SaLoginModel新增setToken方法，用于预定本次登录产生的Token。 
- 新增：新增 StpUtil.createLoginSession() 方法，用于无Token注入的方式创建登录会话。 
- 新增：OAuth2 与 StpUtil 登录会话数据互通。
- 新增：新增 `StpUtil.renewTimeout(100);` 方法，用于 Token 的 Timeout 值续期。 
- 修复：修复默认dao实现类中 `updateObject` 无效的bug 
- 完善：完善单元测试。


### v1.28.0 @2021-11-5
- 新增：新增 `sa-token-jwt` 插件，用于与jwt的整合 **[重要]**
- 新增：新增 `sa-token-context-dubbo` 插件，用于与 Dubbo 的整合 **[重要]**
- 文档：文档新增章节：Sa-Token 插件开发指南 **[重要]**
- 文档：文档新增章节：名称解释
- 优化：抽离 `getSaTokenDao()` 方法，方便重写 
- 新增：单元测试新增多账号模式数据不互通测试
- 优化：优化在线文档，修复部分错误之处 	
- 优化：优化未登录异常抛出提示，标注无效的Token值 
- 修复：修复单词拼写错误 `getDeviceOrDefault` 
- 优化：优化 jwt 集成示例 
- 文档：新增常见问题总结


### v1.27.0 @2021-10-11
- 升级：增强 SaRouter 链式匹配能力  	**[重要]**  	
- 新增：新增插件 Thymeleaf 标签方言   **[重要]**  	
- 新增：@SaCheckPermission 增加 orRole 字段，用于权限角色“双重or”匹配    **[重要]**
- 升级：Cookie模式增加 `secure`、`httpOnly`、`sameSite`等属性的配置 	**[重要]**  	
- 重构：重构SSO三种模式，抽离出统一的认证中心   **[重要]**   
- 新增：新增 SaStrategy 策略类，方便内部逻辑按需重写 **[重要]**		
- 新增：临时认证模块新增 deleteToken 方法用于回收 Token  
- 新增：新增 kickout、replaced 等注销会话的方法，更灵活的控制会话周期  **[重要]** 
- 新增：权限认证增加API：`StpUtil.hasPermissionAnd`、`StpUtil.hasPermissionOr` 
- 新增：角色认证增加API：`StpUtil.hasRoleAnd`、`StpUtil.hasRoleOr` 
- 新增：新增 `StpUtil.getRoleList()` 和 `StpUtil.getPermissionList()` 方法  
- 新增：新增 StpLogic 自动注入特性，可快速方便的扩展 StpLogic 对象 
- 优化：优化同端互斥登录逻辑，如果登录时没有指定设备类型标识，则默认顶替所有设备类型下线  
- 优化：在未登录时调用 hasRole 和 hasPermission 不再抛出异常，而是返回false 
- 升级：升级注解鉴权算法，并提供更简单的重写方式    
- 文档：新增常见报错排查，方便快速排查异常报错 
- 文档：文档新增SSO单点登录与OAuth2技术选型对比  
- 破坏式更新：
	- [向下兼容] 废弃 SaTokenAction 接口，替代方案： SaStrategy  
	- [向下兼容] 移除 `StpUtil.logoutByLoginId()` 更换为 `StpUtil.kickout()`;
	- [不向下兼容] 侦听器 doLogoutByLoginId 与 doReplaced 方法移除 device 参数 
	- [不向下兼容] 侦听器 doLogoutByLoginId 方法重命名为 doKickout  


### v1.26.0 @2021-9-2
- 优化：优化单点登录文档 
- 新增：新增 `Http Basic` 认证 **[重要]** 
- 新增：文档新增跨域解决方案 
- 文档：新增 Nginx 转发请求丢失uri的解决方案
- 文档：新增 SSO 自定义 API 路由示例  **[重要]** 
- 示例：新增 `SSO-Server` 端前后端分离示例  **[重要]** 


### v1.25.0 @2021-8-16
- 新增：`SaRequest`新增`getHeader(name, defaultValue)`方法，用于获取header默认值 
- 新增：`SaRequest` 添加 `forward` 转发方法  
- 新增：Readme新增源码模块介绍、友情链接、正在使用Sa-Token的项目 
- 重构：重构SSO单点登录模块源码，增加可读性 
- 新增：SSO配置表新增所属端说明 
- 新增：SSO模式三新增账号资料同步示例  **[重要]** 
- 新增：前后端分离模式下接入SSO的示例  **[重要]** 
- 优化：优化SSO单点注销重定向逻辑 
- 重构：重构SSO单点登录模块部分API 
- 优化：优化SaQuickBean中过滤器处理逻辑  
- 文档：优化文档样式，增加示例  
- 文档：代码鉴权、注解鉴权、路由拦截鉴权，选择指南 
- 文档：文档新增 SSO旧有系统改造指南 
- 文档：SSO集成文档里添加API列表 
- 文档：新增 `Sa-Token-Study` 链接，讲解 Sa-Token 源码涉及到的技术点 
- 不兼容更新重构：
	- 重构：修复 `SaReactorHolder.getContent()` 拼写错误：`content` -> `context` 


### v1.24.0 @2021-7-24
- 修复：修复部分场景下Alone-Redis插件导致项目无法启动的问题
- 优化：增加对SpringBoot1.x版本的兼容性 
- 新增：SaOAuth2Util新增checkScope函数，用于校验令牌是否具备指定权限 
- 新增：OAuth2.0模块新增revoke接口，用于提前回收 Access-Token 令牌 
- 新增：新增`Sa-Id-Token` 模块，解决微服务内部调用鉴权  **[重要]**
- 文档：新增OAuth2.0模块常用方法说明  
- 优化：大幅度优化文档示例 


### v1.23.0 @2021-7-19
- 新增：Sa-Token-OAuth2 模块正式发布   **[重要]** 
- 修复：修复jwt集成demo无法正确注册StpLogic的bug
- 修复：修复登录时某些场景下Session续期可能不正常的bug  
- 优化：代码注释优化，文档优化  


### v1.22.0 @2021-7-10
- 新增：SaSsoConfig 部分属性增加set连缀风格 
- 优化：SaSsoUtil 可定制化底层的 `StpLogic`
- 新增：新增 `SaSsoHandle` 大幅度简化单点登录整合步骤  **[重要]** 
- 新增：新增Sa-Token在线测评，链接：[https://ks.wjx.top/vj/wFKPziD.aspx](https://ks.wjx.top/vj/wFKPziD.aspx)  **[重要]**
- 新增：Sa-Token-Quick-Login 插件新增拦截与放行路径配置
- 优化：大幅度优化文档示例 


### v1.21.0 @2021-7-2
- 新增：新增Token二级认证 	**[重要]** 
- 新增：新增`Sa-Token-Alone-Redis`独立Redis插件   **[重要]**  
- 新增：新增SSO三种模式，彻底解决所有场景下的单点登录问题   **[重要]**  
- 新增：新增多账号模式下，注解合并示例		**[重要]**  
- 新增：新增`SaRouter.back()`函数，用于停止匹配返回结果  
- 不兼容更新重构：
	- 更改yml配置前缀：原`[spring.sa-token.]` 改为 `[sa-token.]`，目前版本暂时向下兼容，请尽快更新 


### v1.20.0 @2021-6-17
- 新增：新增Solon适配插件，感谢大佬 `@刘西东` 提供的pr **[重要]** 
- 新增：新增`SaRouter.stop()`函数，用于一次性跳出匹配链功能 **[重要]** 
- 新增：新增单元测试   **[重要]** 
- 新增：新增临时令牌验证模块   **[重要]**  
- 新增：新增`sa-token-temp-jwt`模块整合jwt临时令牌鉴权    **[重要]**  
- 新增：会话 `SaSession.get()` 增加缓存API，简化代码 
- 新增：新增框架调查问卷 
- 修复：修复同时引入 `Spring Cloud Bus` 与 `Sa-Token` 冲突的问题   **[重要]** 
- 修复：修复`SaServletFilter`异常函数中无法自定义`Content-Type`的问题 
- 文档：新增微服务依赖引入说明 
- 文档：新增认证流程图 
- 不兼容更新重构：
	- 方法：`StpUtil.setLoginId(id)` -> `StpUtil.login(id)` 
	- 方法：`StpUtil.getLoginKey()` -> `StpUtil.getLoginType()` (注意其它所有地方的`LoginKey`均已更改为`loginType`)
	- 工具类：`SaRouterUtil` -> `SaRouter` 
	- 配置类：`allowConcurrentLogin` -> `isConcurrent` 
	- 配置类：`isV` -> `isPrint` 
	- 为保证平滑更新，旧API仍旧保留，但已增加`@Deprecated`注解，请尽快更新至新API  


### v1.19.0 @2021-5-10
- 新增：注解鉴权新增定制loginType功能  **[重要]** 
- 重构：重构目录结构，抽离`plugin`模块  **[重要]** 
- 新增：新增 `sa-token-quick-login` 插件，零代码集成登录功能  **[重要]** 
- 优化：所有函数式接口增加`@FunctionalInterface`注解，感谢群友`@MrXionGe`提供的建议 
- 优化：文档优化... 


### v1.18.0 @2021-4-24
- 新增：新增权限通配符功能，灵活设置权限  **[重要]** 
- 修复：修复自动续签处的逻辑错误 
- 新增：新增Web开发常见漏洞防护建议 
- 修复：修复`SaRequest`中缺少`getMethod()`的bug 
- 修复：修复自动续签时的逻辑错误，感谢群成员`@N`的建议 
- 新增：全局过滤器新增 `beforAuth` 前置函数 
- 修复：修复在带有上下文的项目中无法正确获取请求路径的bug，感谢群成员`@dlwlrma`提供的建议
- 新增：新增`SaHolder`上下文持有类，可方便的在上下文中读写数据 
- 重构：`SaTokenManager` -> `SaManager` 
- 重构：`SaTokenInsideUtil` -> `SaFoxUtil` 


### v1.17.0 @2021-4-17
- 修复：在WebFlux环境中引入Redis集成包无法启动的问题 
- 修复：修复JWT集成示例中版本升级API的变更 
- 优化：优化启动时字符画打印
- 文档：新增集成环境说明
- 文档：新增功能介绍图  
- 新增：全局过滤器增加限定[拦截路径]与[排除路径]功能 
- 重构：全局过滤器执行函数放到成员变量里，连缀风格配置 
- 新增：新增全局侦听器，可在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作 **[重要]** 


### v1.16.0 @2021-4-12
- 新增：新增账号封禁功能，指定时间内账号无法登陆 			**[重要]**
- 新增：核心包脱离`ServletAPI`，彻底零依赖！  				**[重要]**
- 新增：新增基于`ThreadLocal`的上下文容器					**[重要]**
- 新增：新增`Reactor`响应式编程支持，`WebFlux`集成！			**[重要]** 
- 新增：新增全局过滤器，解决拦截器无法拦截静态资源的问题			**[重要]** 
- 新增：新增微服务网关鉴权方案！可接入`ShenYu`、`Gateway`等网关组件!	**[重要]** 
- 新增：AOP切面定义`Order`顺序为`-100`，可保证在多个自定义切面前执行 
- 文档：新增推荐公众号列表 


### v1.15.0 @2021-3-23
- 新增：文档添加源码涉及技术栈说明 
- 优化：优化路由拦截器模块文档，更简洁的示例
- 修复：修复非web环境下的错误提示，Request->Response
- 修复：修复Cookie注入时path判断错误，感谢@zhangzi0291提供的PR
- 新增：文档集成Redis章节新增redis配置示例说明，感谢群友 `@-)` 提供的建议
- 新增：增加token前缀模式，可在配置token读取前缀，适配`Bearer token`规范 **[重要]**
- 优化：`SaTokenManager`初始化Bean去除`initXxx`方法，优化代码逻辑
- 新增：`SaTokenManager`新增`stpLogicMap`集合，记录所有`StpLogic`的初始化，方便查找
- 新增：`Session`新增timeout操作API，可灵活修改Session的剩余有效时间 
- 新增：token前缀改为强制校验模式，如果配置了前缀，则前端提交token时必须带有
- 优化：精简`SaRouteInterceptor`，只保留自定义验证和默认的登陆验证，去除冗余功能 
- 优化：`SaRouterUtil`迁移到core核心包，优化依赖架构
- 优化：默认Dao实现类里`Timer定时器`改为子线程 + sleep 模拟 
- 新增：`Session`新增各种类型转换API，可快速方便存取值  **[重要]** 
- 升级注意：
	- `SaRouterUtil`类迁移到核心包，注意更换import地址
	- `SaRouteInterceptor`去出冗余API，详情参考路由鉴权部分


### v1.14.0 @2021-3-12
- 新增：新增`SaLoginModel`登录参数Model，适配 [记住我] 模式	 **[重要]**
- 新增：新增 `StpUtil.login()` 时指定token有效期，可灵活控制用户的一次登录免验证时长 
- 新增：新增Cookie时间判断，在`timeout`设置为-1时，`Cookie`有效期将为`Integer.MAX_VALUE`	 **[重要]**
- 新增：新增密码加密工具类，可快速MD5、SHA1、SHA256、AES、RSA加密 	**[重要]**
- 新增：新增 OAuth2.0 模块  	**[重要]** 
- 新增：`SaTokenConfig`配置类所有set方法支持链式调用 
- 新增：`SaOAuth2Config` sa-token oauth2 配置类所有set方法新增支持链式调用 
- 优化：`StpLogic`类所有`getKey`方法重名为`splicingKey`，更语义化的函数名称 
- 新增：`IsRunFunction`新增`noExe`函数，用于指定当`isRun`值为`false`时执行的函数 
- 新增：`SaSession`新增数据存取值操作API 
- 优化：优化`SaTokenDao`接口，增加Object操作API
- 优化：jwt示例`createToken`方法去除默认秘钥判断，只在启动项目时打印警告 
- 文档：常见问题新增示例(修改密码后如何立即掉线)
- 文档：权限认证文档新增[如何把权限精确搭到按钮级]示例说明 
- 文档：优化文档，部分模块添加图片说明 


### v1.13.0 @2021-2-9
- 优化：优化源码注释与文档
- 新增：文档集成Gitalk评论系统 
- 优化：源码包`Maven`版本号更改为变量形式 
- 修复：文档处方法名`getPermissionList`错误的bug 
- 修复：修复`StpUtil.getTokenInfo()`会触发自动续签的bug 
- 修复：修复接口 `SaTokenDao` 的 `searchData` 函数注释错误 
- 新增：`SaSession`的创建抽象到`SaTokenAction`接口，方便按需重写 
- 新建：框架内异常统一继承 `SaTokenException` 方便在异常处理时分辨处理 
- 新增：`SaSession`新增`setId()`与`setCreateTime()`方法，方便部分框架的序列化 
- 新增：新增`autoRenew`配置，用于控制是否打开自动续签模式
- 新增：同域模式下的单点登录  **[重要]**
- 新增：完善分布式会话的文档说明


### v1.12.0 @2021-1-12
- 新增：提供JWT集成示例 **[重要]**
- 新增：新增路由式鉴权，可方便的根据路由匹配鉴权  **[重要]**
- 新增：新增身份临时切换功能，可在一个代码段内将会话临时切换为其它账号  **[重要]**
- 优化：将`SaCheckInterceptor.java`更名为`SaAnnotationInterceptor.java`，更语义化的名称 
- 优化：优化文档
- 升级：v1.12.1，新增`SaRouterUtil`工具类，更方便的路由鉴权   **[重要]**


### v1.11.0 @2021-1-10
- 新增：提供AOP注解鉴权方案 **[重要]**
- 优化自动生成token的算法


### v1.10.0 @2021-1-9
- 新增：提供查询所有会话方案  **[重要]**
- 修复：修复token设置为永不过期时无法正常被顶下线的bug，感谢github用户 @zjh599245299 提出的bug


### v1.9.0 @2021-1-6
- 优化：`spring-boot-starter-data-redis` 由 `2.3.7.RELEASE` 改为 `2.3.3.RELEASE` 
- 修复：补上注解拦截器里漏掉验证`@SaCheckRole`的bug
- 新增：新增同端互斥登录，像QQ一样手机电脑同时在线，但是两个手机上互斥登录  **[重要]**


### v1.8.0 @2021-1-2
- 优化：优化源码注释
- 修复：修复部分文档错别字 
- 修复：修复项目文件夹名称错误
- 优化：优化文档配色，更舒服的代码展示
- 新增：提供`sa-token`集成 `redis` 的 `spring-boot-starter` 方案  **[重要]**
- 新增：新增集成 `redis` 时，以`jackson`作为序列化方案  **[重要]**
- 新增：dao层默认实现增加定时清理过期数据功能  **[重要]**
- 新增：新增`token专属session`, 更灵活的会话管理  **[重要]**
- 新增：增加配置，指定在获取`token专属session`时是否必须登录
- 新增：在无token时自动创建会话，完美兼容`token-session`会话模型!   **[重要]**
- 修改：权限码限定必须为String类型 
- 优化：注解验证模式由boolean属性改为枚举方式
- 删除：`StpUtil`删除部分冗长API，保持API清爽性
- 新增：新增角色验证 (角色验证与权限验证已完全分离)  **[重要]**
- 优化：移除`StpUtil.kickoutByLoginId()`API，由`logoutByLoginId`代替
- 升级：开源协议修改为`Apache-2.0`


### v1.7.0 @2020-12-24
- 优化：项目架构改为maven多模块形式，方便增加新模块 **[重要]**
- 优化：与`springboot`的集成改为`springboot-starter`模式，无需`@SaTokenSetup`注解即可完成自动装配 **[重要]**
- 新增：新增`activity-timeout`配置，可控制token临时过期与续签功能 **[重要]**
- 新增：`timeout`过期时间新增-1值，代表永不过期 
- 新增：`StpUtil.getTokenInfo()`改为对象形式，新增部分常用字段 
- 优化：解决在无cookie模式下，不集成redis时会话无法主动过期的问题 
- 修复：修复文档首页样式问题 


### v1.6.0 @2020-12-17
- 新增：花式token生成方案 **[重要]** 
- 优化：优化`readme.md` 
- 修复：修复`SaCookieOper`与`SaTokenAction`无法自动注入的问题 


### v1.5.1 @2020-12-16
- 新增：细化未登录异常类型，提供五种场景值：未提供token、token无效、token已过期 、token已被顶下线、token已被踢下线 **[重要]**
- 修复：修复`StpUtil.getSessionByLoginId(String loginId)`方法转换key出错的bug，感谢群友 @(＃°Д°)、@一米阳光 发现的bug 
- 优化：修改方法`StpUtil.getSessionByLoginId(Object loginId)`的isCreate值默认为true 
- 修改：`方法delSaSession`修改为`deleteSaSession`，更加语义化的函数名称 
- 新增：新增`StpUtil.getTokenName()`方法，更语义化的获取tokenName 
- 新增：新增`SaTokenAction`框架行为Bean，方便重写逻辑 
- 优化：`Cookie操作`改为接口代理模式，使其可以被重写 
- 优化：文档里集成redis部分增加redis的pom依赖示例
- 修复：登录验证-> `StpUtil.getLoginId_defaultNull()` 修复方法名错误的问题 
- 优化：优化`readme.md` 
- 升级：开源协议修改为`MIT`


### v1.4.0 @2020-9-7
- 优化：修改一些函数、变量名称，使其更符合阿里java代码规范
- 优化：`tokenValue`的读取优先级改为：`request` > `body` > `header` > `cookie`  **[重要]**
- 新增：新增`isReadCookie`配置，决定是否从`cookie`里读取`token`信息 
- 优化：如果`isReadCookie`配置为`false`，那么在登录时也不会把`cookie`写入`cookie` 
- 新增：新增`getSessionByLoginId(Object loginId, boolean isCreate)`方法
- 修复：修复文档部分错误，修正群号码


### v1.3.0 @2020-5-2
- 新增：新增 `StpUtil.checkLogin()` 方法，更符合语义化的鉴权方法
- 新增：注册拦截器时可设置 `StpLogic` ，方便不同模块不同鉴权方式
- 新增：抛出异常时增加 `loginType` 区分，方便多账号体系鉴权处理 
- 修复：修复启动时的版本字符画版本号打印不对的bug  
- 修复：修复文档部分不正确之处
- 新增：新增文档的友情链接


### v1.2.0 @2020-3-7
- 新增：新增注解式验证，可在路由方法中使用注解进行权限验证  **[重要]**
- 参考：[注解式验证](use/at-check)


### v1.1.0 @2020-2-12
- 修复：修复`StpUtil.getLoginId(T defaultValue)`取值转换错误的bug


### v1.0.0 @2020-2-4
- 第一个版本出炉

---

### Sa Token Doc New Docs More Wenjuan (`sa-token-doc-new/docs/more/wenjuan.md`)

---
title: "Sa-Token 问卷调查"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,问卷调查,其它"
description: "Sa-Token 用户问卷调查：我们想按运营产品的心态打磨开源框架，因此迫切希望你抽空认真填写这份短卷。全卷仅 6 道选择题、没有简答题，大约占用 1 到 3 分钟。结果会用于优化文档示例、社区体验与功能优先级，每一条意见我们都会认真看待。请通过本页给出的腾讯问卷链接提交，我们会认真阅读每一份答卷。"
---

# 问卷调查

我们想以运营一款产品的心态来运营一个开源框架，所以我们迫切希望您能够填写这份问卷，这有6道选择题，应该只会略微占用您1-3分钟的时间，Sa-Token将会非常重视每一位粉丝的宝贵意见！

[https://wj.qq.com/s2/14587150/b5b4/](https://wj.qq.com/s2/14587150/b5b4/)

---

### Sa Token Doc New Docs Oauth2 Oauth2 Apidoc (`sa-token-doc-new/docs/oauth2/oauth2-apidoc.md`)

---
title: "Sa-Token OAuth2-Server端开放 API 接口"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,OAuth2-Server端开放 API 接口,OAuth2.0,OAuth2"
description: "OAuth2-Server 开放 API：授权码走 /oauth2/authorize 再 /oauth2/token 换令牌；另有 refresh、revoke、userinfo。doLogin、doConfirm 处理登录与确认，并覆盖 implicit、password、client_credentials。"
---

# Sa-Token-OAuth2 Server端 API列表
基于官方仓库的搭建示例，`OAuth2-Server`端会暴露出以下API，`OAuth2-Client`端可据此文档进行对接  

--- 

## 1、模式一：授权码（Authorization Code）

### 1.1、获取授权码

根据以下格式构建URL，引导用户访问 （复制时请注意删减掉相应空格和换行符）
``` url
http://{host}:{port}/oauth2/authorize
	?response_type=code
	&client_id={client_id}
	&redirect_uri={redirect_uri}
	&scope={scope}
	&state={state}
```

参数详解：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| response_type	| 是		| 返回类型，这里请填写：`code`							|
| client_id		| 是		| 应用 id												|
| redirect_uri	| 是		| 用户确认授权后，重定向的 url 地址							|
| scope			| 否		| 具体请求的权限，多个用逗号(或空格)隔开						|
| state			| 否		| 随机值，此参数会在重定向时追加到url末尾，不填不追加，如果填写则每次填写的值不可以重复	|

注意点：
1. 如果用户在 `OAuth-Server` 端尚未登录：会被转发到登录视图，你可以参照文档或官方示例自定义登录页面。
2. 如果 `scope` 参数为空，或者请求的 `scope` 用户近期已确认授权过，则无需用户再次确认，达到静默授权的效果，否则需要用户手动确认，服务器才可以下放 `code` 授权码。

用户确认授权之后，会被重定向至`redirect_uri`，并追加 `code` 参数与 `state` 参数，形如：
``` url
redirect_uri?code={code}&state={state}
```

`Code` 授权码具有以下特点：
1. 每次授权产生的 `Code` 码都不一样。
2. `Code` 码用完即废，不能二次使用。
3. 一个 `Code` 的有效期默认为五分钟，超时自动作废。
4. 每次授权产生新 `Code` 码，会导致旧 `Code` 码立即作废，即使旧 `Code` 码尚未使用。


<details>
<summary>RestAPI 登录接口：/oauth2/doLogin</summary>

如果用户在 OAuth-Server 端尚未登录，则会被阻塞在登录界面，开始登录，需要在页面上调用`/oauth2/doLogin`完成登录（此接口非 OAuth2 标准协议接口）

``` url
http://{host}:{port}/oauth2/doLogin
	?name={name}
	&pwd={pwd}
```
参数详解：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| name			| 否		| 账号													|
| pwd			| 否		| 密码													|

访问此接口将进入自定义的 `cfg.doLoginHandle` 函数开始登录，你只要在此函数内调用 `StpUtil.login(xxx)` 即代表登录成功。

另外需要注意：此接口并非只能携带 `name`、`pwd` 参数，因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。

</details>


<details>
<summary>RestAPI 确认授权接口：/oauth2/doConfirm</summary>

如果 oauth-client 端申请的 scope 在 OAuth-Server 端需要用户手动确认授权，则会被阻塞在授权界面，
需要在页面上调用`/oauth2/doConfirm`完成授权（此接口非 OAuth2 标准协议接口）

``` url
http://{host}:{port}/oauth2/doConfirm
    ?client_id={value}
    &scope={value}
    &build_redirect_uri={true|false}
    &response_type={value}
    &redirect_uri={value}
    &state={value}
```
参数详解：

| 参数					| 是否必填	| 说明													|
| :--------				| :--------	| :--------												|
| client_id				| 是		| 应用 id												|
| scope					| 是		| 具体确认的权限，多个用逗号(或空格)隔开					|
| response_type			| 是		| 取 url 上的 `response_type` 参数来提交					|
| redirect_uri			| 是		| 取 url 上的 `redirect_uri` 参数来提交					|
| build_redirect_uri	| 否		| 是否立即构建 `redirect_uri` 授权地址，取值：true / false (默认)		|
| state					| 否		| 取 url 上的 `state` 参数来提交							|

此接口有两种调用方式，均需提交 `client_id`、`scope`、`response_type`、`redirect_uri` 参数（可直接取当前授权页 URL 上的 query 参数），区别仅在于是否立即构建授权地址：

**方式一**：不提供 `build_redirect_uri`（或设为 `false`），仅确认授权，返回结果代表是否确认授权成功：
``` js
{
    code: 200, 
    msg: 'ok', 
    data: null,
}
```

确认成功后，需再访问 `/oauth2/authorize` 完成授权跳转。

**方式二**：指定 `build_redirect_uri: true`，并同时提供 `state` 等参数，
此时返回结果包括最终的 code 授权地址：
``` js
{
    code: 200, 
    msg: 'ok', 
    data: null,
	redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC'
}
```

前端在 ajax 回调函数中直接使用 `location.href=res.redirect_uri` 跳转即可，无需再重复访问 `/oauth2/authorize` 接口。

</details>



### 1.2、根据授权码获取 Access-Token
获得 `Code` 码后，我们可以通过以下接口，获取到用户的 `Access-Token`、`Refresh-Token` 等信息。

``` url
http://{host}:{port}/oauth2/token
	?grant_type=authorization_code
	&client_id={client_id}
	&client_secret={client_secret}
	&code={code}
```

参数详解：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| grant_type	| 是		| 授权类型，这里请填写：`authorization_code`				|
| client_id		| 是		| 应用 id												|
| client_secret	| 是		| 应用秘钥												|
| code			| 是		| 步骤 1.1 中获取到的授权码								|

也可以通过 `Basic Authorization` 方式提交 `client` 信息，格式为在请求 `header` 头添加 `Authorization` 参数：
``` js
header['Authorization'] = base64(`${client_id}:${client_secret}`);
```

接口返回示例：

``` js
{
    "code": 200,    // 200表示请求成功，非200标识请求失败, 以下不再赘述 
    "msg": "ok",
    "data": null,
    "token_type": "Bearer",
    "access_token": "Gly7mnnXSdCxkOqmOwcA5SbG6ZtPmJVX7ZgSn1pidhRmnenBEgxbWJS8VWxA",     // Access-Token值
    "refresh_token": "EuYNwpxdc18MpaZLPyhFeyAyzr2IOWEr4q3QUGgPWqdJujQqvohjQEDJpwOm",    // Refresh-Token值
    "expires_in": 7199,                  // Access-Token剩余有效期，单位秒  
    "refresh_expires_in": 2591999,       // Refresh-Token剩余有效期，单位秒  
    "client_id": "1001",                 // 应用 id
    "scope": "userinfo"                  // 此令牌包含的权限
}
```


### 1.3、根据 Refresh-Token 刷新 Access-Token （如果需要的话）
Access-Token的有效期较短，如果每次过期都需要重新授权的话，会比较影响用户体验，因此我们可以在后台通过`Refresh-Token` 刷新 `Access-Token`

``` url
http://{host}:{port}/oauth2/refresh
	?grant_type=refresh_token
	&client_id={client_id}
	&client_secret={client_secret}
	&refresh_token={refresh_token}
```

参数详解：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| grant_type	| 是		| 授权类型，这里请填写：`refresh_token`				|
| client_id		| 是		| 应用 id												|
| client_secret	| 是		| 应用秘钥												|
| refresh_token | 是		| 步骤1.2中获取到的 `Refresh-Token` 值							|

接口返回值同章节1.2，此处不再赘述 


### 1.4、回收 Access-Token （如果需要的话）
在A ccess-Token 过期之前主动将其回收 

``` url
http://{host}:{port}/oauth2/revoke
	?client_id={client_id}
	&client_secret={client_secret}
	&access_token={access_token}
```

参数详解：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| client_id		| 是		| 应用 id												|
| client_secret	| 是		| 应用秘钥												|
| access_token  | 是		| 步骤1.2中获取到的`Access-Token`值						|

返回值样例：
``` js
{
    "code": 200,
    "msg": "ok",
    "data": null
}
```


### 1.5、根据 Access-Token 获取相应用户的账号信息  
注：此接口非 OAuth2 标准协议接口，为官方仓库 demo 模拟接口，正式项目中大家可以根据此样例，自定义需要的接口及参数 

``` url
http://{host}:{port}/oauth2/userinfo?access_token={access_token}
```

返回值样例：
``` js
{
    "code": 200,
    "msg": "ok",
	"nickname": "shengzhang_",         // 账号昵称
	"avatar": "http://xxx.com/1.jpg",  // 头像地址
	"age": "18",                       // 年龄
	"sex": "男",                       // 性别
	"address": "山东省 青岛市 城阳区"   // 所在城市 
}
```

除了直接在 url 中以 query 参数方式提交 `access_token`，你也可以在 `Authorization` 请求头以 `Bearer Token` 方式提交：
``` js
header['Authorization'] = 'Bearer access_token';
```


## 2、模式二：隐藏式（Implicit）

根据以下格式构建URL，引导用户访问：
``` url
http://{host}:{port}/oauth2/authorize
	?response_type=token
	&client_id={client_id}
	&redirect_uri={redirect_uri}
	&scope={scope}
	&state={state}
```

参数详解：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| response_type	| 是		| 返回类型，这里请填写：`token`							|
| client_id		| 是		| 应用 id												|
| redirect_uri	| 是		| 用户确认授权后，重定向的url地址							|
| scope			| 否		| 具体请求的权限，多个用逗号(或空格)隔开							|
| state			| 否		| 随机值，此参数会在重定向时追加到url末尾，不填不追加，如果填写则每次填写的值不可以重复			|

此模式会越过授权码的步骤，直接返回 `Access-Token` 到前端页面，形如：
``` url
redirect_uri#token=xxxx-xxxx-xxxx-xxxx
```
注意 token 是以 `#` 锚参数的形式拼接到 url 上的。


## 3、模式三：密码式（Password）
首先在Client端构建表单，让用户输入 Server 端的账号和密码，然后在 Client 端访问接口 
``` url
http://{host}:{port}/oauth2/token
	?grant_type=password
	&client_id={client_id}
	&client_secret={client_secret}
	&username={username}
	&password={password}
	&scope={scope}
```

参数详解：

| 参数			| 是否必填	| 说明							|
| :--------		| :--------	| :--------						|
| grant_type	| 是		| 返回类型，这里请填写：`password`|
| client_id		| 是		| 应用 id						|
| client_secret	| 是		| 应用秘钥												|
| username		| 是		| 用户的 `OAuth2-Server` 端账号			|
| password		| 是		| 用户的 `OAuth2-Server` 端密码			|
| scope			| 否		| 具体请求的权限，多个用逗号(或空格)隔开						|

接口返回示例：

``` js
{
    "code": 200,	// 200表示请求成功，非200标识请求失败, 以下不再赘述 
    "msg": "ok",
	"access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ",     // Access-Token 值
	"refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66",    // Refresh-Token 值
	"expires_in": 7199,                 // Access-Token 剩余有效期，单位秒  
	"refresh_expires_in": 2591999,      // Refresh-Token 剩余有效期，单位秒  
	"client_id": "1001",                // 应用 id
	"scope": "",                        // 此令牌包含的权限
}
```

::: warning 重写认证处理器
在正式项目中，password 认证模式需要重写 `PasswordGrantTypeHandler` 处理器，在后面的 [自定义 grant_type](/oauth2/oauth2-custom-grant_type) 章节我们会详细介绍
:::


## 4、模式四：凭证式（Client Credentials）
以上三种模式获取的都是用户的 `Access-Token`，代表用户对第三方应用的授权，
在OAuth2.0中还有一种针对 Client级别的授权， 即：`Client-Token`，代表应用自身的资源授权

在 Client 端的后台访问以下接口：

``` url
http://{host}:{port}/oauth2/client_token
	?grant_type=client_credentials
	&client_id={client_id}
	&client_secret={client_secret}
	&scope={scope}
```

参数详解：

| 参数			| 是否必填	| 说明									|
| :--------		| :--------	| :--------								|
| grant_type	| 是		| 返回类型，这里请填写：`client_credentials`|
| client_id		| 是		| 应用 id								|
| client_secret	| 是		| 应用秘钥								|
| scope			| 否		| 具体请求的权限，多个用逗号(或空格)隔开	|

接口返回值样例：
``` js
{
    "code": 200,
    "msg": "ok",
	"client_token": "HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO",	// Client-Token 值
	"expires_in": 7199,     // Token剩余有效时间，单位秒 
	"client_id": "1001",    // 应用 id
	"scope": null           // 包含权限 
}
```

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

---

### Sa Token Doc New Docs Oauth2 Oauth2 At Check (`sa-token-doc-new/docs/oauth2/oauth2-at-check.md`)

---
title: "Sa-Token 使用注解校验 Access-Token"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,使用注解校验 Access-Token,OAuth2.0,OAuth2"
description: "OAuth2 注解校验：注册拦截器。@SaCheckAccessToken 校验 access_token、scope；@SaCheckClientToken 校验 client_token；@SaCheckClientIdSecret 校验 client_id 与 secret。token 可放参数或 Bearer。"
---

# Sa-Token OAuth2 模块相关注解

sa-token-oauth2 模块扩展了三个注解用于相关数据校验：
- `@SaCheckAccessToken`：指定请求中必须包含有效的 `access_token` ，并且包含指定的 `scope`。
- `@SaCheckClientToken`：指定请求中必须包含有效的 `client_token` ，并且包含指定的 `scope`。
- `@SaCheckClientIdSecret`：指定请求中必须包含有效的 `client_id` 和 `client_secret` 信息。

和 Sa-Token-Code 模块的注解一样，你必须先注册框架的内置拦截器，才可以使用这些注解，详细参考：[注解鉴权](/use/at-check) 。

--- 


### 1、@SaCheckAccessToken 示例

``` java
@RestController
@RequestMapping("/test")
public class TestController {

    // 测试：携带有效的 access_token 才可以进入请求
    // 你可以在请求参数中携带 access_token 参数，或者从请求头以 Authorization: bearer xxx 的形式携带 
    @SaCheckAccessToken
    @RequestMapping("/checkAccessToken")
    public SaResult checkAccessToken() {
        return SaResult.ok("访问成功");
    }

    // 测试：携带有效的 access_token ，并且具备指定 scope 才可以进入请求
    @SaCheckAccessToken(scope = "userinfo")
    @RequestMapping("/checkAccessTokenScope")
    public SaResult checkAccessTokenScope() {
        return SaResult.ok("访问成功");
    }

    // 测试：携带有效的 access_token ，并且具备指定 scope 列表才可以进入请求
    @SaCheckAccessToken(scope = {"openid", "userinfo"})
    @RequestMapping("/checkAccessTokenScopeList")
    public SaResult checkAccessTokenScopeList() {
        return SaResult.ok("访问成功");
    }

}
```


### 2、@SaCheckClientToken 示例

``` java
@RestController
@RequestMapping("/test")
public class TestController {

    // 测试：携带有效的 client_token 才可以进入请求
    // 你可以在请求参数中携带 client_token 参数，或者从请求头以 Authorization: bearer xxx 的形式携带
    @SaCheckClientToken
    @RequestMapping("/checkClientToken")
    public SaResult checkClientToken() {
        return SaResult.ok("访问成功");
    }

    // 测试：携带有效的 client_token ，并且具备指定 scope 才可以进入请求
    @SaCheckClientToken(scope = "userinfo")
    @RequestMapping("/checkClientTokenScope")
    public SaResult checkClientTokenScope() {
        return SaResult.ok("访问成功");
    }

    // 测试：携带有效的 client_token ，并且具备指定 scope 列表才可以进入请求
    @SaCheckClientToken(scope = {"openid", "userinfo"})
    @RequestMapping("/checkClientTokenScopeList")
    public SaResult checkClientTokenScopeList() {
        return SaResult.ok("访问成功");
    }

}
```


### 3、@SaCheckClientIdSecret 示例
``` java
@RestController
@RequestMapping("/test")
public class TestController {

    // 测试：携带有效的 client_id 和 client_secret 信息，才可以进入请求
    // 你可以在请求参数中携带 client_id 和 client_secret 参数，或者从请求头以 Authorization: Basic base64(client_id:client_secret) 的形式携带
    @SaCheckClientIdSecret
    @RequestMapping("/checkClientIdSecret")
    public SaResult checkClientIdSecret() {
        return SaResult.ok("访问成功");
    }

}
```

---

### Sa Token Doc New Docs Oauth2 Oauth2 Check Domain (`sa-token-doc-new/docs/oauth2/oauth2-check-domain.md`)

---
title: "Sa-Token 配置 client 域名校验"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,配置 client 域名校验,OAuth2.0,OAuth2"
description: "OAuth2 Client 域名校验：AllowRedirectUris 是授权回调白名单，生产禁止配 *。否则攻击者可改 /oauth2/authorize 的 redirect_uri 把 code 劫持到自己的站。应写成具体回调地址，名单外 URL 无法下发授权码。通配 * 只允许出现在末尾。"
---

# OAuth2 整合-配置域名校验

--- 

### 1、code 劫持攻击
在前面章节的 OAuth-Server 搭建示例中：

``` java
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
	// 根据 clientId 获取 Client 信息
	@Override
	public SaClientModel getClientModel(String clientId) {
		if("1001".equals(clientId)) {
			return new SaClientModel()
					// ...
					.addAllowRedirectUris("*")    // 所有允许授权的 url
					// ...
		}
		return null;
	}
	// 其它代码 ... 
}
```

配置项 `AllowRedirectUris` 意为配置此 `Client` 端所有允许的授权地址，不在此配置项中的 URL 将无法下发 `code` 授权码。

为了方便测试，上述代码将其配置为`*`，但是，<font color="#FF0000" >在生产环境中，此配置项绝对不能配置为 * </font>，否则会有被 `code` 劫持的风险。

假设攻击者根据模仿我们的授权地址，巧妙的构造一个URL：

> [http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com](http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com)

当不知情的小红被诱导访问了这个 URL 时，它将被重定向至百度首页。

<img class="s-w-sh" src="/big-file/doc/oauth2-new/oauth2-ticket-jc.png" alt="OAuth2 非法 redirect 跳转示例" />

可以看到，代表着用户身份的 code 授权码也显现到了URL之中，借此漏洞，攻击者完全可以构建一个 URL 将小红的 code 授权码自动提交到攻击者自己的服务器，伪造小红身份登录网站。


### 2、防范方法

造成此漏洞的直接原因就是我们对此 client 配置了过于宽泛的 `AllowRedirectUris` 允许授权地址，防范的方法也很简单，就是缩小 `AllowRedirectUris` 授权范围。

我们将其配置为一个具体的URL：

``` java
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
	// 根据 clientId 获取 Client 信息
	@Override
	public SaClientModel getClientModel(String clientId) {
		if("1001".equals(clientId)) {
			return new SaClientModel()
					// ...
					.addAllowRedirectUris("http://sa-oauth-client.com:8002/")    // 所有允许授权的 url
					// ...
		}
		return null;
	}
	// 其它代码 ... 
}
```

再次访问上述链接：

<img class="s-w-sh" src="/big-file/doc/oauth2-new/oauth2-feifa-rf.png" alt="OAuth2 配置 redirect 白名单后拒绝非法跳转" />

URL 没有通过校验，拒绝授权！


### 3、配置安全性参考表

| 配置方式		| 举例											| 安全性								|  建议									|
| :--------		| :--------										| :--------							| :--------								|
| 配置为*		| `*`											| <font color="#F00" >低</font>		| **<font color="#F00" >禁止在生产环境下使用</font>**	|
| 配置到域名		| `http://sa-oauth-client.com:8002/*`			| <font color="#F70" >中</font>		| <font color="#F70" >不建议在生产环境下使用</font>	|
| 配置到详细地址	| `http://sa-oauth-client.com:8002/xxx/xxx`		| <font color="#080" >高</font>		| <font color="#080" >可以在生产环境下使用</font>	|


### 4、其它规则

1、AllowRedirectUris 配置的地址不允许出现 `@` 字符。

- 反例：`http://user@sa-token.com`
- 反例：`http://sa-oauth-client.com@sa-token.com`

*详见源码：[SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) 
`checkRedirectUri` 方法。*

2、AllowRedirectUris 配置的地址 `*` 通配符只允许出现在字符串末尾，不允许出现在字符串中间位置。

- 反例：`http*://sa-oauth-client.com/`
- 反例：`http://*.sa-oauth-client.com/`

*详见源码： [SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) 
`checkRedirectUriListNormalStaticMethod` 方法。*

参考：[github/issue/529](https://github.com/dromara/Sa-Token/issues/529)
感谢这位 `@m4ra7h0n` 用户反馈的漏洞。

---

### Sa Token Doc New Docs Oauth2 Oauth2 Custom Api (`sa-token-doc-new/docs/oauth2/oauth2-custom-api.md`)

---
title: "Sa-Token 自定义 API 路由"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,自定义 API 路由,OAuth2.0,OAuth2"
description: "OAuth2 自定义 API 路由：默认 /oauth2/* 由 Processor 聚合分发。可改 SaOAuth2Consts.Api 常量，或拆成 authorize、doLogin、doConfirm、token、refresh、revoke、client_token 独立映射，对齐业务 URL。"
---

# OAuth2-自定义 API 路由 

---

### 方式一：修改全局变量

在之前的章节中，我们演示了如何搭建一个 OAuth2 认证中心：
``` java
/**
 * Sa-Token-OAuth2 Server端 Controller 
 */
@RestController
public class SaOAuth2ServerController {

	// OAuth2-Server 端：处理所有 OAuth2 相关请求 
	@RequestMapping("/oauth2/*")
	public Object request() {
		return SaOAuth2ServerProcessor.instance.dister();
	}
	
	// ... 其它代码
	
}
```

这种写法集成简单但却不够灵活。例如获取 code 授权码地址只能是：`http://{host}:{port}/oauth2/authorize`，如果我们想要自定义其API地址，应该怎么做呢？

打开 OAuth2 模块相关源码，有关 API 的设计都定义在：
[SaOAuth2Consts.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/consts/SaOAuth2Consts.java)
中，我们可以对其进行二次修改。

例如，我们可以在 Main 方法启动类或者 OAuth2 配置方法中修改变量值：
``` java
// 配置 OAuth2 相关参数 
@Autowired
private void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
	// 自定义API地址
	SaOAuth2Consts.Api.authorize = "/oauth2/authorize2";
	// ... 
}
```

启动项目，统一认证地址就被我们修改成了：`http://{host}:{port}/oauth2/authorize2`


### 方式二：拆分路由入口
根据上述路由入口：`@RequestMapping("/oauth2/*")`，我们给它起一个合适的名字 —— 聚合式路由。

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

``` java
/**
 * Sa-Token-OAuth2 Server端 Controller 
 */
@RestController
public class SaOAuth2ServerController {

	// 模式一：Code授权码 || 模式二：隐藏式
	@RequestMapping("/oauth2/authorize")
	public Object authorize() {
		return SaOAuth2ServerProcessor.instance.authorize();
	}

	// 用户登录
	@RequestMapping("/oauth2/doLogin")
	public Object doLogin() {
		return SaOAuth2ServerProcessor.instance.doLogin();
	}

	// 用户确认授权
	@RequestMapping("/oauth2/doConfirm")
	public Object doConfirm() {
		return SaOAuth2ServerProcessor.instance.doConfirm();
	}

	// Code 换 Access-Token || 模式三：密码式
	@RequestMapping("/oauth2/token")
	public Object token() {
		return SaOAuth2ServerProcessor.instance.token();
	}

	// Refresh-Token 刷新 Access-Token
	@RequestMapping("/oauth2/refresh")
	public Object refresh() {
		return SaOAuth2ServerProcessor.instance.refresh();
	}

	// 回收 Access-Token
	@RequestMapping("/oauth2/revoke")
	public Object revoke() {
		return SaOAuth2ServerProcessor.instance.revoke();
	}

	// 模式四：凭证式
	@RequestMapping("/oauth2/client_token")
	public Object clientToken() {
		return SaOAuth2ServerProcessor.instance.clientToken();
	}

}
```

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

---

### Sa Token Doc New Docs Oauth2 Oauth2 Custom Grant Type (`sa-token-doc-new/docs/oauth2/oauth2-custom-grant_type.md`)

---
title: "Sa-Token 自定义 grant_type"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,自定义 grant_type,OAuth2.0,OAuth2"
description: "OAuth2 自定义 grant_type：/oauth2/token 内置 authorization_code 与 password。生产须重写 PasswordGrantTypeHandler。也可新增处理器，例如先调 /oauth2/sendPhoneCode 再凭验证码换 access_token。"
---

# OAuth2-自定义权限处理器 


### 1、需求场景

OAuth2 协议的 `/oauth2/token` 接口定义了两种获取 `access_token` 的 `grant_type`，分别是：
- `authorization_code`：使用用户授权的授权码获取 access_token。
- `password`：使用用户提交的账号、密码来获取 access_token。

你可以重写内置 `grant_type` 处理器，或添加自定义 `grant_type` 处理器，来支持更多的场景。



--- 


### 2、重写 password 认证模式处理器

当我们按照文档搭建的代码直接测试 password 认证模式时，控制台会得到警告：
``` txt
警告信息：当前 password 认证模式，使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle)，仅供开发测试
正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法
```

这是因为为方便测试，框架内部直接将 password 认证请求转发到了 `SaOAuth2Strategy.instance.doLoginHandle` 来处理，
在真正的项目中需要大家重写 password 认证模式处理器：

``` java
/**
 * 自定义 Password Grant_Type 授权模式处理器认证过程
 */
@Component
public class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler {

    @Override
    public PasswordAuthResult loginByUsernamePassword(String username, String password) {
        if("sa".equals(username) && "123456".equals(password)) {
            long userId = 10001;
            return new PasswordAuthResult(userId);
        } else {
            throw new SaOAuth2Exception("无效账号密码");
        }
    }

}
```


### 3、添加自定义 grant_type 处理器

假设有以下需求：通过 手机号+验证码 登录，返回 `access_token`。

#### 3.1、新增验证码发送接口

首先在 oauth2-server 端开放一个接口，为指定手机号发送验证码。

``` java
/**
 * 自定义手机登录接口
 */
@RestController
public class PhoneLoginController {

    @RequestMapping("/oauth2/sendPhoneCode")
    public SaResult sendCode(String phone) {
        String code = SaFoxUtil.getRandomNumber(100000, 999999) + "";
        SaManager.getSaTokenDao().set("phone_code:" + phone, code, 60 * 5);
        System.out.println("手机号：" + phone + "，验证码：" + code + "，已发送成功");
        return SaResult.ok("验证码发送成功");
    }

}
```

真实项目肯定是要对接短信服务商的，此处我们仅做模拟代码，将发送的验证码打印在控制台上。


#### 3.2、自定义 grant_type 处理器

在 oauth2-server 新建 `PhoneCodeGrantTypeHandler` 实现 `SaOAuth2GrantTypeHandlerInterface` 接口：

``` java
/**
 * 自定义 phone_code 授权模式处理器 
 */
@Component
public class PhoneCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {

    @Override
    public String getHandlerGrantType() {
        return "phone_code";
    }

    @Override
    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {

        // 获取前端提交的参数 
        String phone = req.getParamNotNull("phone");
        String code = req.getParamNotNull("code");
        String realCode = SaManager.getSaTokenDao().get("phone_code:" + phone);

        // 1、校验验证码是否正确
        if(!code.equals(realCode)) {
            throw new SaOAuth2Exception("验证码错误");
        }

        // 2、校验通过，删除验证码
        SaManager.getSaTokenDao().delete("phone_code:" + phone);

        // 3、登录
        long userId = 10001; // 模拟 userId，真实项目应该根据手机号从数据库查询

        // 4、构建 ra 对象
        RequestAuthModel ra = new RequestAuthModel();
        ra.clientId = clientId;
        ra.loginId = userId;
        ra.scopes = scopes;

        // 5、生成 Access-Token
        AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = "phone_code");
        return at;
    }
}
```

#### 3.3、为应用添加允许的授权类型

在 `SaOAuth2DataLoader` 实现类中，为 client 的允许授权类型增加自定义的 `phone_code` 

``` java
// Sa-Token OAuth2：自定义数据加载器 
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
	@Override
	public SaClientModel getClientModel(String clientId) {
		if("1001".equals(clientId)) {
			return new SaClientModel()
					.setClientId("1001")  
					.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")  
					.addAllowRedirectUris("*")    // 所有允许授权的 url
					.addContractScopes("openid", "userid", "userinfo")  
					.addAllowGrantTypes( 
							GrantType.authorization_code, 
							GrantType.implicit, 
							GrantType.refresh_token, 
							GrantType.password, 
							GrantType.client_credentials, 
							"phone_code"  // 重要代码：自定义授权模式 手机号验证码登录
					)
			;
		}
		return null;
	}
	// 其它代码 ... 
}
```

完工，开始测试。


### 4、测试步骤

#### 1、先发送验证码 

``` url
http://sa-oauth-server.com:8000/oauth2/sendPhoneCode?phone=13144556677
```

#### 2、请求 token  

注意 `grant_type` 要填写我们自定义的 `phone_code`，code 的具体值可以在后端的控制台上看到 

``` url
http://sa-oauth-server.com:8000/oauth2/token
    ?grant_type=phone_code
    &client_id=1001
    &client_secret=aaaa-bbbb-cccc-dddd-eeee
	&scope=openid
    &phone=13144556677
	&code={value}
```

返回结果参考如下：

``` js
{
    "code": 200,
    "msg": "ok",
    "data": null,
    "token_type": "Bearer",
    "access_token": "pfxRz6KVacwvKNu4IHmDsCJs33kvvARs2z1lTch7stog8nRt6rfVLowtAZ0E",
    "refresh_token": "qcFD6Wo2qZidofXQtWF5oK5ML6ljHKufQ5SbouBxzGnHhnMjUG4VV0iXZhdE",
    "expires_in": 7199,
    "refresh_expires_in": 2591999,
    "client_id": "1001",
    "scope": "openid",
    "openid": "ded91dc189a437dd1bac2274be167d50"
}
```

---

### Sa Token Doc New Docs Oauth2 Oauth2 Custom Login (`sa-token-doc-new/docs/oauth2/oauth2-custom-login.md`)

---
title: "Sa-Token 定制化登录页面与授权页面"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,定制化登录页面与授权页面,OAuth2.0,OAuth2"
description: "OAuth2 定制登录与授权页：重写 notLoginView 返回登录视图，ajax 调 /oauth2/doLogin（name、pwd，其它参数用 SaHolder 取）。confirmView 定制确认页并调 /oauth2/doConfirm。也可自写同名接口。推荐提交当前页 query。"
---

# OAuth2 定制化登录页面

---



### 1、如何自定义 OAuth-Server 端的登录视图？

重写 `SaOAuth2Strategy.instance.notLoginView` 策略：

``` java
@Autowired
public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
	// 配置：未登录时返回的View 
	SaOAuth2Strategy.instance.notLoginView = ()->{
		return new ModelAndView("xxx.html");
	};
}
```

在以上返回的视图中 ajax 方式调用 `/oauth2/doLogin` 接口，该接口接受以下参数：

| 参数			| 是否必填	| 说明									|
| :--------		| :--------	| :--------								|
| name			| 否		| 账号									|
| pwd			| 否		| 密码									|

接口返回值根据你重写的 `cfg.doLoginHandle` 策略进行自由决定。



### 2、如何自定义登录API的接口地址？
根据需求点选择解决方案：

#### 2.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数？
``` java
// 在任意代码处获取前端提交的参数 
String xxx = SaHolder.getRequest().getParam("xxx");
```

#### 2.2、想完全自定义一个接口来接受前端登录请求？
``` java
// 直接定义一个拦截路由为 `/oauth2/doLogin` 的接口即可 
@RequestMapping("/oauth2/doLogin")
public SaResult ss(String name, String pwd) {
	System.out.println("------ 请求进入了自定义的API接口 ---------- ");
	if("sa".equals(name) && "123456".equals(pwd)) {
		StpUtil.login(10001);
		return SaResult.ok("登录成功！");
	}
	return SaResult.error("登录失败！");
}
```

#### 2.3、不想使用`/oauth2/doLogin`这个接口，想自定义一个API地址？

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



### 3、如何自定义 OAuth-Server 端的确认授权视图？

重写 `SaOAuth2Strategy.instance.confirmView` 策略：

``` java
@Autowired
public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
	// 配置：授权确认视图 
	SaOAuth2Strategy.instance.confirmView = (clientId, scopes)->{
		Map<String, Object> map = new HashMap<>();
		map.put("clientId", clientId);
		map.put("scope", scopes);
		return new ModelAndView("confirm.html", map);
	};
}
```

在以上返回的视图中 ajax 方式调用 `/oauth2/doConfirm` 接口，即可完成授权，该接口接受以下参数：

| 参数			| 是否必填	| 说明									|
| :--------		| :--------	| :--------								|
| client_id		| 是		| 应用 id								|
| scope			| 是		| 具体授予的权限，多个用逗号(或空格)隔开	|
| response_type	| 是		| 取当前授权页 URL 上的 `response_type` 参数提交	|
| redirect_uri	| 是		| 取当前授权页 URL 上的 `redirect_uri` 参数提交	|
| state			| 否		| 取当前授权页 URL 上的 `state` 参数提交			|

推荐直接提交当前页面的 query 参数，例如：`fetch('/oauth2/doConfirm' + location.search, {method: 'POST'})`

接口返回值样例：
``` js
{
    "code": 200,
    "msg": "ok",
    "data": null,
}
```

---

### Sa Token Doc New Docs Oauth2 Oauth2 Custom Scope (`sa-token-doc-new/docs/oauth2/oauth2-custom-scope.md`)

---
title: "Sa-Token 自定义 Scope 权限及处理器"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,自定义 Scope 权限及处理器,OAuth2.0,OAuth2"
description: "OAuth2 自定义 Scope：access_token 不含昵称头像。可开放 /oauth2/userinfo 并校验 userinfo 权限，也可写 ScopeHandler 在发 token 时追加字段。申请 code 时带 scope=userinfo，再走 /oauth2/token 换令牌。"
---

# OAuth2-自定义 Scope 权限及处理器

--- 

### 1、需求场景
一般情况下，对于第三方 oauth2-client 来讲，仅仅拿到用户的 access_token 是不够的，还需要拿到更多的信息，比如用户昵称、头像等资料。

sa-token-oauth2 提供两种模式，让 access_token 可以得到更多信息。

- 自定义接口模式：在 oauth2-server 端开放一个资料查询接口，在 oauth2-client 得到 `access_token` 后，再次调用这个接口来获取 `userinfo` 信息。
- 自定义权限处理器模式：自定义一个 `ScopeHandler`，直接在返回 `access_token` 时追加字段，将 `userinfo` 信息和 `access_token` 一并返回到 oauth2-client。


### 2、自定义接口模式

#### 1、新建查询接口

在 oauth2-server 新建接口，查询指定 `access_token` 代表的 `userId` 其 `userinfo`：

``` java
// 获取 userinfo 信息：昵称、头像、性别等等
@RequestMapping("/oauth2/userinfo")
public SaResult userinfo() {
	// 获取 Access-Token 对应的账号id
	String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());
	Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
	System.out.println("-------- 此Access-Token对应的账号id: " + loginId);
	
	// 校验 Access-Token 是否具有权限: userinfo
	SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo");
	
	// 模拟账号信息 （真实环境需要查询数据库获取信息）
	Map<String, Object> map = new LinkedHashMap<>();
	// map.put("userId", loginId);  一般原则下，oauth2-server 不能把 userId 返回给 oauth2-client
	map.put("nickname", "林小林");
	map.put("avatar", "http://xxx.com/1.jpg");
	map.put("age", "18");
	map.put("sex", "男");
	map.put("address", "山东省 青岛市 城阳区");
	return SaResult.ok().setMap(map);
}
```


#### 2、申请 code 时指定权限
oauth2-client 申请 `code` 时，一定需要加上 `userinfo` 权限

``` url
http://sa-oauth-server.com:8000/oauth2/authorize
	?response_type=code
	&client_id=1001
	&redirect_uri=http://sa-oauth-client.com:8002/
	&scope=userinfo
```


#### 3、code 换 access_token
访问上述链接后，得到 `code` 授权码，然后我们拿着 `code` 换 `access_token`

``` url
http://sa-oauth-server.com:8000/oauth2/token
    ?grant_type=authorization_code
    &client_id=1001
    &client_secret=aaaa-bbbb-cccc-dddd-eeee
    &code=${code}
```

#### 4、access_token 取 userinfo 
使用返回的 `access_token` 再次访问接口 `/oauth2/userinfo`

``` url
http://sa-oauth-server.com:8000/oauth2/userinfo?access_token=${access_token}
```

返回以下结果：
``` js
{
  "code": 200,
  "msg": "ok",
  "data": null,
  "nickname": "林小林",
  "avatar": "http://xxx.com/1.jpg",
  "age": "18",
  "sex": "男",
  "address": "山东省 青岛市 城阳区"
}
```

拿到 userinfo。



### 3、自定义权限处理器模式

#### 1、新建权限处理器
在 oauth2-server 新建 `UserinfoScopeHandler.java` 实现 `SaOAuth2ScopeHandlerInterface` 接口：

``` java
/**
 *  自定义 userinfo scope 处理器 
 */
@Component
public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface {

    // 指示当前处理器所要处理的 scope 
    @Override
    public String getHandlerScope() {
        return "userinfo";
    }

    // 当构建的 AccessToken 具有此权限时，所需要执行的方法
    @Override
    public void workAccessToken(AccessTokenModel at) {
        System.out.println("--------- userinfo 权限，加工 AccessTokenModel --------- ");
        // 模拟账号信息 （真实环境需要查询数据库获取信息）
        Map<String, Object> map = new LinkedHashMap<String, Object>();
        map.put("userId", "10008");
        map.put("nickname", "shengzhang_");
        map.put("avatar", "http://xxx.com/1.jpg");
        map.put("age", "18");
        map.put("sex", "男");
        map.put("address", "山东省 青岛市 城阳区");
        at.extraData.putAll(map);
    }

    // 当构建的 ClientToken 具有此权限时，所需要执行的方法
    @Override
    public void workClientToken(ClientTokenModel ct) {
    }

    // 当使用 RefreshToken 刷新 AccessToken 时，是否重新执行 workAccessToken 构建方法
	// 在一些实时性较高的数据中需要指定为 true 
    @Override
    public boolean refreshAccessTokenIsWork() {
        return true;
    }
    
}
```

如上所述，所有写入到 `extraData` 中的数据，都将追加返回到 oauth2-client 端。


#### 2、申请 code 时指定权限
oauth2-client 申请 `code` 时，一定需要加上 `userinfo` 权限
``` url
http://sa-oauth-server.com:8000/oauth2/authorize
	?response_type=code
	&client_id=1001
	&redirect_uri=http://sa-oauth-client.com:8002/
	&scope=userinfo
```


#### 3、code 换 access_token
访问上述链接后，得到 `code` 授权码，然后我们拿着 `code` 换 `access_token`
``` url
http://sa-oauth-server.com:8000/oauth2/token
    ?grant_type=authorization_code
    &client_id=1001
    &client_secret=aaaa-bbbb-cccc-dddd-eeee
    &code=${code}
```

返回结果如下
``` js
{
  "code": 200,
  "msg": "ok",
  "data": null,
  "token_type": "Bearer",
  "access_token": "LQ24xI0hX25vIzvciHPA0PNsnGCweSFM1Bzl8783li07VAXpw8sEfn9xsta2",
  "refresh_token": "rKB8mby1Mw8yZXHbWzliHx6lmatcLcULLw5C5cUMBhMMRx72DFg5u0owZgrA",
  "expires_in": 7199,
  "refresh_expires_in": 2591999,
  "client_id": "1001",
  "scope": "openid,userid,userinfo",
  "userinfo": {
    "userId": "10008",
    "nickname": "shengzhang_",
    "avatar": "http://xxx.com/1.jpg",
    "age": "18",
    "sex": "男",
    "address": "山东省 青岛市 城阳区"
  }
}
```

拿到 userinfo。

#### 总结
相比于自定义接口模式，自定义权限处理器模式可以少一次网络请求，让 oauth2-client 端提前拿到 `userinfo` 信息。



### 4、最终权限处理器
当一个自定义权限处理器，监听的 scope 字符串为 `_FINALLY_WORK_SCOPE` 时，则代表这个权限处理器为“最终权限处理器”。

最终权限处理器会永远在所有权限处理器工作完成之后执行一次，即使 oauth2-client 端没有申请任何 scope，最终权限处理器也会固定执行。

示例：
``` java
/**
 * 最终权限处理器：在所有权限处理器工作完成之后，执行此权限处理器
 */
@Component
public class FinallyWorkScopeHandler implements SaOAuth2ScopeHandlerInterface {

    @Override
    public String getHandlerScope() {
        return SaOAuth2Consts._FINALLY_WORK_SCOPE;
    }

    @Override
    public void workAccessToken(AccessTokenModel at) {
        // 在所有权限处理器工作完成之后，执行此处方法加工 AccessToken
        // System.out.println(123);
    }

    @Override
    public void workClientToken(ClientTokenModel ct) {
        // System.out.println(456);
    }
}
```

---

### Sa Token Doc New Docs Oauth2 Oauth2 Data Loader (`sa-token-doc-new/docs/oauth2/oauth2-data-loader.md`)

---
title: "Sa-Token 自定义数据加载器"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,自定义数据加载器,OAuth2.0,OAuth2"
description: "OAuth2 自定义数据加载器：测试可用 addClient 或 yml 配 clients。生产实现 SaOAuth2DataLoader，按 clientId 从库加载 secret、AllowRedirectUris、签约 scope 与 grant_type，并自定义 getOpenid 生成。"
---

# OAuth2-自定义数据加载器



### 1、基于内存的数据加载

在之前搭建 OAuth2-Server 示例中，我们演示了 client 信息配置方案：
``` java
// Sa-Token OAuth2 定制化配置
@Autowired
public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
	
	// 添加 client 
	oauth2Server.addClient(
		new SaClientModel()
			.setClientId("1001")    // client id
			.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")    // client 秘钥
			.addAllowRedirectUris("*")    // 所有允许授权的 url
			.addContractScopes("openid", "userid", "userinfo")    // 所有签约的权限
			.addAllowGrantTypes(	 // 所有允许的授权模式
					GrantType.authorization_code, // 授权码式
					GrantType.implicit,  // 隐式式
					GrantType.refresh_token,  // 刷新令牌
					GrantType.password,  // 密码式
					GrantType.client_credentials,  // 客户端模式
			)
	)
	
	// 可以添加更多 client 信息，只要保持 clientId 唯一就行了
	// oauth2Server.addClient(...)

}
```

你也可以在 `application.yml` 配置中 `client` 信息：

:::tabs
== yaml 风格

``` yaml
# sa-token配置
sa-token: 
    # OAuth2.0 配置 
    oauth2-server:
		# client 列表 
        clients:
            # 客户端1
            1001:
                # 客户端id
                client-id: 1001
                # 客户端秘钥
                client-secret: aaaa-bbbb-cccc-dddd-eeee
                # 所有允许授权的 url
                allow-redirect-uris:
                  - http://sa-oauth-client.com:8002
                  - http://sa-oauth-client.com:8002/*
                # 所有签约的权限
                contract-scopes:
                  - openid
                  - userid
                  - userinfo
                # 所有允许的授权模式
                allow-grant-types:
                  - authorization_code
                  - implicit
                  - refresh_token
                  - password
                  - client_credentials
            # 客户端2
            1002:
                # 客户端id
                client-id: 1002
				# 更多配置 ...
```

== properties 风格

``` properties
########### 客户端1
# 客户端id
sa-token.oauth2-server.clients.1001.client-id=1001
# 客户端秘钥
sa-token.oauth2-server.clients.1001.client-secret=aaaa-bbbb-cccc-dddd-eeee
# 所有允许授权的 url
sa-token.oauth2-server.clients.1001.allow-redirect-uris[0]=http://sa-oauth-client.com:8002
sa-token.oauth2-server.clients.1001.allow-redirect-uris[1]=http://sa-oauth-client.com:8002/*
# 所有签约的权限
sa-token.oauth2-server.clients.1001.contract-scopes[0]=openid
sa-token.oauth2-server.clients.1001.contract-scopes[1]=userid
sa-token.oauth2-server.clients.1001.contract-scopes[2]=userinfo
# 所有允许的授权模式
sa-token.oauth2-server.clients.1001.allow-grant-types[0]=authorization_code
sa-token.oauth2-server.clients.1001.allow-grant-types[1]=implicit
sa-token.oauth2-server.clients.1001.allow-grant-types[2]=refresh_token
sa-token.oauth2-server.clients.1001.allow-grant-types[3]=password
sa-token.oauth2-server.clients.1001.allow-grant-types[4]=client_credentials

########### 客户端2
sa-token.oauth2-server.clients.1002.client-id=1002
sa-token.oauth2-server.clients.1002.client-secret=...
```

:::



这两种方案都是基于内存形式的 client 信息配置，只适合简单的测试，一般真实项目的 client 信息都是保存在数据库中的，下面演示一下如何在数据库中动态获取 client 信息


### 2、基于数据库的数据加载

你只需要自定义数据加载器：新建 `SaOAuth2DataLoaderImpl` 实现 `SaOAuth2DataLoader` 接口。


``` java
/**
 * Sa-Token OAuth2：自定义数据加载器
 */
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
	
	// 根据 clientId 获取 Client 信息
	@Override
	public SaClientModel getClientModel(String clientId) {
		// 此为模拟数据，真实环境需要从数据库查询 
		if("1001".equals(clientId)) {
			return new SaClientModel()
					.setClientId("1001")    // client id
					.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")    // client 秘钥
					.addAllowRedirectUris("*")    // 所有允许授权的 url
					.addContractScopes("openid", "userid", "userinfo")    // 所有签约的权限
					.addAllowGrantTypes(	 // 所有允许的授权模式
							GrantType.authorization_code, // 授权码式
							GrantType.implicit,  // 隐式式
							GrantType.refresh_token,  // 刷新令牌
							GrantType.password,  // 密码式
							GrantType.client_credentials  // 客户端模式
					)
			;
		}
		return null;
	}
	
	// 根据 clientId 和 loginId 获取 openid
	@Override
	public String getOpenid(String clientId, Object loginId) {
		// 此处使用框架默认算法生成 openid，真实环境建议改为从数据库查询
		return SaOAuth2DataLoader.super.getOpenid(clientId, loginId);
	}

}
``` 

此种形式更加灵活，后续文档将默认按照此种形式来展示示例。

---

### Sa Token Doc New Docs Oauth2 Oauth2 Dev (`sa-token-doc-new/docs/oauth2/oauth2-dev.md`)

---
title: "Sa-Token OAuth2 代码 API 参考"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,OAuth2 代码 API 参考,OAuth2.0,OAuth2"
description: "OAuth2 代码 API：开放更多资源时用 SaOAuth2Util。可校验 clientSecret、签约 scope、redirect_uri，以及 checkCode、checkAccessToken、getLoginIdByAccessToken，并回收 Refresh-Token、Client-Token。"
---

# Sa-Token-OAuth2 Server端 二次开发用到的所有函数说明 

官方示例只提供了基本的授权流程，以及 userinfo 资源的开放，如果您需要开放更多的接口，则二次开发时可能用到以下相关 API 方法 

--- 

### Client 信息相关

``` java
// 获取 ClientModel，根据 clientId
SaOAuth2Util.getClientModel(clientId);

// 校验 clientId 信息并返回 ClientModel，如果找不到对应 Client 信息则抛出异常
SaOAuth2Util.checkClientModel(clientId);

// 校验：clientId 与 clientSecret 是否正确
SaOAuth2Util.checkClientSecret(clientId, clientSecret);

// 校验：clientId 与 clientSecret 是否正确，并且是否签约了指定 scopes
SaOAuth2Util.checkClientSecretAndScope(clientId, clientSecret, scopes);

// 判断：该 Client 是否签约了指定的 Scope，返回 true 或 false
SaOAuth2Util.isContractScope(clientId, scopes);

// 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常
SaOAuth2Util.checkContractScope(clientId, scopes);

// 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常
SaOAuth2Util.checkContractScope(clientModel, scopes);

// 校验：该 Client 使用指定 url 作为回调地址，是否合法
SaOAuth2Util.checkRedirectUri(clientId, url);

// 判断：指定 loginId 是否对一个 Client 授权给了指定 Scope
SaOAuth2Util.isGrantScope(loginId, clientId, scopes);

// 删除：指定 loginId 针对指定 Client 的授权信息
SaOAuth2Util.deleteGrantScope(loginId, clientId);
```


### Code 相关
``` java
// 获取 CodeModel，无效的 code 会返回 null
SaOAuth2Util.getCode(code);

// 校验 Code，成功返回 CodeModel，失败则抛出异常
SaOAuth2Util.checkCode(code);

// 获取 Code，根据索引： clientId、loginId
SaOAuth2Util.getCodeValue(clientId, loginId);
```


### Access-Token 相关
``` java
// 获取 AccessTokenModel，无效的 AccessToken 会返回 null
SaOAuth2Util.getAccessToken(accessToken);

// 校验 Access-Token，成功返回 AccessTokenModel，失败则抛出异常
SaOAuth2Util.checkAccessToken(accessToken);

// 获取 Access-Token 列表：此应用下 对 某个用户 签发的所有 Access-token
SaOAuth2Util.getAccessTokenValueList(clientId, loginId);

// 判断：指定 Access-Token 是否具有指定 Scope 列表，返回 true 或 false
SaOAuth2Util.hasAccessTokenScope(accessToken, ...scopes);

// 校验：指定 Access-Token 是否具有指定 Scope 列表，如果不具备则抛出异常
SaOAuth2Util.checkAccessTokenScope(accessToken, ...scopes);

// 获取 Access-Token 所代表的LoginId
SaOAuth2Util.getLoginIdByAccessToken(accessToken);

// 获取 Access-Token 所代表的 clientId
SaOAuth2Util.getClientIdByAccessToken(accessToken);

// 回收一个 Access-Token
SaOAuth2Util.revokeAccessToken(accessToken);

// 回收全部 Access-Token：指定应用下 指定用户 的全部 Access-Token
SaOAuth2Util.revokeAccessTokenByIndex(clientId, loginId);
```


### Refresh-Token 相关
``` java
// 获取 RefreshTokenModel，无效的 RefreshToken 会返回 null
SaOAuth2Util.getRefreshToken(refreshToken);

// 校验 Refresh-Token，成功返回 RefreshTokenModel，失败则抛出异常
SaOAuth2Util.checkRefreshToken(refreshToken);

// 获取 Refresh-Token 列表：此应用下 对 某个用户 签发的所有 Refresh-Token
SaOAuth2Util.getRefreshTokenValueList(clientId, loginId);

// 回收一个 Refresh-Token
SaOAuth2Util.revokeRefreshToken(refreshToken);

// 回收全部 Refresh-Token：指定应用下 指定用户 的全部 Refresh-Token
SaOAuth2Util.revokeRefreshTokenByIndex(clientId, loginId);

// 根据 RefreshToken 刷新出一个 AccessToken
SaOAuth2Util.refreshAccessToken(refreshToken);
```


### Client-Token 相关

``` java
// 获取 ClientTokenModel，无效的 ClientToken 会返回 null
SaOAuth2Util.getClientToken(clientToken);

// 校验 Client-Token，成功返回 ClientTokenModel，失败则抛出异常
SaOAuth2Util.checkClientToken(clientToken);

// 获取 Client-Token 列表：此应用下 对 某个用户 签发的所有 Client-token
SaOAuth2Util.getClientTokenValueList(clientId);

// 判断：指定 Client-Token 是否具有指定 Scope 列表，返回 true 或 false
SaOAuth2Util.hasClientTokenScope(clientToken, ...scopes);

// 校验：指定 Client-Token 是否具有指定 Scope 列表，如果不具备则抛出异常
SaOAuth2Util.checkClientTokenScope(clientToken, ...scopes);

// 回收一个 ClientToken
SaOAuth2Util.revokeClientToken(clientToken);

// 回收全部 Client-Token：指定应用下的全部 Client-Token
SaOAuth2Util.revokeClientTokenByIndex(clientId);
```


### 请求查询

``` java
// 数据读取：从当前请求对象中读取 access_token，并查询到 AccessTokenModel 信息，无效 access_token 抛出异常
// 1、请求参数 access_token，2、请求头 Authorization Bearer access_token
SaOAuth2Util.currentAccessToken();

// 数据读取：从当前请求对象中读取 client_token，并查询到 ClientTokenModel 信息，无效 client_token 抛出异常
// 1、请求参数 client_token，2、请求头 Authorization Bearer client_token
SaOAuth2Util.currentClientToken();
```



详情请参考源码：[码云：SaOAuth2Util.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Util.java)


### OAuth2-Server 所有可重写策略


#### 权限处理器
``` java
// 根据 scope 信息对一个 AccessTokenModel 进行加工处理
SaOAuth2Strategy.instance.workAccessTokenByScope = at -> {
	// ... 
}

// 当使用 RefreshToken 刷新 AccessToken 时，根据 scope 信息对一个 AccessTokenModel 进行加工处理
SaOAuth2Strategy.instance.refreshAccessTokenWorkByScope = at -> {
	// ... 
}

// 根据 scope 信息对一个 ClientTokenModel 进行加工处理
SaOAuth2Strategy.instance.workClientTokenByScope = at -> {
	// ... 
}
```


#### grant_type 处理器
``` java
// 根据 grantType 构造一个 AccessTokenModel
SaOAuth2Strategy.instance.grantTypeAuth = req -> {
	// ... 
}
```


#### 凭证创建
``` java
// 创建一个 code value
SaOAuth2Strategy.instance.createCodeValue = (clientId, loginId, scopes) -> {
	// ... 
}

// 创建一个 AccessToken value
SaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> {
	// ... 
}

// 创建一个 RefreshToken value
SaOAuth2Strategy.instance.createRefreshToken = (clientId, loginId, scopes) -> {
	// ... 
}

// 创建一个 ClientToken value
SaOAuth2Strategy.instance.createClientToken = (clientId, scopes) -> {
	// ... 
}
```


#### 认证流程回调
``` java
// OAuth-Server端：未登录时返回的View
SaOAuth2Strategy.instance.notLoginView = () -> {
	// ... 
}

// OAuth-Server端：确认授权时返回的View
SaOAuth2Strategy.instance.confirmView = (clientId, scopes) -> {
	// ... 
}

// OAuth-Server端：登录函数
SaOAuth2Strategy.instance.doLoginHandle = (name, pwd) -> {
	// ... 
}

// OAuth-Server端：用户在授权指定 client 前的检查，如果检查不通过，请直接抛出异常
SaOAuth2Strategy.instance.userAuthorizeClientCheck = (loginId, clientId) -> {
	// ... 
}
```


#### 其它
``` java
// 在创建 SaClientModel 时，设置其默认字段
SaOAuth2Strategy.instance.setSaClientModelDefaultFields = (clientModel) -> {
	// ... 
}
```

---

### Sa Token Doc New Docs Oauth2 Oauth2 H5 (`sa-token-doc-new/docs/oauth2/oauth2-h5.md`)

---
title: "Sa-Token OAuth2-Server端前后台分离"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,OAuth2-Server端前后台分离,OAuth2.0,OAuth2"
description: "OAuth2-Server 前后端分离：把 /oauth2/authorize 登录判断、scope 确认与 redirect 搬到 H5。后端 /oauth2/getRedirectUri 按 401 未登录、411 待确认、200 返回 redirect_uri；token、refresh 仍走后端。"
---

# OAuth2-Server 端前后台分离

### 1、设计分析

要使 OAuth2-Server 端做到前后台分离，则需要对接口进行一部分改造：

- 改造前的接口列表：
	- `http:{后端主机}/oauth2/authorize`
	- `http:{后端主机}/oauth2/token`
	- `http:{后端主机}/oauth2/refresh`
	- 更多...
- 改造后的接口列表：
	- `http:{前端主机}/oauth2/authorize`
	- `http:{后端主机}/oauth2/token`
	- `http:{后端主机}/oauth2/refresh`
	- 更多...

也就是，只需要重点改造 `/oauth2/authorize` 一个接口即可，`/oauth2/authorize` 接口主要做了三件事：
1. 判断用户在 oauth2-server 端是否登录，未登录会进入 [登录页面]，已登录则进入下一步。
2. 判断应用请求的 scope 是否需要用户手动确认授权，需要会进入 [确认授权页面]，不需要则进入下一步。
3. 重定向至 `redirect_uri` 指定的 url 地址，并携带 code 授权码参数。

我们只需要把上述逻辑从 oauth2-server 的后端搬到 oauth2-server 的前端即可。


### 2、OAuth2-Server 后端添加接口

首先在 `oauth2-server` 的后端添加一个接口，用于获取最终授权重定向地址：

```
/* Detailed source-code truncated for AI context efficiency. */
```

### 3、新建前端项目

既然是前后台分离，那肯定要有一个独立的前端项目，所需代码比较冗长，不便于在文档处直接展示，大家可以参考仓库示例`/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5`（完整压缩包见 [Sa-Token 集成示例大全下载](/more/download-demos) ）：




### 4、运行测试

在前端 ide 中导入 demo 案例的 `sa-token-demo-oauth2-server-h5` 项目，然后直接预览 `oauth2-authorize.html` 页面，如图所示：

<img class="s-w-sh" src="/big-file/doc/oauth2-new/sa-oauth2-server-authorize-h5.png" alt="OAuth2-Server 前后端分离授权页预览" />

复制上述地址，然后将其配置到 “OAuth2前端测试页” 的 “OAuth2 Server 授权页地址” 选项中，其它选项保持默认不变：

<img class="s-w-sh" src="/big-file/doc/oauth2-new/sa-oauth2-client-test-h5-page-setting.png" alt="OAuth2 前端测试页 Server 授权地址配置" />

然后根据 “OAuth2前端测试页” 的页面提示进行测试即可，此处不再赘述。

---

### Sa Token Doc New Docs Oauth2 Oauth2 Interworking (`sa-token-doc-new/docs/oauth2/oauth2-interworking.md`)

---
title: "Sa-Token OAuth2-与登录会话实现数据互通"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,OAuth2-与登录会话实现数据互通,OAuth2.0,OAuth2"
description: "OAuth2 与登录会话互通：access_token 与 satoken 默认互不认。Server 可重写 createAccessToken 返回会话令牌；Client 用 SaLoginParameter.setToken 预定。互通后过期按 timeout 与 access-token-timeout 各算。"
---

# OAuth2 与登录会话实现数据互通

--- 

### 前提

前提，我们：
- 把 OAuth2 模块生成的令牌称作资源令牌（access_token），
- 把 StpUtil 登录会话生成的令牌称作会话令牌（satoken）。

正常情况下，资源令牌 与 会话令牌 的数据是不互通的，具体表现就是：当我们拿着 access_token 去访问 satoken 令牌的接口，会被抛出异常：`无效Token：xxxxx`

那么，有什么办法可以做到这两个模块的数据互通呢？



### OAuth2-Server 端数据互通

很简单，你只需要在 `configOAuth2Server` 中重写 Access-Token 的生成策略：

``` java
// Sa-Token OAuth2 定制化配置 
@Autowired
public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
	// 其它配置 ...
	
	// 重写 AccessToken 创建策略，返回会话令牌
	SaOAuth2Strategy.instance.createAccessToken = (clientId, loginId, scopes) -> {
		System.out.println("----返回会话令牌");
		return StpUtil.getOrCreateLoginSession(loginId);
	};

}
```

重启项目，然后在 OAuth2 模块授权登录，现在生成的 `access_token` ，可以用来访问 `satoken` 的会话接口了。


::: warning 注意点
数据互通，让前端与后端的交互更加方便，一个 token 即可访问所有接口，但也一定程度上失去了OAuth2的 “不同 Client 不同权限” 的设计意义，
同时也默认每个 Client 都拥有了账号的会话权限（access_token 与 satoken 为同一个）。

应该根据自己的架构合理分析是否应该整合数据互通。
:::



### OAuth2-Client 数据互通
除了Server端，Client端也可以打通 `access_token` 与 `satoken` 会话。做法是在 Client 端拿到 `access_token` 后进行登录时，使用 `SaLoginParameter` 预定登录生成的 Token 值 

``` java
// 1. 获取到access_token
String access_token = ...

// 2. 登录时预定生成的token 
StpUtil.login(uid, new SaLoginParameter().setToken(access_token));

// 3. 其它代码...
```



**疑问：数据互通后，两个 token 的过期策略是什么？**

会话 token 由 `sa-token.timeout` 决定，`access_token` 由 `sa-token.oauth2-server.access-token-timeout` 决定。

数据互通只是将 token 拷贝一份进行复用，动作完成之后两者不再有任何联系。

---

### Sa Token Doc New Docs Oauth2 Oauth2 Oidc (`sa-token-doc-new/docs/oauth2/oauth2-oidc.md`)

---
title: "Sa-Token 开启 OIDC 协议"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,开启 OIDC 协议,OAuth2.0,OAuth2"
description: "Sa-Token 开启 OIDC：引入 sa-token-jwt，给 client 签约 oidc 并配 jwt-secret-key。申请 scope=oidc 后 token 接口返回 id_token，载荷含 iss、sub、aud。可继承 OidcScopeHandler 追加 nickname、email。"
---

# OAuth2 开启 OIDC 协议 （OpenID Connect）

--- 

### 1、开启步骤

1、引入 `sa-token-jwt` 依赖，用来签发 `id_token` 

:::tabs
== Maven 方式

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-jwt</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// sa-token-jwt 签发 OIDC id_token 令牌 
implementation 'cn.dev33:sa-token-jwt:${sa.top.version}'
```

:::



2、在 `SaOAuth2DataLoader` 实现类中，返回的 `SaClientModel` 中添加 `oidc` 的签约权限。

``` java
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
	@Override
	public SaClientModel getClientModel(String clientId) {
		// 此为模拟数据，真实环境需要从数据库查询 
		if("1001".equals(clientId)) {
			return new SaClientModel()
					// .... 
					.addContractScopes("openid", "userid", "userinfo", "oidc")    // 此处添加上签约权限：oidc 
					.addAllowGrantTypes(	
							// ... 
					)
			;
		}
		return null;
	}
	// 其它代码 ... 
}
```

3、在 `application.yml` 配置文件中配置 jwt 生成秘钥：

:::tabs
== yaml 风格

``` yaml
sa-token:
	# jwt秘钥 
	jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
```

== properties 风格

``` properties
# jwt秘钥 
sa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
```

:::


注：为了安全起见请不要直接复制官网示例这个字符串（随便按几个字符就好了）



### 2、测试

1、在 OAuth2-Client 端申请授权码时，添加上 `oidc` 权限：
``` url
http://sa-oauth-server.com:8000/oauth2/authorize
	?response_type=code
	&client_id=1001
	&redirect_uri=http://sa-oauth-client.com:8002/
	&scope=oidc
```

2、得到授权码后，然后拿着 `code` 换 `access_token`
``` url
http://sa-oauth-server.com:8000/oauth2/token
    ?grant_type=authorization_code
    &client_id=1001
    &client_secret=aaaa-bbbb-cccc-dddd-eeee
    &code=${code}
```

3、返回的结果中将包含 `id_token` 字段：
``` js
{
	"code": 200,
	"msg": "ok",
	"data": null,
	"token_type": "Bearer",
	"access_token": "WdpjZdGlXdOzsAcr7gqPwmLVInHrhpznQa2pDOVqZmLXQynBflkcWqE6f5o2",
	"refresh_token": "hKHwBm3eH6iqSHlXRGWQaziV8OoyHvzmUb97lKEEZnZJLt3NunBFx7rVZWbT",
	"expires_in": 7199,
	"refresh_expires_in": 2591999,
	"client_id": "1001",
	"scope": "oidc",
	"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzI0NDI1OTg5LCJpYXQiOjE3MjQ0MjUzODksImF1dGhfdGltZSI6MTcyNDQwMDUyNiwibm9uY2UiOiJLTHlNR08zZ1R0YVdhMEFRcHF0RUNpTk9SWkY1QkhvRCIsImF6cCI6IjEwMDEifQ.gP3UYMexaQ9v0huKUuqhV9-dPxPpaEuFPIlPb2UZaOI"
}
```

4、解析 `id_token` 将得到以下载荷
``` js
{
  "iss": "http://sa-oauth-server.com:8000",  // 签发人
  "sub": "10001",   // userId 
  "aud": "1001",   // clientId
  "exp": 1724425989,   // 令牌到期时间，10位时间戳 
  "iat": 1724425389,   // 签发此令牌的时间，10位时间戳 
  "auth_time": 1724400526,   // 用户认证时间，10位时间戳 
  "nonce": "KLyMGO3gTtaWa0AQpqtECiNORZF5BHoD",  // 随机数，防止重放攻击 
  "azp": "1001"   // clientId
}
```

如果默认携带的载荷无法满足你的业务需求，你还可以自定义追加扩展字段，让 `id_token` 返回更多信息 


### 3、扩展 id_token 载荷

新建 `CustomOidcScopeHandler` 继承 `OidcScopeHandler`，扩展 OIDC 权限处理器，返回更多字段：
``` java
/**
 * 扩展 OIDC 权限处理器，返回更多字段
 */
@Component
public class CustomOidcScopeHandler extends OidcScopeHandler {

    @Override
    public IdTokenModel workExtraData(IdTokenModel idToken) {
        Object userId = idToken.sub;
        System.out.println("----- 为 idToken 追加扩展字段 ----- ");

        idToken.extraData.put("uid", userId); // 用户id
        idToken.extraData.put("nickname", "lin_xiao_lin"); // 昵称
        idToken.extraData.put("picture", "https://sa-token.com/logo.png"); // 头像
        idToken.extraData.put("email", "456456@xx.com"); // 邮箱
        idToken.extraData.put("phone_number", "13144556677"); // 手机号
        // 更多字段 ...
        // 可参考：https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims

        return idToken;
    }

}
```

重启项目，再次请求授权，返回的 `id_token` 载荷将包含更多字段：

``` js
{
  "iss": "http://sa-oauth-server.com:8000",
  "sub": "10001",
  "aud": "1001",
  "exp": 1724430149,
  "iat": 1724429549,
  "auth_time": 1724400526,
  "nonce": "SBRLOcfeo9FFmLTB8OINvuulam5FMOre",
  "azp": "1001",
  "uid": "10001",
  "nickname": "lin_xiao_lin",
  "picture": "https://sa-token.com/logo.png",
  "email": "456456@xx.com",
  "phone_number": "13144556677"
}
```

---

### Sa Token Doc New Docs Oauth2 Oauth2 Openid (`sa-token-doc-new/docs/oauth2/oauth2-openid.md`)

---
title: "Sa-Token OpenId 与 UnionId"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,OpenId 与 UnionId,OAuth2.0,OAuth2"
description: "OAuth2 OpenId 与 UnionId：openid 是用户在单个 client 下的标识，默认 md5(prefix_clientId_loginId)；unionid 按 subjectId 分组，同主体多应用一致。可改 digest-prefix 或 DataLoader 自定义，生产建议入库映射。"
---

# OpenId 与 UnionId

<p><a class="case-btn case-btn-video" href="https://www.bilibili.com/video/BV1oz6AY5ERJ/" target="_blank">
	视频讲解：OAuth2 授权流程中的 clientId、openId、unionId、userId 都是干嘛的？
</a></p>


### 1、OpenId 

openid 是用户在某一 client 下的唯一标识，其有如下特点：

- 一个用户在同一个 client 下，openid 是固定的，每次请求都会返回相同的值。
- 一个用户在不同的 client 下，openid 是不同的，会返回不同的值。

oauth2-client 在每次授权时可根据返回的 openid 值来确定用户身份。

框架默认的 openid 生成算法为：
``` java
md5(prefix + "_" + clientId + "_" + loginId);
```

其中的 prefix 前缀默认值为：`openid_default_digest_prefix`，你可以通过以下方式配置：

:::tabs
== yaml 风格

``` yaml
# sa-token配置
sa-token:
	oauth2-server:
		# 默认 openid 生成算法中使用的摘要前缀
		openid-digest-prefix: xxxxxx
```

== properties 风格

``` properties
# 默认 openid 生成算法中使用的摘要前缀
sa-token.oauth2-server.openid-digest-prefix=xxxxxx
```

:::



你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 OpenId 生成算法：

``` java
/**
 * Sa-Token OAuth2：自定义数据加载器
 */
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
	
	// 自定义 openid 生成算法 
	@Override
	public String getOpenid(String clientId, Object loginId) {
		// 此种写法代表使用框架默认算法生成 openid，真实环境建议改为从数据库查询
		return SaOAuth2DataLoader.super.getOpenid(clientId, loginId);
	}

}
``` 


#### openid 算法要求


正常来讲，openid 算法需要保证：

1. 单个 clientId 下同一 loginId 生成的 `openid` 一致。[必须]
2. 多个 clientId 下同一 loginId 生成的 `openid` 不一致。[非常建议]
3. 客户端无法通过 clientId + loginId 推测 `openid` 值。[建议]
4. 客户端无法通过 clientId + loginId + openid 推测该 loginId 在其它 clientId 下的 `openid` 值。[建议]
5. oauth2-server 自身由 `openid` 可以反查出对应的 clientId 和 loginId。[根据业务需求而定是否满足]

框架内置的算法，可以满足 1和2，如果自定义了 `sa-token.oauth2-server.openid-digest-prefix` 配置，可以满足3。

如果自定义配置的 prefix 长度较短，或比较简单呈现规律性，则有客户端根据 clientId + loginId + openid 穷举爆破出 `prefix` 的风险，
从而获得提前计算彩虹表来推测出其它 clientId、loginId 对应 openid 值的能力。

如果自定义的 prefix 前缀比较复杂，让客户端无法爆破，则可以满足4。但依然无法满足5。

所以 openid 算法的最优解，应该是 oauth2-server 采用随机字符串作为 openid，然后自建数据库表来维护其映射关系，这样可以同时满足12345。

表结构参考如下：

- id：数据id，主键。
- client_id：应用id。
- user_id：用户账号id。
- openid：对应的 openid 值，随机字符串。
- create_time：数据创建时间。
- xxx：其它需要扩展的字段。



### 2、UnionId 

`UnionId` 的特点与 `OpenId` 几乎一致：同一用户在不同 client 里的 UnionId 值是不同的，除非这些应用属于同一主体。

例如：甲公司申请了`应用A`、`应用B`、`应用C`，乙公司申请了`应用D`、`应用F`，那么用户张三：
- 在应用 A、B、C 里的 UnionId 值一致。
- 在应用 D、F 里的 UnionId 值一致。
- 在应用 A 和 应用 D 之间，UnionId 值不一致。

那么 Sa-Token 框架是如何识别到某两个应用是否为同一主体的呢？这就需要你在注册应用时指定 `subjectId` 属性了：

``` java
/**
 * Sa-Token OAuth2：自定义数据加载器
 */
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
    
    // 根据 clientId 获取 Client 信息
    @Override
    public SaClientModel getClientModel(String clientId) {
        // 此为模拟数据，真实环境需要从数据库查询 
        if("1001".equals(clientId)) {
            return new SaClientModel()
					.setClientId("xxxx")  
					.setClientSecret("xxxx")   
					.setSubjectId("1000001")   // ⚠️ 关键代码：主体 id (可选)
					// ....
            ;
        }
        return null;
    }
    
}
```

`subjectId` 代表此应用的拥有者，相同 `subjectId` 值的应用将被识别为同一主体，在授权中返回的 `unionid` 值也将一致。

框架默认的 `unionid` 生成算法为：

``` java
md5(prefix + "_" + subjectId + "_" + loginId);
```

其中的 prefix 前缀默认值为：`unionid_default_digest_prefix`，你可以通过以下方式配置：

:::tabs
== yaml 风格

``` yaml
# sa-token配置
sa-token:
	oauth2-server:
		# 默认 unionid 生成算法中使用的摘要前缀
		unionid-digest-prefix: xxxxxx
```

== properties 风格

``` properties
# 默认 unionid 生成算法中使用的摘要前缀
sa-token.oauth2-server.unionid-digest-prefix=xxxxxx
```

:::



你也可以通过实现 `SaOAuth2DataLoader` 接口完全自定义 UnionId 生成算法：

``` java
/**
 * Sa-Token OAuth2：自定义数据加载器
 */
@Component
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
	
	// 自定义 unionid 生成算法 
	@Override
	public String getUnionid(String subjectId, Object loginId) {
		// 此种写法代表使用框架默认算法生成 unionid，真实环境建议改为从数据库查询
		return SaOAuth2DataLoader.super.getUnionid(subjectId, loginId);
	}

}
``` 

unionid 算法要求与 openid 基本一致，可参考上述 openid 算法要求介绍，此处暂不赘述。




### 3、总结

| 类型			| 概念															|
| :--------		| :--------														|
| userid       	| 在 oauth2-server 端的用户，其唯一标识							|
| clientid     	| 第三方公司在 oauth2-server 开放平台申请的应用，其唯一标识			|
| openid       	| 用户在某个应用下的唯一标识										|
| unionid      	| 用户在某一组应用下的唯一标识	（按照主体id分组）					|

---

### Sa Token Doc New Docs Oauth2 Oauth2 Questions (`sa-token-doc-new/docs/oauth2/oauth2-questions.md`)

---
title: "OAuth2.0 常见问题"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,常见问题总结,OAuth2.0,OAuth2"
description: "OAuth2 集成常见问题：访问错路由会返回 not handle，正确入口是 /oauth2/authorize。code 只能用一次，重复换 token 会报无效。多账号要在 configOAuth2Server 里 setStpLogic。授权码流程的 state 须随机、一次性，用来防止账号伪装。"
---

# Sa-Token-OAuth2整合-常见问题总结

OAuth2 集成常见问题整理

[[toc]]

--- 


### 问：搭建好 oauth2-server 服务后，访问返回：`{"msg": "not handle"}`。

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

- 统一认证登录地址是：`http://{host}:{port}/oauth2/authorize`。
- 而你访问的却是：`http://{host}:{port}/oauth2/authorize2`。

地址写错了，框架就不会处理这个请求，会直接返回 `{"msg": "not handle"}`，所有开放地址可参考：[OAuth2 开放接口](/oauth2/oauth2-apidoc)



### 问：我参照文档搭建 oauth2-server，一直提示：code 无效，请问怎么回事？
一个 code 码只能使用一次，多次使用就会报这个错。



### 问：Sa-Token-OAuth2 怎么集成多账号模式？

在 `configOAuth2Server` 里指定 oauth2 模块使用的 `StpLogic` 对象即可： 

``` java
// Sa-Token OAuth2 定制化配置
@Autowired
public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) {
	// 其它配置 ... 
	
	// 指定 oauth2 模块使用的 `StpLogic` 对象 
	SaOAuth2Manager.setStpLogic(StpUserUtil.stpLogic);
}
```




### 问：授权码流程中 state 参数是干吗用的？

state 参数用于验证授权码流程的发起端和接受端是否为同一个客户端，以防止OAuth-Server账号伪装攻击。

授权流程发起端必须保证：
- state 参数必须足够随机，不可被预测。
- state 参数与授权码流程发起客户端 一 一 对 应，授权流程发起时创建的 state 必须与接受时返回的 state 值一致。
- 安全起见，一个 state 参数只允许使用一次。

---

### Sa Token Doc New Docs Oauth2 Oauth2 Scope Level (`sa-token-doc-new/docs/oauth2/oauth2-scope-level.md`)

---
title: "Sa-Token 为 Scope 划分等级"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,为 Scope 划分等级,OAuth2.0,OAuth2"
description: "OAuth2 Scope 等级：yml 用 higher-scope、lower-scope 划分高级、一般、低级。openid 等高级每次都要用户确认才下发 code；一般权限近期授过可静默；userinfo 等低级自动静默。混合申请含高级时必须确认。高级申请须走 /oauth2/doConfirm。"
---

# OAuth2 - 为 Scope 划分等级


### 1、划分等级 

我们可以通过配置文件来为 scope 划分等级

:::tabs
== yaml 风格

``` yaml
# sa-token 配置
sa-token: 
    # OAuth2.0 配置 
    oauth2-server:
        # 定义哪些 scope 是高级权限，多个用逗号隔开
        higher-scope: openid,userid
        # 定义哪些 scope 是低级权限，多个用逗号隔开
        lower-scope: userinfo
```

== properties 风格

``` properties
# 定义哪些 scope 是高级权限，多个用逗号隔开
sa-token.oauth2-server.higher-scope=openid,userid
# 定义哪些 scope 是低级权限，多个用逗号隔开
sa-token.oauth2-server.lower-scope=userinfo
```

:::


如上所示：
- 通过 `sa-token.oauth2-server.higher-scope` 配置项指定的 `scope` 将变成 **高级权限**。
- 通过 `sa-token.oauth2-server.lower-scope` 配置项指定的 `scope` 将变成 **低级权限**。
- 其它未指定的 `scope` 将默认为 **一般权限**。

不同的权限等级其差异主要表现在：oauth2-client 授权时是否需要用户手动确认授权。

| 权限等级		| 申请授权时表现						| 
| :--------		| :--------							| 
| 高级权限		| 申请授权时：每次都需要用户手动点击确认授权按钮，才会下放 code 授权码	| 
| 一般权限		| 申请授权时：如果申请的 scope 用户近期授权过，则静默授权，如果近期未授权过，则需要手动点击确认授权按钮		| 
| 低级权限		| 申请授权时：不需要用户手动点击确认授权，程序自动完成静默授权						| 


### 2、详细举例

1、如下例子，oauth2-client 申请的 `openid` 权限为**高级权限**，每次都需要用户手动点击确认授权按钮，才会下放 code 授权码。

``` url
http://{host}:{port}/oauth2/authorize
	?response_type=code
	&client_id=1001
	&redirect_uri=http://sa-oauth-client.com:8002/
	&scope=openid
```

2、如下例子，oauth2-client 申请的 `userinfo` 权限为**低级权限**，此时不需要用户手动点击确认授权，程序自动完成静默授权。

``` url
http://{host}:{port}/oauth2/authorize
	?response_type=code
	&client_id=1001
	&redirect_uri=http://sa-oauth-client.com:8002/
	&scope=userinfo
```

3、如下例子，oauth2-client 申请的 `fans_list` 权限为**一般权限**，首次申请时，需要用户手动点击确认授权，第二次再申请则是静默授权。

``` url
http://{host}:{port}/oauth2/authorize
	?response_type=code
	&client_id=1001
	&redirect_uri=http://sa-oauth-client.com:8002/
	&scope=fans_list
```

4、如下例子，oauth2-client 申请的 `openid,userid,userinfo,fans_list` 权限同时包括 **高级权限**、**低级权限**、**一般权限**：

``` url
http://{host}:{port}/oauth2/authorize
	?response_type=code
	&client_id=1001
	&redirect_uri=http://sa-oauth-client.com:8002/
	&scope=openid,userid,userinfo,fans_list
```

此时是否需要用户手动点击确认授权按钮？具体规则表现为：
- 如果请求的 scope 列表包括高级权限，则必须用户手动点击确认授权。
- 如果 scope 列表不包括高级权限，则将 scope 列表中的所有低级权限剔除。
- 剔除后的 list 大小如果为零，则直接静默授权通过。
- 剔除后的 list 大小如果不为零，则判断剩余的这些 scope 是否全部已近期授权过：
	- 如果是，则静默授权。
	- 如果否，则需要用户手动点击确认授权。


### 3、申请高级权限时 `/oauth2/authorize` 无法通过验证

由于申请高级权限时，每次都必须用户手动点击确认授权，`/oauth2/authorize` 路由接口是无法完成权限验证操作的。

此时需要将构建 `redirect_uri` 的动作提前，在 `/oauth2/doConfirm` 确认授权接口时提交当前授权页的全部 query 参数，并额外追加 `build_redirect_uri=true`：
``` url
http://{host}:{port}/oauth2/doConfirm
    ?client_id={value}
    &scope={value}
    &response_type={value}
    &redirect_uri={value}
    &state={value}
    &build_redirect_uri=true
```

返回结果示例：
``` js
{
	code: 200, 
	msg: 'ok', 
	data: null,
	redirect_uri: 'http://sa-oauth-client.com:8002/?code=n12TTc1M9REfJVqKm0wewDz0tNZDBhE1A90irOJmxD0zb92pdhUK8NghJfuC'
}
```

其中 `redirect_uri` 参数为授权挂载code地址，直接在 ajax 回调函数中使用 `location.href=res.redirect_uri` 跳转即可。

自定义确认授权视图修改参考：
``` java
// 授权确认视图
cfg.confirmView = (clientId, scopes)->{
	String scopeStr = SaFoxUtil.convertListToString(scopes);
	String yesCode =
			"fetch('/oauth2/doConfirm' + location.search + '&build_redirect_uri=true', {method: 'POST'})" +
					".then(res => res.json())" +
					".then(res => location.href=res.redirect_uri)";
	String res = "<p>应用 " + clientId + " 请求授权：" + scopeStr + "，是否同意？</p>"
			+ "<p>" +
			"		<button onclick=\"" + yesCode + "\">同意</button>" +
			"		<button onclick='history.back()'>拒绝</button>" +
			"</p>";
	return res;
};
```

---

### Sa Token Doc New Docs Oauth2 Oauth2 Server (`sa-token-doc-new/docs/oauth2/oauth2-server.md`)

---
title: "Sa-Token OAuth2-Server搭建"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,OAuth2-Server搭建,OAuth2.0,OAuth2"

description: "搭建 Sa-Token OAuth2-Server：hosts 映射域名，引入 sa-token-oauth2。SaClientModel 配签约 scope 与 authorization_code、implicit、password、client_credentials；/oauth2/* 走授权，附 demo。"
---

# 搭建OAuth2-Server

--- 

### 1、准备工作 
首先修改hosts文件`(C:\windows\system32\drivers\etc\hosts)`，添加以下IP映射，方便我们进行测试：
``` url
127.0.0.1 sa-oauth-server.com
127.0.0.1 sa-oauth-client.com
```


### 2、引入依赖 
创建SpringBoot项目 `sa-token-demo-oauth2-server`（不会的同学自行百度或参考仓库示例：[Sa-Token 集成示例大全下载](/more/download-demos) 获取全套 Demo），引入 `pom.xml` 依赖：

:::tabs
== Maven 方式

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-boot-starter</artifactId>
	<version>${sa.top.version}</version>
</dependency>


<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-oauth2</artifactId>
	<version>${sa.top.version}</version>
</dependency>


<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 权限认证，在线文档：https://sa-token.com
implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'

// Sa-Token OAuth2.0 模块
implementation 'cn.dev33:sa-token-oauth2:${sa.top.version}'

// Sa-Token 整合 RedisTemplate (可选)
implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'
implementation 'org.apache.commons:commons-pool2'
```

:::


注：Redis 相关依赖是非必须的，如果集成了 redis，可以让你更细致的观察到 sa-token-oauth2 的底层数据格式。


### 3、开放服务 


1、新建`SaOAuth2ServerController`
```
/* Detailed source-code truncated for AI context efficiency. */
```
注意：
- 在 `doLoginHandle` 函数里如果要获取 name, pwd 以外的参数，可通过 `SaHolder.getRequest().getParam("xxx")` 来获取。
- 你可以在 [框架配置](/use/config#4.3、SaClientModel属性定义) 了解有关 `SaClientModel` 对象所有属性的详细定义。


2、全局异常处理
``` java
@RestControllerAdvice
public class GlobalExceptionHandler {
	@ExceptionHandler
	public SaResult handlerException(Exception e) {
		e.printStackTrace(); 
		return SaResult.error(e.getMessage());
	}
}
```

3、创建启动类：
``` java
/**
 * 启动：Sa-OAuth2 Server端 
 */
@SpringBootApplication 
public class SaOAuth2ServerApplication {
	public static void main(String[] args) {
		SpringApplication.run(SaOAuth2ServerApplication.class, args);
		System.out.println("\nSa-Token-OAuth2 Server端启动成功，配置如下：");
		System.out.println(SaOAuth2Manager.getServerConfig());
	}
}
```
启动项目


### 4、访问测试 

1、由于暂未搭建Client端，我们可以使用 Sa-Token 官网作为重定向URL进行测试：
``` url
http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://sa-token.com&scope=openid
```

2、由于首次访问，我们在OAuth-Server端暂未登录，会被转发到登录视图 

<img class="s-w-sh" src="/big-file/doc/oauth2-new/sa-oauth2-server-login-view--v43.png" alt="OAuth2-Server 授权码模式登录页" />

3、输入 `sa/123456` 进行登录之后，会提示我们确认授权
<img class="s-w-sh" src="/big-file/doc/oauth2-new/sa-oauth2-server-scope.png" alt="OAuth2-Server 授权确认页（scope 范围）" />

4、点击同意授权之后，我们会被重定向至 redirect_uri 页面，并携带了code参数 

<img class="s-w-sh" src="/big-file/doc/oauth2-new/sa-oauth2-server-code.png" alt="OAuth2 授权成功重定向携带 code 参数" />

4、我们拿着code参数，访问以下地址：
``` url
http://sa-oauth-server.com:8000/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code}
```

将得到 `Access-Token`、`Refresh-Token`、`openid`等授权信息：

``` js
{
  "code": 200,
  "msg": "ok",
  "data": null,
  "token_type": "Bearer",
  "access_token": "cAls8jnBLmeo5yuCUMwb8zxaSsQPPzGINXF3NOCjCqFHplr6hagdT6A5HeR2",
  "refresh_token": "L2rPbJ3aaOXwaB4Zu0EGWNz5EjVNpw5u2oMP9CS2IEap7rR3Hb76ZqqHS07J",
  "expires_in": 7199,
  "refresh_expires_in": 2591999,
  "client_id": "1001",
  "scope": "openid",
  "openid": "ded91dc189a437dd1bac2274be167d50"
}
```


测试完毕


### 5、运行官方示例
以上代码只是简单模拟了一下OAuth2.0的授权流程，现在，我们运行一下官方示例，里面有制作好的UI界面

- OAuth2-Server端： `/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/` [源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server) <br/>
- 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/>

依次启动`OAuth2-Server` 与 `OAuth2-Client`，然后从浏览器访问：[http://sa-oauth-client.com:8002](http://sa-oauth-client.com:8002)

<img class="s-w-sh" src="/big-file/doc/oauth2-new/sa-oauth2-client-index.png" alt="OAuth2-Client 四种模式测试首页" />

如图，可以针对OAuth2.0四种模式进行详细测试 



### 6、OAuth2 前端测试页

OAuth2 前端测试页： 
`/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5/` 
[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client-h5) <br/>

此示例允许你在前端自由配置 OAuth-Client 端所需的各个参数，方便对 OAuth2 四种模式的测试。

<img class="s-w-sh" src="/big-file/doc/oauth2-new/sa-oauth2-client-test-h5-page.png" alt="OAuth2 前端测试页配置界面" />

<p><a class="case-btn case-btn-video" href="https://www.bilibili.com/video/BV13LSMYzEmE/" target="_blank">
	视频讲解：OAuth2 四种模式 前端测试页
</a></p>

---

### Sa Token Doc New Docs Plugin Alone Redis (`sa-token-doc-new/docs/plugin/alone-redis.md`)

---
title: "Sa-Token Alone独立Redis插件"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Alone独立Redis插件,插件,Redis"
description: "Sa-Token Alone 独立 Redis：引入 sa-token-alone-redis（Boot 4 换 by-spring-boot4 包），alone-redis 与 spring.redis 分库，降低冲突。仅对 redis-template 生效，Redisson 请改用 Alone-Redisson。"
---

# Sa-Token-Alone-Redis 独立Redis插件
--- 

Sa-Token默认的Redis集成方式会把权限数据和业务缓存放在一起，但在部分场景下我们需要将他们彻底分离开来，比如：

::: info 业务场景
搭建两个Redis服务器，一个专门用来做业务缓存，另一台专门存放Sa-Token权限数据 
:::


<button class="show-img" img-src="/big-file/doc/plugin/g3--alone-redis.gif">加载动态演示图</button>


要将Sa-Token的数据单独抽离出来很简单，你只需要为Sa-Token单独配置一个Redis连接信息即可 

--- 


### 1、首先引入Alone-Redis依赖 


::: warning Spring Boot 4 用户
若使用 Spring Boot 4.x，请引入 `sa-token-alone-redis-by-spring-boot4` 替代 `sa-token-alone-redis`。
:::

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-alone-redis</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 整合 Redis （使用 jackson 序列化方式）
implementation 'cn.dev33:sa-token-alone-redis:${sa.top.version}'
```

:::



### 2、然后在application.yml中增加配置

:::tabs
== yaml 风格

``` yaml
# Sa-Token 配置
sa-token: 
	# Token名称
	token-name: satoken
	# Token有效期
	timeout: 2592000
	# Token风格
	token-style: uuid
	
	# 配置 Sa-Token 单独使用的 Redis 连接 
	alone-redis: 
		# Redis数据库索引（默认为0）
		database: 2
		# Redis服务器地址
		host: 127.0.0.1
		# Redis服务器连接端口
		port: 6379
		# Redis服务器连接密码（默认为空）
		password: 
		# 连接超时时间
		timeout: 10s

spring: 
	# 配置业务使用的 Redis 连接 
	redis: 
		# Redis数据库索引（默认为0）
		database: 0
		# Redis服务器地址
		host: 127.0.0.1
		# Redis服务器连接端口
		port: 6379
		# Redis服务器连接密码（默认为空）
		password: 
		# 连接超时时间
		timeout: 10s
```

== properties 风格

``` properties
############## Sa-Token 配置 ############## 
# Token名称
sa-token.token-name=satoken
# Token有效期
sa-token.timeout=2592000
# Token风格
sa-token.token-style=uuid

############## 配置 Sa-Token 单独使用的 Redis 连接  ############## 
# Redis数据库索引（默认为0）
sa-token.alone-redis.database=2
# Redis服务器地址
sa-token.alone-redis.host=127.0.0.1
# Redis服务器连接端口
sa-token.alone-redis.port=6379
# Redis服务器连接密码（默认为空）
sa-token.alone-redis.password=
# 连接超时时间
sa-token.alone-redis.timeout=10s

############## 配置业务使用的 Redis 连接 ############## 
# Redis数据库索引（默认为0）
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码（默认为空）
spring.redis.password=
# 连接超时时间
spring.redis.timeout=10s

```

:::



具体可参考示例：[码云：application.yml](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redis/src/main/resources/application.yml)

集群配置说明: alone-redis同样可以配置集群(cluster模式和sentinel模式), 且基础配置参数和spring redis集群配置别无二致

集群配置示例可参考demo项目sa-token-demo-alone-redis-cluster


### 3、测试
新建Controller测试一下 
``` java
@RestController
@RequestMapping("/test/")
public class TestController {

	@Autowired
	StringRedisTemplate stringRedisTemplate;
	
	// 测试Sa-Token缓存
	@RequestMapping("login")
	public SaResult login(@RequestParam(defaultValue="10001") String id) {
		System.out.println("--------------- 测试Sa-Token缓存");
		StpUtil.login(id);	
		return SaResult.ok();
	}
	
	// 测试业务缓存
	@RequestMapping("test")
	public SaResult test() {
		System.out.println("--------------- 测试业务缓存");
		stringRedisTemplate.opsForValue().set("hello", "Hello World");
		return SaResult.ok();
	}
	
}
```

分别访问两个接口，观察Redis中增加的数据 

<img class="s-w" src="/big-file/doc/plugin/alone-redis.png" alt="Alone-Redis 插件架构示意图" />

测试完毕！

### 4、注意点
目前 Sa-Token-Alone-Redis 仅对以下插件有 Redis 分离效果：
- sa-token-redis-template
- sa-token-redis-template-jdk-serializer

若使用的是 Redisson 而非 RedisTemplate，请改用 [Alone 独立 Redisson 插件](/plugin/alone-redisson)。

---

### Sa Token Doc New Docs Plugin Alone Redisson (`sa-token-doc-new/docs/plugin/alone-redisson.md`)

---
title: "Sa-Token Alone独立Redisson插件"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Alone独立Redisson插件,插件,Redis"
description: "Sa-Token Alone 独立 Redisson：引入 sa-token-alone-redisson 单独配 RedissonClient，权限数据与业务缓存分连接，不必加 sa-token-redisson。支持单机/集群 yaml；Dao 默认 StringCodec，Boot 4 勿用官方 starter。"
---

# Sa-Token-Alone-Redisson 独立 Redisson 插件
---

Sa-Token 默认的 Redisson 集成会把权限数据和业务缓存放在同一个 `RedissonClient` 上。需要彻底分离时，可再为 Sa-Token 单独起一个 Redisson 连接。

::: info 业务场景
搭建两个 Redis：一个给业务 RedissonClient，另一台专门存放 Sa-Token 权限数据。
:::

---

### 1、引入依赖

引入 `sa-token-alone-redisson` 即可，**无需再引入** `sa-token-redisson` 或 `sa-token-redisson-spring-boot-starter`（本插件已包含 Dao 实现并完成注册）。

业务代码如果也要用 `RedissonClient`：
- Spring Boot 2 / 3：可再引入官方 `redisson-spring-boot-starter`，或自行注册 Bean。
- Spring Boot 4：当前 Redisson 3.45 的 starter 仍引用已移除的 `RedisAutoConfiguration`，**不要引入** `redisson-spring-boot-starter`，改为引入 `redisson` 并自行注册 Bean。`sa-token-alone-redisson` 本身无需 Boot 4 专用包。

:::tabs
== Maven 方式

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-alone-redisson</artifactId>
	<version>${sa.top.version}</version>
</dependency>


<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson-spring-boot-starter</artifactId>
	<version>${redisson.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
implementation 'cn.dev33:sa-token-alone-redisson:${sa.top.version}'
// 业务 Redis（按需）
implementation 'org.redisson:redisson-spring-boot-starter:${redisson.version}'
```

:::



### 2、配置独立连接（Redisson 原生 yaml）

配置项与官方 `spring.redis.redisson.config` / `file` 相同，单机用 `singleServerConfig`，集群用 `clusterServersConfig`。

:::tabs
== 单机（内嵌 yaml）

``` yaml
sa-token:
    token-name: satoken
    timeout: 2592000
    # Sa-Token 单独使用的 Redisson 连接
    alone-redisson:
        config: |
            singleServerConfig:
                address: "redis://127.0.0.1:6379"
                database: 2
                password: null

spring:
    redis:
        # 业务使用的 Redisson 连接
        redisson:
            config: |
                singleServerConfig:
                    address: "redis://127.0.0.1:6379"
                    database: 0
                    password: null
```

== 集群（内嵌 yaml）

``` yaml
sa-token:
    alone-redisson:
        config: |
            clusterServersConfig:
                nodeAddresses:
                    - "redis://127.0.0.1:3000"
                    - "redis://127.0.0.1:3001"
                    - "redis://127.0.0.1:3002"
                password: null
```

== 外置文件

``` yaml
# application.yml
sa-token:
    alone-redisson:
        file: classpath:sa-redisson.yml
```

`src/main/resources/sa-redisson.yml` 示例（单机）：

``` yaml
singleServerConfig:
    address: "redis://127.0.0.1:6379"
    database: 2
    password: null
```

集群则把内容换成上面「集群」页签里的 `clusterServersConfig` 即可。

:::


`config` 与 `file` 二选一，同时配置时优先 `config`。

完整示例：
- 单机：[sa-token-demo-alone-redisson](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson/src/main/resources/application.yml)
- 集群：[sa-token-demo-alone-redisson-cluster](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson-cluster/src/main/resources/application.yml)
- Spring Boot 3：[sa-token-demo-alone-redisson-sb3](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson-sb3/src/main/resources/application.yml)
- Spring Boot 4：[sa-token-demo-alone-redisson-sb4](https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-alone-redisson-sb4/src/main/resources/application.yml)


### 3、测试

``` java
@RestController
@RequestMapping("/test/")
public class TestController {

	@Autowired
	RedissonClient redissonClient;

	// 测试 Sa-Token 缓存 --- http://localhost:8084/test/login
	@RequestMapping("login")
	public SaResult login(@RequestParam(defaultValue="10001") String id) {
		StpUtil.login(id);
		return SaResult.ok();
	}

	// 测试业务缓存 --- http://localhost:8084/test/test
	@RequestMapping("test")
	public SaResult test() {
		redissonClient.getBucket("hello").set("Hello World");
		return SaResult.ok();
	}
}
```

分别访问两个接口，观察两套 Redis 中增加的数据。



### 4、注意点
- 引入本插件后，无需再引入 `sa-token-redisson` 或 `sa-token-redisson-spring-boot-starter`。
- 业务代码注入的 `RedissonClient` 来自官方 `redisson-spring-boot-starter` 或你自己注册的 Bean，与 Sa-Token 使用的独立连接互不影响。Spring Boot 4 请自行注册，不要引入 3.45 的 starter。
- `SaTokenDaoForRedisson` 默认使用 `StringCodec`，与业务 `RedissonClient` 的全局 codec 无关。
- Redis Cluster 没有 database 索引，集群配置里不要写 `database`。
- 哨兵模式同样走 Redisson 原生 yaml（`sentinelServersConfig`），按 [Redisson 配置文档](https://github.com/redisson/redisson/wiki/2.-Configuration) 填写即可。

---

### Sa Token Doc New Docs Plugin Aop At (`sa-token-doc-new/docs/plugin/aop-at.md`)

---
title: "Sa-Token AOP注解鉴权"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,AOP注解鉴权,插件"
description: "Sa-Token AOP 注解鉴权：默认拦截器只能在 Controller 用 @SaCheckLogin 等注解。引入 sa-token-spring-aop 后可在 Service 等任意层级鉴权。注意拦截器模式与 AOP 模式不可同时集成，否则 Controller 会校验两次。覆盖角色、权限等注解。"
---

# AOP注解鉴权
--- 

在 [注解式鉴权](/use/at-check) 章节，我们非常轻松的实现了注解鉴权，
但是默认的拦截器模式却有一个缺点，那就是无法在`Controller层`以外的代码使用进行校验

因此Sa-Token提供AOP插件，你只需在`pom.xml`里添加如下依赖，便可以在任意层级使用注解鉴权

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-aop</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 整合 SpringAOP 实现注解鉴权
implementation 'cn.dev33:sa-token-spring-aop:${sa.top.version}'
```

:::



#### 注意点：
- 使用拦截器模式，只能把注解写在`Controller层`，使用AOP模式，可以将注解写在任意层级 <br>
- **拦截器模式和AOP模式不可同时集成**，否则会在`Controller层`发生一个注解校验两次的bug

---

### Sa Token Doc New Docs Plugin Api Key (`sa-token-doc-new/docs/plugin/api-key.md`)

---
title: "Sa-Token API Key 接口调用秘钥"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,API Key 接口调用秘钥,插件"
description: "Sa-Token API Key 接口调用秘钥：sa-token-apikey 为开放接口生成绑定用户的调用凭证，用 scope 做最小化授权，避免第三方插件索要账号密码。SaApiKeyUtil 创建/校验，支持 @SaCheckApiKey、有效期回收、缓存或数据库模式。前端用 apikey 参数提交。"
---

# API Key 接口调用秘钥

API Key（应用程序编程接口密钥） 是一种用于身份验证和授权的字符串代码，通常由服务提供商生成并分配给开发者或用户。它的主要作用是标识调用 API（应用程序编程接口）的请求来源，确保请求的合法性，并控制访问权限。

以上是官话，简单理解：API Key 是一种接口调用密钥，类似于会话 token ，但比会话 token 具有更灵活的权限控制。

示例仓库地址：[sa-token-demo-apikey](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-apikey) 🔗

<img class="s-w-sh" src="/big-file/doc/plugin/sa-api-key.png" alt="API Key 模块鉴权示意图" />


### 1、需求场景

为了帮助大家更好的理解 API Key 的应用场景，我们假设具有以下业务场景：

::: info 业务场景
你们公司开发了一款论坛网站，非常火爆。

某日，你发现一位用户的头像可以随着日期而变化，Ta 的头像总是显示当前最新日期。

这并未引起你的警觉，因为你是一个程序员，在你看来，写一个任务脚本，每天定时调用 API 更新自己的头像是一件非常简单的事情。

一个月后，越来越多的账号“具有了此功能”，仿佛发生了人传人，Ta 们的头像都可以随着日期而变化，而且颜色各不相同，DIY 的不亦乐乎。

这引起了你的怀疑，如此大批账号的自动化更新行为，显然不是 “某个程序员利用定时脚本更新账号信息” 可以解释的。

一番调查之后，你发现了事情的真相，没有灰产公司捣乱，这批账号也不是机器账号，只是有一个公司为你们的网站开发了一款插件。

这款插件的作用是：用户把自己的 账号+密码 保存在插件中，插件便可以定时更新该账号的头像、昵称、资料等信息。

你觉得插件很有意思，但是插件“要求用户提交账号密码”的行为，让你感到很不爽。

总有一些用户为了得到“些许便利”，而出卖自己的账号密码给插件。

随着时间推移，越来越多的第三方公司或个人为你的网站开发插件：有的可以自动更新账号资料、有的可以自动发帖，有的检测到新粉丝就发送消息通知...

最终，不守规矩的插件出现了：一款插件在提供功能的同时，大量收集用户密码等隐私信息，作为不法用途。

为了遏制这种现象，你们公司升级了系统，增加了 IP 校验等风控判断，阻断了这些插件的 API 调用。

似乎……解决了问题？用户再也不会把账号密码交给第三方插件了。

但是插件的需求总是存在的呀，有些用户确实很需要这些插件的能力来提高网站使用体验。

俗话说的好，堵不如疏，既然用户有需求，第三方公司愿意免费打工开发插件，我们何不设计一套授权架构，
既不需要让用户把账号密码交给第三方插件，又能让插件得到一些权限来调用特定 API 为用户服务。

API Key 就是为了完成这种“可控式部分授权” 而设计的一种身份凭证。
:::


为了让第三方插件为用户工作，用户必定是要为插件提供一个“凭证”信息的，然后插件利用“凭证”信息，代替用户调用特定 API 完成一些功能。

不同的凭证信息将会带来不同的后果：


| 提供的凭证					| 后果										|
| :--------					| :--------									|
| 账号密码					| 插件可以得到账号所有权限，安全风险极高		|
| 会话 token					| 插件可以调用几乎所有 API，安全风险极高，且容易受到用户退出登录导致 token 失效的影响		|
| API Key					| 在可控的范围内进行部分授权，且可以方便的随时取消授权，只要设计得当，不会造成安全问题		|

API Key 具有以下特点：
- 1、格式类似于会话 token，是一个随机字符串。
- 2、每个 API Key 都会和具体的用户 id 发生绑定，后端可以查询到此 API Key 的授权人是谁。
- 3、一个用户可以创建多个 API Key，用作不同的插件中。
- 4、每个 API Key 都可以赋予不同的 scope 权限，以做到最小化授权。
- 5、API Key 可以设置有效期，并且随时删除回收，做到灵活控制。



### 2、引入依赖
在使用 API Key 模块之前，你必须先引入依赖：
``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-apikey</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```


### 3、创建 API Key

理解了应用场景后，让我们看看 Sa-Token 为 API Key 提供了哪些方法：


``` java
// 为指定用户创建一个新的 API Key 
ApiKeyModel akModel = SaApiKeyUtil.createApiKeyModel(10001).setTitle("test");
System.out.println("API Key 值：" + akModel.getApiKey());

// 保存 API Key 
SaApiKeyUtil.saveApiKey(akModel);

// 删除 API Key 
SaApiKeyUtil.deleteApiKey(apiKey);
```

一个 ApiKeyModel 可设置以下属性：
``` java
ApiKeyModel akModel = new ApiKeyModel();
akModel.setLoginId(10001);  // 设置绑定的用户 id
akModel.setApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp");  // 设置 API Key 值
akModel.setTitle("commit");	  // 设置名称
akModel.setIntro("提交代码专用");   // 设置描述
akModel.addScope("commit", "pull");  // 设置权限范围
akModel.setExpiresTime(System.currentTimeMillis() + 2592000);  // 设置失效时间，13位时间戳，-1=永不失效
akModel.setIsValid(true);   // 设置是否有效
akModel.addExtra("name", "张三");   // 设置扩展信息
// 保存 
SaApiKeyUtil.saveApiKey(akModel);  
```

查询：

``` java
// 获取 API Key 详细信息 
ApiKeyModel akModel = SaApiKeyUtil.getApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp");

// 直接获取 ApiKey 所代表的 loginId
Object loginId = SaApiKeyUtil.getLoginIdByApiKey("AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp");

// 获取指定 loginId 的 ApiKey 列表记录
List<ApiKeyModel> apiKeyList = SaApiKeyUtil.getApiKeyList(10001);
```


### 4、校验 API Key

``` java
// 校验指定 API Key 是否有效，无效会抛出异常 ApiKeyException
SaApiKeyUtil.checkApiKey("AK-XxxXxxXxx");

// 校验指定 API Key 是否具有指定 Scope 权限，不具有会抛出异常 ApiKeyScopeException
SaApiKeyUtil.checkApiKeyScope("AK-XxxXxxXxx", "userinfo");

// 校验指定 API Key 是否具有指定 Scope 权限，返回 true 或 false
SaApiKeyUtil.hasApiKeyScope("AK-XxxXxxXxx", "userinfo");

// 校验指定 API Key 是否属于指定账号 id 
SaApiKeyUtil.checkApiKeyLoginId("AK-XxxXxxXxx", 10001);
```

注解鉴权示例：
``` java
/**
 * API Key 资源 相关接口
 */
@RestController
public class ApiKeyResourcesController {

	// 必须携带有效的 ApiKey 才能访问
	@SaCheckApiKey
	@RequestMapping("/akRes1")
	public SaResult akRes1() {
		ApiKeyModel akModel = SaApiKeyUtil.currentApiKey();
		System.out.println("当前 ApiKey: " + akModel);
		return SaResult.ok("调用成功");
	}

	// 必须携带有效的 ApiKey ，且具有 userinfo 权限
	@SaCheckApiKey(scope = "userinfo")
	@RequestMapping("/akRes2")
	public SaResult akRes2() {
		ApiKeyModel akModel = SaApiKeyUtil.currentApiKey();
		System.out.println("当前 ApiKey: " + akModel);
		return SaResult.ok("调用成功");
	}

	// 必须携带有效的 ApiKey ，且同时具有 userinfo、chat 权限
	@SaCheckApiKey(scope = {"userinfo", "chat"})
	@RequestMapping("/akRes3")
	public SaResult akRes3() {
		ApiKeyModel akModel = SaApiKeyUtil.currentApiKey();
		System.out.println("当前 ApiKey: " + akModel);
		return SaResult.ok("调用成功");
	}

	// 必须携带有效的 ApiKey ，且具有 userinfo、chat 其中之一权限
	@SaCheckApiKey(scope = {"userinfo", "chat"}, mode = SaMode.OR)
	@RequestMapping("/akRes4")
	public SaResult akRes4() {
		ApiKeyModel akModel = SaApiKeyUtil.currentApiKey();
		System.out.println("当前 ApiKey: " + akModel);
		return SaResult.ok("调用成功");
	}

}
```


### 5、前端如何提交 API Key？
默认情况下，前端可以从任意途径提交 API Key 字符串，只要后端能接受到。

但是如果后端是通过 `SaApiKeyUtil.currentApiKey()` 方法获取，或者 `@SaCheckApiKey` 注解校验，则需要前端按照一定的格式来提交了：

方式一：通过请求参数或请求头，参数名为 `apikey`（全小写）

``` url
/user/getInfo?apikey=AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp
```


方式二：通过 Basic 参数提交

``` url
http://AK-NAO6u57zbOWCmLaiVQuVW2tyt3rHpZrXkaQp@localhost:8081/user/getInfo
```





### 6、打开数据库模式

框架默认将所有 API Key 信息保存在缓存中，这可以称之为“缓存模式”，这种模式下，重启缓存库后，数据将丢失。

如果你想改为“数据库模式”，可以通过 `implements SaApiKeyDataLoader` 实现从数据库加载的逻辑。

``` java
/**
 * API Key 数据加载器实现类 （从数据库查询）
 */
@Component 
public class SaApiKeyDataLoaderImpl implements SaApiKeyDataLoader {

    @Autowired
    SaApiKeyMapper apiKeyMapper;

    // 指定框架不再维护 API Key 索引信息，而是由我们手动从数据库维护
    @Override
    public Boolean getIsRecordIndex() {
        return false;
    }

    // 根据 apiKey 从数据库获取 ApiKeyModel 信息 （实现此方法无需为数据做缓存处理，框架内部已包含缓存逻辑）
    @Override
    public ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) {
        return apiKeyMapper.getApiKeyModel(apiKey);
    }

}
```

参考上述代码实现后，框架内部逻辑将会做出一些改变，请注意以下事项：

- 1、调用 `SaApiKeyUtil.getApiKey("ApiKey")` 时，会先从缓存中查询，查询不到时调用 `getApiKeyModelFromDatabase` 从数据库加载。
- 2、框架不再维护 API Key 索引数据，这意味着无法再调用 `SaApiKeyUtil.getApiKeyList(10001)` 来获取一个用户的所有的 API Key 数据，请自行从数据库查询。
- 3、调用 `SaApiKeyUtil.saveApiKey(akModel)` 保存时，只会把 API Key 数据保存到缓存中，请自行补充额外代码向数据库保存数据。
- 4、调用 `SaApiKeyUtil.deleteApiKey("ApiKey")` 时，只会删除这个 API Key 在缓存中的数据，不会删除数据库的数据，请自行补充相关代码保证数据双删。
- 5、其它诸如查询 `SaApiKeyUtil.getApiKey("ApiKey")` 或校验 `SaApiKeyUtil.checkApiKeyScope("ApiKey", "userinfo")` 等方法，依旧可以正常调用。



### 7、多账号模式使用 

如果系统有多套账号表，比如 Admin 和 User，只需要指定不同的命名空间即可：

例如 User 账号的 API Key，我们使用原生 `SaApiKeyUtil` 进行创建与校验。

对于 Admin 账号的 API Key，我们则新建一个 `SaApiKeyTemplate` 实例

``` java
// 新建 Admin 账号的 apiKeyTemplate 对象，命名空间为 "admin-apikey"
public static SaApiKeyTemplate adminApiKeyTemplate = new SaApiKeyTemplate("admin-apikey");

// 创建一个新的 ApiKey，并返回
@RequestMapping("/createApiKey")
public SaResult createApiKey() {
	ApiKeyModel akModel = adminApiKeyTemplate.createApiKeyModel(StpUtil.getLoginId()).setTitle("test");
	adminApiKeyTemplate.saveApiKey(akModel);
	return SaResult.data(akModel);
}

// ...校验、查询等操作，均使用新创建的 adminApiKeyTemplate，而非原生 `SaApiKeyUtil`
```

---

### Sa Token Doc New Docs Plugin Api Sign (`sa-token-doc-new/docs/plugin/api-sign.md`)

---
title: "Sa-Token API 接口参数签名"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,API 接口参数签名,插件"
description: "Sa-Token API 接口参数签名：sa-token-sign 用 timestamp、nonce、sign 防伪造、防篡改、防重放。SaSignUtil 自动补全签名参数，接收端 checkRequest 一句校验；支持 @SaCheckSign 注解与 sign-many 多应用不同秘钥及字典序拼参。"
---

# API 接口参数签名

<p><a class="case-btn case-btn-video" href="https://www.bilibili.com/video/BV17oeKeZEHo/" target="_blank">
	观看本节视频讲解（B站：抓蛙师）
</a></p>

在涉及跨系统接口调用时，我们容易碰到以下安全问题：

- 请求身份被伪造。
- 请求参数被篡改。
- 请求被抓包，然后重放攻击。

sa-token-sign 模块将帮你轻松解决以上难题。

本篇将根据假设的需求场景，循序渐进讲明白跨系统接口调用时必做的几个步骤，以及为什么要有这些步骤的原因。


### 1、需求场景

假设我们有如下业务需求：

::: info 业务场景
用户在 A 系统参与活动成功后，活动奖励以余额的形式下发到 B 系统。
:::


### 2、初始方案：直接裸奔

在不考虑安全问题的情况下，我们很容易完成这个需求：

1、在 B 系统开放一个接口。

``` java
/**
 * 为指定用户添加指定余额
 * 
 * @param userId 用户 id
 * @param money 要添加的余额，单位：分
 * @return / 
 */
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money) {
	// 处理业务 
	// ...
	
	// 返回 
	return SaResult.ok();
}
```

2、在 A 系统使用 http 工具类调用这个接口。

``` java
long userId = 10001;
long money = 1000;
String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money);
```

上述代码简单的完成了需求，但是很明显它有一个安全问题：

B 系统开放的接口不仅可以被 A 系统调用，还可以被其它任何人调用，甚至别人可以本地跑一个 for 循环调用这个接口，为自己无限充值金额。


### 3、方案升级：增加 secretKey 校验

为防止 B 系统开放的接口被陌生人任意调用，我们增加一个 secretKey 参数

``` java
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money, String secretKey) {
	// 1、先校验 secretKey 参数是否正确，如果不正确直接拒绝响应请求
	if( ! check(secretKey) ) {
		return SaResult.error("无效 secretKey，无法响应请求");
	}
	
	// 2、业务代码 
	// ...
	
	// 3、返回
	return SaResult.ok();
}
```

由于 A 系统是我们 “自己人”，所以它可以拿着 `secretKey` 进行合法请求：

``` java
long userId = 10001;
long money = 1000;
String secretKey = "xxxxxxxxxxxxxxxxxxxx";
String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "&secretKey=" + secretKey);
```

现在，即使 B 系统的接口被暴露了，也不会被陌生人任意调用了，安全性得到了一定的保证，但是仍然存在一些问题：

- 如果请求被抓包，secretKey 就会泄露，因为每次请求都在 url 中明文传输了 secretKey 参数。
- 如果请求被抓包，请求的其它参数就可以被任意修改，例如可以将 money 参数修改为 9999999，B系统无法确定参数是否被修改过。



### 4、方案再升级：使用摘要算法生成参数签名

首先，在 A 系统不要直接发起请求，而是先计算一个 sign 参数：

``` java
// 声明变量
long userId = 10001;
long money = 1000;
String secretKey = "xxxxxxxxxxxxxxxxxxxx";

// 计算 sign 参数
String sign = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey);

// 将 sign 拼接在请求地址后面
String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "&sign=" + sign);
```

**注意此处计算签名时，需要将所有参数按照字典顺序依次排列（key除外，挂在最后面）。**以下所有计算签名时同理，不再赘述。

然后在 B 系统接收请求时，使用同样的算法、同样的秘钥，生成 sign 字符串，与参数中 sign 值进行比较：


``` java
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money, String sign) {

	// 在 B 系统，使用同样的算法、同样的密钥，计算出 sign2，与传入的 sign 进行比对
	String sign2 = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey);
	if( ! sign2.equals(sign)) {
		return SaResult.error("无效 sign，无法响应请求");
	}

	// 2、业务代码 
	// ...
	
	// 3、返回
	return SaResult.ok();
}
```

因为 sign 的值是由 userId、money、secretKey 三个参数共同决定的，所以只要有一个参数不一致，就会造成最终生成 sign 也是不一致的，所以，根据比对结果：

- 如果 sign 一致，说明这是个合法请求。
- 如果 sign 不一致，说明发起请求的客户端秘钥不正确，或者请求参数被篡改过，是个不合法请求。

此方案优点：
- 不在 url 中直接传递 secretKey 参数了，避免了泄露风险。
- 由于 sign 参数的限制，请求中的参数也不可被篡改，B 系统可放心的使用这些参数。

此方案仍然存在以下缺陷：
- 被抓包后，请求可以被无限重放，B 系统无法判断请求是真正来自于 A 系统发出的，还是被抓包后重放的。



### 5、方案再再升级：追加 nonce 随机字符串

首先，在 A 系统发起调用前，追加一个 nonce 参数，一起参与到签名中：

``` java
// 声明变量
long userId = 10001;
long money = 1000;
String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串
String secretKey = "xxxxxxxxxxxxxxxxxxxx";

// 计算 sign 参数
String sign = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey);

// 将 sign 拼接在请求地址后面
String res = HttpUtil.request("http://b.com/api/addMoney?userId=" + userId + "&money=" + money + "nonce=" + nonce + "&sign=" + sign);
```

然后在 B 系统接收请求时，也把 nonce 参数加进去生成 sign 字符串，进行比较：

``` java
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money, String nonce, String sign) {

	// 1、检查此 nonce 是否已被使用过了
	if(CacheUtil.get("nonce_" + nonce) != null) {
		return SaResult.error("此 nonce 已被使用过了，请求无效");
	}

	// 2、验证签名
	String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey);
	if( ! sign2.equals(sign)) {
		return SaResult.error("无效 sign，无法响应请求");
	}

	// 3、将 nonce 记入缓存，防止重复使用
	CacheUtil.set("nonce_" + nonce, "1");

	// 4、业务代码 
	// ...

	// 5、返回
	return SaResult.ok();
}
```

代码分析：
 
- 为方便理解，我们先看第 3 步：此处在校验签名成功后，将 nonce 随机字符串记入缓存中。
- 再看第 1 步：每次请求进来，先查看一下缓存中是否已经记录了这个随机字符串，如果是，则立即返回：无效请求。

这两步的组合，保证了一个 nonce 随机字符串只能被使用一次，如果请求被抓包后重放，是无法通过 nonce 校验的。

至此，问题似乎已被解决了 …… 吗？

别急，我们还有一个问题没有考虑：这个 nonce 在字符串在缓存应该被保存多久呢？

- 保存 15 分钟？那抓包的人只需要等待 15 分钟，你的 nonce 记录在缓存中消失，请求就可以被重放了。
- 那保存 24 小时？保存一周？保存半个月？好像无论保存多久，都无法从根本上解决这个问题。

你可能会想到，那我永久保存吧。这样确实能解决问题，但显然服务器承载不了这么做，即使再微小的数据量，在时间的累加下，也总一天会超出服务器能够承载的上限。


### 6、方案再再再升级：追加 timestamp 时间戳

我们可以再追加一个 timestamp 时间戳参数，将请求的有效性限定在一个有限时间范围内，例如 15分钟。

首先，在 A 系统追加 timestamp 参数：

``` java
// 声明变量
long userId = 10001;
long money = 1000;
String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串
long timestamp = System.currentTimeMillis(); // 系统当前时间戳 
String secretKey = "xxxxxxxxxxxxxxxxxxxx";

// 计算 sign 参数
String sign = md5("money=" + money + "&nonce=" + nonce + "&timestamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey);

// 将 sign 拼接在请求地址后面
String res = HttpUtil.request("http://b.com/api/addMoney" +
		"?userId=" + userId + "&money=" + money + "&nonce=" + nonce + "&timestamp=" + timestamp + "&sign=" + sign);
```

在 B 系统检测这个 timestamp 是否超出了允许的范围 

``` java
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) {

	// 1、检查 timestamp 是否超出允许的范围（此处假定最大允许15分钟差距）
	long timestampDisparity = System.currentTimeMillis() - timestamp; // 实际的时间差
	if(timestampDisparity > 1000 * 60 * 15) {
		return SaResult.error("timestamp 时间差超出允许的范围，请求无效");
	}

	// 2、检查此 nonce 是否已被使用过了
	// 代码同上，不再赘述

	// 3、验证签名
	// 代码同上，不再赘述

	// 4、将 nonce 记入缓存，ttl 有效期和 allowDisparity 允许时间差一致 
	CacheUtil.set("nonce_" + nonce, "1", 1000 * 60 * 15);

	// 5、业务代码 ...

	// 6、返回
	return SaResult.ok();
}
```

至此，抓包者：

- 如果在 15 分钟内重放攻击，nonce 参数不答应：缓存中可以查出 nonce 值，直接拒绝响应请求。
- 如果在 15 分钟后重放攻击，timestamp 参数不答应：超出了允许的 timestamp 时间差，直接拒绝响应请求。


### 7、服务器的时钟差异造成安全问题

以上的代码，均假设 A 系统服务器与 B 系统服务器的时钟一致，才可以正常完成安全校验，但在实际的开发场景中，有些服务器会存在时钟不准确的问题。

假设 A 服务器与 B 服务器的时钟差异为 10 分钟，即：在 A 服务器为 8:00 的时候，B 服务器为 7:50。

1. A 系统发起请求，其生成的时间戳也是代表 8:00。
2. B 系统接受到请求后，完成业务处理，此时 nonce 的 ttl 为 15分钟，到期时间为 7:50 + 15分 = 8:05。
3. 8.05 后，nonce 缓存消失，抓包者重放请求攻击：
	- timestamp 校验通过：因为时间戳差距仅有 8.05 - 8.00 = 5分钟，小于 15 分钟，校验通过。
	- nonce 校验通过：因为此时 nonce 缓存已经消失，可以通过校验。
	- sign 校验通过：因为这本来就是由 A 系统构建的一个合法签名。
	- 攻击完成。

要解决上述问题，有两种方案：
- 方案一：修改服务器时钟，使两个服务器时钟保持一致。
- 方案二：在代码层面兼容时钟不一致的场景。

要采用方案一的同学可自行搜索一下同步时钟的方法，在此暂不赘述，此处详细阐述一下方案二。

我们只需简单修改一下，B 系统校验参数的代码即可：

``` java
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) {

	// 1、检查 timestamp 是否超出允许的范围 （⚠️ 重点一：此处需要取绝对值）
	long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp);
	if(timestampDisparity > 1000 * 60 * 15) {
		return SaResult.error("timestamp 时间差超出允许的范围，请求无效");
	}

	// 2、检查此 nonce 是否已被使用过了
	// 代码同上，不再赘述 

	// 3、验证签名
	// 代码同上，不再赘述 

	// 4、将 nonce 记入缓存，防止重复使用（⚠️ 重点二：此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ）
	CacheUtil.set("nonce_" + nonce, "1", (1000 * 60 * 15) * 2);

	// 5、业务代码 ...

	// 6、返回
	return SaResult.ok();
}
```


### 8、最终版方案

此处再贴一下完整的代码。

A 系统（发起请求端）：

``` java
// 声明变量
long userId = 10001;
long money = 1000;
String nonce = SaFoxUtil.getRandomString(32); // 随机32位字符串
long timestamp = System.currentTimeMillis(); // 当前时间戳
String secretKey = "xxxxxxxxxxxxxxxxxxxx";

// 计算 sign 参数
String sign = md5("money=" + money + "&nonce=" + nonce + "&timestamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey);

// 将 sign 拼接在请求地址后面
String res = HttpUtil.request("http://b.com/api/addMoney" +
		"?userId=" + userId + "&money=" + money + "&nonce=" + nonce + "&timestamp=" + timestamp + "&sign=" + sign);
```

B 系统（接收请求端）：

``` java
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money, long timestamp, String nonce, String sign) {

	// 1、检查 timestamp 是否超出允许的范围
	long allowDisparity = 1000 * 60 * 15;	// 允许的时间差：15分钟
	long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp); // 实际的时间差
	if(timestampDisparity > allowDisparity) {
		return SaResult.error("timestamp 时间差超出允许的范围，请求无效");
	}

	// 2、检查此 nonce 是否已被使用过了
	if(CacheUtil.get("nonce_" + nonce) != null) {
		return SaResult.error("此 nonce 已被使用过了，请求无效");
	}

	// 3、验证签名
	String sign2 = md5("money=" + money + "&nonce=" + nonce + "&timestamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey);
	if( ! sign2.equals(sign)) {
		return SaResult.error("无效 sign，无法响应请求");
	}

	// 4、将 nonce 记入缓存，防止重复使用，注意此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2  
	CacheUtil.set("nonce_" + nonce, "1", allowDisparity * 2);

	// 5、业务代码 ...

	// 6、返回
	return SaResult.ok();
}
```


### 9、使用 Sa-Token 框架完成 API 参数签名

接下来步入正题，使用 sa-token-sign 模块，方便的完成 API 签名创建、校验等步骤：
- 不限制请求的参数数量，方便组织业务需求代码。
- 自动补全 nonce、timestamp 参数，省时省力。
- 自动构建签名，并序列化参数为字符串。
- 一句代码完成 nonce、timestamp、sign 的校验，防伪造请求调用、防参数篡改、防重放攻击。


#### 9.1、引入依赖
请求发起端和接收端都需要引入：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-sign</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 整合 API 参数签名校验
implementation 'cn.dev33:sa-token-sign:${sa.top.version}'
```

:::



#### 9.2、配置秘钥
请求发起端和接收端需要配置一个相同的秘钥，在 `application.yml` 中配置：

``` yml
sa-token: 
    sign:
        # API 接口签名秘钥 （随便乱摁几个字母即可）
        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```

#### 9.3、请求发起端构建签名

``` java
// 请求地址
String url = "http://b.com/api/addMoney";

// 请求参数
Map<String, Object> paramMap = new LinkedHashMap<>();
paramMap.put("userId", 10001);
paramMap.put("money", 1000);
// 更多参数，不限制数量...

// 补全 timestamp、nonce、sign 参数，并序列化为 kv 字符串
String paramStr = SaSignUtil.addSignParamsAndJoin(paramMap);

// 将参数字符串拼接在请求地址后面
url += "?" + paramStr;

// 发送请求
String res = HttpUtil.request(url);

// 根据返回值做后续处理
System.out.println("server 端返回信息：" + res);
```


#### 9.4、请求接受端校验签名
``` java
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money) {

	// 1、校验请求中的签名
	SaSignUtil.checkRequest(SaHolder.getRequest());
	
	// 2、校验通过，处理业务
	System.out.println("userId=" + userId);
	System.out.println("money=" + money);
	
	// 3、返回
	return SaResult.ok();
}
```

如上代码便可简单方便的完成 API 接口参数签名校验，当请求端的秘钥不对，或者请求参数被篡改、请求被重放时，均无法通过 `SaSignUtil.checkRequest` 校验。

``` js
{
  "code": 500,
  "msg": "无效签名：9c3e3e98c7d543fb599766c9d3f3b5ff",
  "data": null
}
```


### 10、使用注解校验签名

`@SaCheckSign` 注解用于为一个接口提供签名校验，用于替代 `SaSignUtil.checkRequest(SaHolder.getRequest())`，示例如下：

``` java
// 校验全部参数：效果等同于  SaSignUtil.checkRequest(SaHolder.getRequest())
@SaCheckSign
@RequestMapping("test1")
public SaResult test1() {
	// code ...
	return SaResult.ok();
}

// 指定参与签名的参数有哪些：效果等同于 SaSignUtil.checkRequest(SaHolder.getRequest(), "id", "name");
@SaCheckSign(verifyParams = {"id", "name"})
@RequestMapping("test2")
public SaResult test2() {
	// code ...
	return SaResult.ok();
}

// 指定: 在多应用模式下，使用的 appid，详情见下
@SaCheckSign(appid = "xm-shop")
@RequestMapping("test3")
public SaResult test3() {
	// code ...
	return SaResult.ok();
}
```

> 注：上述注解与 `SaSignUtil.checkRequest` 默认从 **Query / Form** 参数中取参验签。若业务使用 **JSON Body** 传参，请参考下文第 12 节。


### 11、多应用模式

有时候我们可能需要同时与多个应用对接，每个应用都需要使用不同的秘钥：

首先在配置文件配置多个应用信息：
:::tabs
== yaml 风格

``` yaml
sa-token: 
    # API 签名配置 多应用模式
    sign-many:
        # 应用1
        xm-shop:
            secret-key: 0123456789abcdefg
            digest-algo: md5
        # 应用2
        xm-forum:
            secret-key: 0123456789hijklmnopq
            digest-algo: sha256
        # 应用3
        xm-video:
            secret-key: 12341234aaaaccccdddd
            digest-algo: sha512
```

== properties 风格

``` properties
# API 签名配置 多应用模式
# 应用1
sa-token.sign-many.xm-shop.secret-key=0123456789abcdefg
sa-token.sign-many.xm-shop.digest-algo=md5
# 应用2
sa-token.sign-many.xm-forum.secret-key=0123456789hijklmnopq
sa-token.sign-many.xm-forum.digest-algo=sha256
# 应用3
sa-token.sign-many.xm-video.secret-key=12341234aaaaccccdddd
sa-token.sign-many.xm-video.digest-algo=sha512
```

== 代码风格示例

``` java
@Autowired
public void configSaToken(SaTokenConfig config) {
    // API 签名配置 多应用模式
	// 应用1
	config.getSignMany().put("xm-shop", new SaSignConfig()
			.setSecretKey("0123456789abcdefg")   // 秘钥
			.setDigestAlgo("md5")   // 签名算法
	);
	// 应用2
	config.getSignMany().put("xm-forum", new SaSignConfig()
			.setSecretKey("0123456789hijklmnopq")
			.setDigestAlgo("sha256")
	);
	// 应用3
	config.getSignMany().put("xm-video", new SaSignConfig()
			.setSecretKey("12341234aaaaccccdddd")
			// 自定义签名算法示例
			.setDigestMethod(fullStr -> {
				return SaSecureUtil.sha384(fullStr);
			})
	);
}
```

:::



然后在签名时通过指定 appid 的方式获取对应的 SignTemplate 进行操作：

``` java
// 创建签名示例
String paramStr = SaSignMany.getSignTemplate("xm-shop").addSignParamsAndJoin(paramMap);

// 校验签名示例
SaSignMany.getSignTemplate("xm-shop").checkRequest(SaHolder.getRequest());
```


### 12、JSON Body 验签参考（社区示例）

官方默认的签名校验基于请求参数（Query / Form），无法直接读取 `Content-Type: application/json` 的请求体字段。

若你的接口使用 JSON Body 传参，可参考社区示例自行扩展（自定义 `@SaCheckSign` 处理器、缓存可重复读取的 Request Body 等）：

::: tip 社区示例
仓库：[https://github.com/ly-chn/sa-token-check-sign-from-body](https://github.com/ly-chn/sa-token-check-sign-from-body)  
:::

---

### Sa Token Doc New Docs Plugin Custom Serializer (`sa-token-doc-new/docs/plugin/custom-serializer.md`)

---
title: "Sa-Token 序列化插件扩展包"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,序列化插件扩展包,插件"
description: "Sa-Token 序列化插件扩展包：引入 sa-token-serializer-features，用天干地支、元素周期表、特殊符号或 Emoji 作为 Base64 元字符集，四种方案可切换。娱乐向、不建议上生产，仅通过替换 SaSerializerTemplate 探索 SaTokenDao 序列化边界。"
---

# 序列化插件扩展包
--- 

引入此插件可以为 Sa-Token 提供一些有意思的序列化方案。（娱乐向，不建议上生产，只为探索序列化插件更多边界能力）

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-serializer-features</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 自定义 String 序列化方案合集
implementation 'cn.dev33:sa-token-serializer-features:${sa.top.version}'
```

:::



#### 1、SaSerializerForBase64UseTianGan
base64 编码，采用 十大天干、十二地支 等64个中文字符作为元字符集

``` java
// 设置序列化方案: base64 编码，采用 十大天干、十二地支 等64个中文字符作为元字符集
@PostConstruct
public void rewriteComponent() {
	SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseTianGan());
}
```

效果图：

<img class="s-w" src="/big-file/doc/plugin/sa-custom-serializer-tiangan.png" alt="自定义序列化：天干地支编码示例" />


#### 2、SaSerializerForBase64UsePeriodicTable
base64 编码，采用 元素周期表 前六十四位作为元字符集

``` java
// 设置序列化方案: base64 编码，采用 元素周期表 前六十四位作为元字符集
@PostConstruct
public void rewriteComponent() {
	SaManager.setSaSerializerTemplate(new SaSerializerForBase64UsePeriodicTable());
}
```

效果图：

<img class="s-w" src="/big-file/doc/plugin/sa-custom-serializer-yszqb.png" alt="自定义序列化：三十六字母编码示例" />



#### 3、SaSerializerForBase64UseSpecialSymbols
base64 编码，采用64个特殊符号作为元字符集

``` java
// 设置序列化方案: base64 编码，采用64个特殊符号作为元字符集
@PostConstruct
public void rewriteComponent() {
	SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseSpecialSymbols());
}
```

效果图：

<img class="s-w" src="/big-file/doc/plugin/sa-custom-serializer-tsfh.png" alt="自定义序列化：特殊符号编码示例" />


#### 4、SaSerializerForBase64UseEmoji
base64 编码，采用 64 个 Emoji 小黄脸作为元字符集，无填充字符

``` java
// 设置序列化方案: base64 编码，采用 64 个 Emoji 小黄脸作为元字符集，无填充字符
@PostConstruct
public void rewriteComponent() {
	SaManager.setSaSerializerTemplate(new SaSerializerForBase64UseEmoji());
}
```

效果图：

<img class="s-w" src="/big-file/doc/plugin/sa-custom-serializer-emoji.png" alt="自定义序列化：Emoji 编码示例（输入）" />

<img class="s-w" src="/big-file/doc/plugin/sa-custom-serializer-emoji2.png" alt="自定义序列化：Emoji 编码示例（输出）" />

---

### Sa Token Doc New Docs Plugin Dao Extend (`sa-token-doc-new/docs/plugin/dao-extend.md`)

---
title: "Sa-Token 缓存层扩展"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,缓存层扩展,插件"
description: "Sa-Token 缓存层扩展：权限数据经 SaTokenDao 抽象持久化。官方提供内存默认、Redis Template、Redisson、Caffeine、Hutool Timed-Cache、Redisx 及 Alone 分连接；也可自行实现接口对接 MongoDB 等中间件。升级请先清空旧 Redis 缓存。"
---

# 缓存层扩展
--- 

对于权限框架来讲，最容易碰到的扩展点便是数据存储方式，为了方便对接不同的缓存中间件，Sa-Token将所有数据持久化操作抽象到SaTokenDao接口，
开发者要对接不同的平台只需要实现此接口即可，接口签名：[SaTokenDao.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/dao/SaTokenDao.java)
 
框架已提供的集成包包括：

- 默认方式：储存在内存中，位于core核心包。
- sa-token-redis-template：Redis Template 集成包。
- sa-token-redis-template-jdk-serializer：Redis 集成包，使用 jdk 默认序列化方式。
- sa-token-hutool-timed-cache：集成 hutool 框架的 Timed-Cache 缓存方案（基于内存）。
- sa-token-caffeine：集成 Caffeine 缓存方案（基于内存）。
- sa-token-redisson：集成 Redisson 客户端。
- sa-token-redisson-spring-boot-starter：集成 Redisson 客户端 - SpringBoot 自动配置包 。
- sa-token-alone-redisson：独立 Redisson 连接，权限缓存与业务缓存分离。
- sa-token-redisx：Redisx 集成包。 

`SaTokenDaoForRedisson` 默认使用 `StringCodec`。从旧版本升级时，请先清空 Redis 中的 Sa-Token 缓存，或构造时传入 `Kryo5Codec` 以兼容旧数据，详见：[集成 Redis - 集成 Redisson](/up/integ-redis#5、集成-Redisson)。


有关 Redis 集成，详细参考：[集成Redis](/up/integ-redis)，更多存储方式欢迎提交PR 


**扩展：集成 MongoDB**

- [集成 MongoDB 参考一](/up/integ-spring-mongod-1)
- [集成 MongoDB 参考二](/up/integ-spring-mongod-2)

---

### Sa Token Doc New Docs Plugin Dubbo Extend (`sa-token-doc-new/docs/plugin/dubbo-extend.md`)

---
title: "Sa-Token 和 Dubbo 集成"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,和 Dubbo 集成,插件"
description: "Sa-Token 整合 Dubbo：两端均引入 sa-token-dubbo（Dubbo3 用 sa-token-dubbo3），解决被调用端「无效上下文」并双向透传 Token。被调用端可安全使用 StpUtil；可开 check-same-token。限制：SaStorage 不通，setHeader 无效。"
---

# 和 Dubbo 集成 

本插件的作用是让 Sa-Token 和 Dubbo 做一个整合。 

--- 

### 先说说要解决的问题 

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

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

- 上下文环境的丢失。
- 上下文参数的丢失。 

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

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

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


### 引入插件 

在项目已经引入 Dubbo 的基础上，继续添加依赖（Consumer 端和 Provider 端都需要引入）：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-dubbo</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
注：如果使用的是 dubbo3，只需要将 `sa-token-dubbo` 修改为 `sa-token-dubbo3` 即可。

== Gradle 方式

``` gradle
// Sa-Token 整合 Dubbo
implementation 'cn.dev33:sa-token-dubbo:${sa.top.version}'
```
注：如果使用的是 dubbo3，只需要将 `sa-token-dubbo` 修改为 `sa-token-dubbo3` 即可。

:::





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

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

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

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

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


### RPC调用鉴权

在之前的 [Same-Token](/micro/same-token) 章节，我们演示了基于 Feign 的 RPC 调用鉴权，下面我们演示一下在 Dubbo 中如何集成 Same-Token 模块。

其实思路和 Feign 模式一致，在 [ 调用端 ] 追加 Same-Token 参数，在 [ 被调用端 ] 校验这个 Same-Token 参数：

- 校验通过：调用成功。
- 校验不通过：调用失败，抛出异常。

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

##### 方式一、使用配置（推荐）

直接在 `application.yml` 配置即可：

:::tabs
== yaml 风格

``` yaml
sa-token: 
	# 打开 RPC 调用鉴权 
	check-same-token: true
```

== properties 风格

``` properties
# 打开 RPC 调用鉴权 
sa-token.check-same-token=true
```

:::



##### 方式二、自建 Dubbo 过滤器校验
此方式略显繁琐，好处是除了Same-Token，我们还可以添加其它自定义参数 (attachment)。

1、在 [ 调用端 ] 的 `\resources\META-INF\dubbo\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件
``` html
dubboConsumerFilter=com.pj.DubboConsumerFilter
```

新建 `DubboConsumerFilter.java` 过滤器

``` java
package com.pj;

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;

import cn.dev33.satoken.same.SaSameUtil;

/**
 * Sa-Token 整合 Dubbo Consumer端过滤器 
 */
@Activate(group = {CommonConstants.CONSUMER}, order = -10000)
public class DubboConsumerFilter implements Filter {

	@Override
	public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
		
		// 追加 Same-Token 参数 
		RpcContext.getContext().setAttachment(SaSameUtil.SAME_TOKEN, SaSameUtil.getToken());
		
		// 如果有其他自定义附加数据，如租户
		// RpcContext.getContext().setAttachment("tenantContext", tenantContext);
		
		// 开始调用
		return invoker.invoke(invocation);
	}

}
```


2、在 [ 被调用端 ] 的 `\resources\META-INF\dubbo\` 目录新建 `org.apache.dubbo.rpc.Filter` 文件
``` html
dubboProviderFilter=com.pj.DubboProviderFilter
```

新建 `DubboProviderFilter.java` 过滤器

``` java
package com.pj;

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;

import cn.dev33.satoken.same.SaSameUtil;

/**
 * Sa-Token 整合 Dubbo Provider端过滤器 
 */
@Activate(group = {CommonConstants.PROVIDER}, order = -10000)
public class DubboProviderFilter implements Filter {

	@Override
	public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
		
		// 取出 Same-Token 进行校验 
		String sameToken = invocation.getAttachment(SaSameUtil.SAME_TOKEN);
		SaSameUtil.checkToken(sameToken);
		
		// 取出其他自定义附加数据
		// TenantContext tenantContext = invocation.getAttachment("tenantContext");
		
		// 开始调用
		return invoker.invoke(invocation);
	}

}
```


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

---

### Sa Token Doc New Docs Plugin Freemarker Extend (`sa-token-doc-new/docs/plugin/freemarker-extend.md`)

---
title: "Sa-Token 和 Freemarker 集成"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,和 Freemarker 集成,插件"
description: "Sa-Token 整合 Freemarker：引入 sa-token-freemarker，注入 SaTokenTemplateModel 后可在 .ftl 用 sa.login、hasRole、hasPermission 标签判断登录与权限。并注入 stp，模板内可调 StpLogic 与 SaSession。"
---

# Freemarker 自定义标签 

本插件的作用是让我们可以在 Freemarker 页面中使用 Sa-Token 自定义标签以及相关API。

--- 

### 1、引入依赖 
首先我们确保项目已经引入 Freemarker 依赖，然后在此基础上继续添加：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-freemarker</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// 在 Freemarker 页面中使用 Sa-Token 自定义标签
implementation 'cn.dev33:sa-token-freemarker:${sa.top.version}'
```

:::




### 2、注入 Sa-Token Freemarker 标签模板模型 对象 
在 SaTokenConfigure 配置类中增加配置 
``` java
@Configuration
public class SaTokenConfigure {

	@Autowired
	FreeMarkerConfigurer configurer;

	/**
	 * 注入 Sa-Token Freemarker 标签模板模型 对象
	 */
	@PostConstruct
	public void setSaTokenTemplateModel() throws TemplateModelException {

		// 注入 Sa-Token Freemarker 标签模板模型，使之可以在 xxx.ftl 文件中使用 sa 标签，
		// 例如：<#if sa.login()>...</#if>
		configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel());

		// 注入 Sa-Token Freemarker 全局对象，使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法，
		// 例如：<span>${stp.getSession().get('name')}</span>
		configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic);
	}

}
```


### 3、使用自定义标签
然后我们就可以愉快的使用在 Freemarker 页面中使用 Sa-Token 自定义标签了 

##### 3.1、登录判断 
``` html
<h2>标签方言测试页面</h2>
<p>
	登录之后才能显示：
	<@sa.login>value</@sa.login>
</p>
<p>
	不登录才能显示：
	<@sa.notLogin>value</@sa.notLogin>
</p>
```

##### 3.2、角色判断
``` html
<p>
	具有角色 admin 才能显示：
	<@sa.hasRole value="admin">value</@sa.hasRole>
</p>
<p>
	同时具备多个角色才能显示：
	<@sa.hasRoleAnd value="admin, ceo, cto">value</@sa.hasRoleAnd>
</p>
<p>
	只要具有其中一个角色就能显示：
	<@sa.hasRoleOr value="admin, ceo, cto">value</@sa.hasRoleOr>
</p>
<p>
	不具有角色 admin 才能显示：
	<@sa.notRole value="admin">value</@sa.notRole>
</p>
```

##### 3.3、权限判断
``` html
<p>
	具有权限 user-add 才能显示：
	<@sa.hasPermission value="user-add">value</@sa.hasPermission>
</p>
<p>
	同时具备多个权限才能显示：
	<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value</@sa.hasPermissionAnd>
</p>
<p>
	只要具有其中一个权限就能显示：
	<@sa.hasPermissionOr value="user-add, user-delete, user-get">value</@sa.hasPermissionOr>
</p>
<p>
	不具有权限 user-add 才能显示：
	<@sa.notPermission value="user-add">value</@sa.notPermission>
</p>
```


### 4、调用 Sa-Token 相关API  

以上的自定义标签，可以满足我们大多数场景下的权限判断，然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API :

 
``` html
<p>
	从SaSession中取值：
	<#if stp.isLogin()>
		<span>${stp.getSession().get('name')}</span>
	</#if>
</p>
```

---

### Sa Token Doc New Docs Plugin Grpc Extend (`sa-token-doc-new/docs/plugin/grpc-extend.md`)

---
title: "Sa-Token 和 gRPC 集成"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,和 gRPC 集成,插件"
description: "Sa-Token 整合 gRPC：引入 sa-token-grpc（调用端与被调用端都要加）。RPC 链路透传 Token，被调用端恢复上下文后可安全调用 StpUtil；登录态双向回传，并支持 Same-Token 校验。限制：SaStorage 两端不通，被调用端 setHeader/setStatus 无效。"
---

# 和 grpc 集成

本插件的作用是让 Sa-Token 和 grpc 做一个整合。

--- 

和dubbo插件一样，解决了以下问题

1. 在 [ 被调用端 ] 安全的调用 Sa-Token 相关 API。
2. 在 [ 调用端 ] 登录的会话，其登录状态可以自动传递到 [ 被调用端 ] ；在 [ 被调用端 ] 登录的会话，其登录状态可以自动回传到 [ 调用端 ]
3. Same-Token 安全校验

---
和dubbo插件一样，具有以下限制：

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

### 引入插件
需要springboot环境，添加依赖（调用端和被调用端都需要引入）：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-grpc</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 整合 grpc
implementation 'cn.dev33:sa-token-grpc:${sa.top.version}'
```

:::



---
### 开启 Same-Token 校验：
直接在 `application.yml` 配置即可：

:::tabs
== yaml 风格

``` yaml
sa-token: 
	# 打开 RPC 调用鉴权 
	check-same-token: true
```

== properties 风格

``` properties
# 打开 RPC 调用鉴权 
sa-token.check-same-token=true
```

:::

---

### Sa Token Doc New Docs Plugin Http Extend (`sa-token-doc-new/docs/plugin/http-extend.md`)

---
title: "Sa-Token HTTP 请求扩展"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,HTTP 请求扩展,插件"
description: "Sa-Token HTTP 请求扩展：SSO 模式三、单点注销、消息推送需走 SaHttpTemplate。可选 sa-token-forest、okhttps、rest-template、rest-client，SPI 自动注入。未配置会抛错误码 30010；classpath 只应有一个 HTTP 插件。"
---

# HTTP 请求扩展

---

## HTTP 请求插件大全

Sa-Token 在 SSO 模式三（`isHttp`）、单点注销、消息推送等场景需要发起 HTTP 请求。框架将 HTTP 调用逻辑抽象到 `SaHttpTemplate` 接口，
开发者只需引入对应的 HTTP 插件依赖，框架会通过 SPI 机制自动完成注入，接口签名：[SaHttpTemplate.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/http/SaHttpTemplate.java)

框架已提供的 HTTP 请求插件包括：

- **sa-token-forest**：整合 Forest，适用于 Spring / 非 Spring 环境（SSO 示例默认使用）。
- **sa-token-okhttps**：整合 OkHttps，适用于 Spring / 非 Spring 环境。
- **sa-token-rest-template**：整合 Spring `RestTemplate`，适用于 Spring Boot 2 / 3 / 4 环境。
- **sa-token-rest-client**：整合 Spring `RestClient`，适用于 Spring Boot 3.2+ / Spring Framework 6.1+ 环境。

> 默认实现：
>
> - `SaHttpTemplateDefaultImpl` 未实现 HTTP 调用，未引入插件时会抛出「本地系统没有配置 http 请求处理器」（错误码 30010）。
> - 同一 classpath 只应存在一个 HTTP 插件；多插件并存时后加载的会覆盖 `SaHttpTemplate`。

:::tabs
== Forest

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-forest</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-forest:${sa.top.version}'`

== OkHttps

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-okhttps</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-okhttps:${sa.top.version}'`

== RestTemplate

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-rest-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-rest-template:${sa.top.version}'`

== RestClient

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-rest-client</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-rest-client:${sa.top.version}'`

:::



## 选型建议

| 运行环境 | 推荐插件 |
| :-------- | :-------- |
| 非 Spring / Solon 等 | `sa-token-forest` 或 `sa-token-okhttps` |
| Spring Boot 2，或 Boot 3.0–3.1 | `sa-token-rest-template` 或 `sa-token-forest` |
| Spring Boot 3.2+ / 4 | **优先** `sa-token-rest-client`，或 `sa-token-rest-template`（兼容旧代码） |

- `RestTemplate` 在 Spring Boot 4 中已标记为 deprecated，但仍可使用。
- `RestClient` 需要 Spring Framework 6.1+，无法在 Spring Boot 2 或 Boot 3.0–3.1 中使用。


## 使用场景

- **SSO 模式三**：ticket 校验、单点注销、消息推送（参考 [模式三](/sso/sso-type3)、[消息推送](/sso/message-push)）
- **SSO-Server 模式三**：Server 端需配置 HTTP 请求处理器（参考 [搭建 SSO-Server](/sso/sso-server)）


## 自定义 Http 请求处理器

**方式一（推荐）**：引入官方插件，SPI 自动注册。

**方式二**：手动注册（参考 [插件开发指南 - 更改全局组件](/fun/plugin-dev#方式2：更改全局组件实现)）：

``` java
// 使用官方插件实现
SaManager.setSaHttpTemplate(new SaHttpTemplateForForest());

// 或自定义实现 SaHttpTemplate 接口
SaManager.setSaHttpTemplate(new SaHttpTemplate() {
	@Override
	public String get(String url) {
		// ...
	}
	@Override
	public String postByFormData(String url, Map<String, Object> params) {
		// ...
	}
});
```

在 Spring Boot 环境中，也可通过注入 `SaHttpTemplate` Bean 覆盖默认实现（参考 [全局类、方法](/more/common-action)）。


## 错误码 30010

若运行时抛出「本地系统没有配置 http 请求处理器」，说明当前 classpath 未引入任何 HTTP 插件，且未手动设置 `SaHttpTemplate`。
请按上文选型建议引入一个插件，或参考 [异常码表 - 30010](/fun/exception-code#sa-token-sso-单点登录相关：) 排查。

---

### Sa Token Doc New Docs Plugin Json Extend (`sa-token-doc-new/docs/plugin/json-extend.md`)

---
title: "Sa-Token JSON 序列化扩展"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,JSON 序列化扩展,插件"
description: "Sa-Token JSON 序列化扩展：Session 存 Redis 时经 SaJsonTemplate 转换。可选 jackson、jackson3、fastjson、fastjson2、Fory、Snack3/4。业务实体须进 JSON 类型白名单（SaJsonType），防止多态反序列化风险。"
---

# JSON 序列化扩展

--- 

## JSON 序列化插件大全

Sa-Token 在 Session 存储、Redis 缓存等场景下需要对对象进行 JSON 序列化与反序列化。框架将 JSON 转换逻辑抽象到 `SaJsonTemplate` 接口，
开发者只需引入对应的 JSON 插件依赖，框架会通过 SPI 机制自动完成注入，接口签名：[SaJsonTemplate.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/json/SaJsonTemplate.java)
 
框架已提供的 JSON 序列化插件包括：

- **sa-token-jackson**：集成 Jackson（com.fasterxml.jackson），适用于 SpringBoot2/3 等环境。
- **sa-token-jackson3**：集成 Jackson 3（tools.jackson.core），适用于 SpringBoot4、Java 17+ 等环境。
- **sa-token-fastjson**：集成 Fastjson。
- **sa-token-fastjson2**：集成 Fastjson2。
- **sa-token-fory-json**：集成 [Apache Fory JSON](https://fory.apache.org/docs/json/)（高性能 JSON 编解码，不在 JSON 中写入类型信息，用法同 Fastjson2）。
- **sa-token-snack3**：集成 Snack3。
- **sa-token-snack4**：集成 Snack4。

> 默认 JSON 组件：
> 
> - `sa-token-spring-boot-starter` 会自动引入 `sa-token-jackson` 作为默认 JSON 方案。
> - `sa-token-spring-boot3-starter` 会自动引入 `sa-token-jackson`  作为默认 JSON 方案。
> - `sa-token-spring-boot4-starter` 会自动引入 `sa-token-jackson3`  作为默认 JSON 方案。
> 
> 如需更换为其它 JSON 框架，引入对应插件依赖即可。

:::tabs
== Jackson

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-jackson</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-jackson:${sa.top.version}'`

== Jackson3

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-jackson3</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-jackson3:${sa.top.version}'`

== Fastjson

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-fastjson</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-fastjson:${sa.top.version}'`

== Fastjson2

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-fastjson2</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-fastjson2:${sa.top.version}'`

== Fory JSON

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-fory-json</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-fory-json:${sa.top.version}'`

== Snack3

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-snack3</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-snack3:${sa.top.version}'`

== Snack4

``` xml

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-snack4</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```
Gradle 参考：`implementation 'cn.dev33:sa-token-snack4:${sa.top.version}'`

:::



## JSON 全局类型白名单机制

### 报错示例

如果你在往 SaSession 上存储实体类字段，或从 Redis 反序列化 Session / 对象时报错：

```
无法反序列化的类型：com.pj.model.SysUser，请先将其注册到 JSON 全局类型白名单
```

### 这是因为

Sa-Token 在 **集成 Redis** 等持久化场景下，会把 `SaSession` 以及 `SaTokenDao.getObject()` / `setObject()` 中的对象先做 **JSON 序列化** 再写入 Redis。而后在使用数据时再进行反序列化。

为防止攻击者篡改 Redis 中的类型标记、实例化 classpath 上任意类（多态反序列化 RCE），或开发者使用 `sa-token-json` 组件将不安全的前端提交内容进行反序列化，框架只允许 **白名单内的类型** 参与这类多态反序列化。  

白名单内置常见 JDK 值类型，以及已实现 `SaJsonType` 的框架 Model 等；**你的业务实体类默认不在白名单中**，因此反序列化时会抛出上述异常。

> 说明：`sa-token-fastjson` / `sa-token-fastjson2` / `sa-token-fory-json` / `sa-token-snack3` 默认不在 JSON 中写入类型信息，一般不会出现此报错；业务对象请通过 `SaSession.getModel(key, Class)` 或 `jsonToObject(json, Class)` 指定类型。

白名单由 [SaJsonStrategy](https://gitee.com/dromara/sa-token/blob/master/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaJsonStrategy.java) 统一管理。**首次** 构建 JSON 插件（如 `SaJsonTemplateForJackson`）时会完成初始化；初始化之后不可再注册类型。

### 下面是注册 JSON 全局类型白名单的几种方式

#### 1、实体类实现 `SaJsonType`（推荐）

业务 Model 实现标记接口 `SaJsonType` 即可加入白名单，无需额外配置：

``` java
public class SysUser implements SaJsonType {
	// ...
}
```

#### 2、启动前调用 `registerAllowType`

在 **JSON 插件完成初始化之前** 注册（Spring Boot 请在 `main` 方法里、`SpringApplication.run` 之前；Solon 请在 `Solon.start` 之前）：

``` java
import cn.dev33.satoken.strategy.SaJsonStrategy;

public static void main(String[] args) {
	// 在项目启动前，将所有需要反序列化的 Bean Class 进行注册
	SaJsonStrategy.instance.registerAllowType(SysUser.class);

	SpringApplication.run(Application.class, args);
}
```

#### 3、通过 SPI 文件批量声明

在 `resources/META-INF/satoken/sa-json-type.list` 中按行写入完整类名（`#` 开头为注释）：

``` properties
# 允许参与多态 JSON 反序列化的业务类型
com.pj.model.SysUser
com.pj.model.SysRole
```

---

有关 Redis 集成与序列化配置，详细参考：[集成 Redis](/up/integ-redis)

---

### Sa Token Doc New Docs Plugin Jwt Extend (`sa-token-doc-new/docs/plugin/jwt-extend.md`)

---
title: "和 jwt 集成"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,整合 Sa-Token JWT,插件,JWT"

description: "Sa-Token 整合 JWT：引入 sa-token-jwt 并配置 jwt-secret-key，按需注入 Simple/Mixin/Stateless 三种 StpLogic。可无状态传递登录态，登录时 setExtra 扩展参数；需注意 hutool-jwt 版本及踢人、Session 等能力因模式而异。"
---

# 和 jwt 集成 

本插件的作用是让 Sa-Token 和 jwt 做一个整合。 

--- 

### 1、引入依赖 
首先在项目已经引入 Sa-Token 的基础上，继续添加：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-jwt</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 整合 jwt
implementation 'cn.dev33:sa-token-jwt:${sa.top.version}'
```

:::



::: warning 版本兼容性
1. 注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本，保险起见：你的项目中要么不引入 hutool，要么引入版本 >= 5.7.14 的 hutool 版本。
2. hutool 5.8.13 和 5.8.14 版本下会出现类型转换问题，[关联issue](https://gitee.com/dromara/sa-token/issues/I6L429)。
:::


### 2、配置秘钥
在 `application.yml` 配置文件中配置 jwt 生成秘钥：

:::tabs
== yaml 风格

``` yaml
sa-token:
	# jwt秘钥 
	jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
```

== properties 风格

``` properties
# jwt秘钥 
sa-token.jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
```

:::


注：为了安全起见请不要直接复制官网示例这个字符串（随便按几个字符就好了）


### 3、注入jwt实现
根据不同的整合规则，插件提供了三种不同的模式，你需要 **选择其中一种** 注入到你的项目中 

:::tabs
== Simple 简单模式

Simple 模式：Token 风格替换
``` java
@Configuration
public class SaTokenConfigure {
    // Sa-Token 整合 jwt (Simple 简单模式)
	@Bean
    public StpLogic getStpLogicJwt() {
    	return new StpLogicJwtForSimple();
    }
}
```

== Mixin 混入模式

Mixin 模式：混入部分逻辑
``` java
@Configuration
public class SaTokenConfigure {
    // Sa-Token 整合 jwt (Mixin 混入模式)
	@Bean
    public StpLogic getStpLogicJwt() {
    	return new StpLogicJwtForMixin();
    }
}
```

== Stateless 无状态模式

Stateless 模式：服务器完全无状态
``` java
@Configuration
public class SaTokenConfigure {
    // Sa-Token 整合 jwt (Stateless 无状态模式)
	@Bean
    public StpLogic getStpLogicJwt() {
    	return new StpLogicJwtForStateless();
    }
}
```

:::


### 4、开始使用
然后我们就可以像之前一样使用 Sa-Token 了 
``` java
/**
 * 登录测试 
 */
@RestController
@RequestMapping("/acc/")
public class LoginController {

    // 测试登录
    @RequestMapping("login")
    public SaResult login() {
		StpUtil.login(10001);
        return SaResult.ok("登录成功");
    }

    // 查询登录状态
    @RequestMapping("isLogin")
    public SaResult isLogin() {
        return SaResult.ok("是否登录：" + StpUtil.isLogin());
    }

    // 测试注销
    @RequestMapping("logout")
    public SaResult logout() {
        StpUtil.logout();
        return SaResult.ok();
    }

}
```

访问上述接口，观察Token生成的样式
``` java
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbklkIjoiMTAwMDEiLCJybiI6IjZYYzgySzBHVWV3Uk5NTTl1dFdjbnpFZFZHTVNYd3JOIn0.F_7fbHsFsDZmckHlGDaBuwDotZwAjZ0HB14DRujQfOQ
```


### 5、不同模式策略对比

注入不同模式会让框架具有不同的行为策略，以下是三种模式的差异点（为方便叙述，以下比较以同时引入 jwt 与 Redis 作为前提）：

| 功能点						| Simple 简单模式		| Mixin 混入模式			| Stateless 无状态模式	|
| :--------					| :--------		| :--------			| :--------			|
| Token风格					| jwt风格		| jwt风格			| jwt风格			|
| 登录数据存储				| Redis中存储		| Token中存储			| Token中存储			|
| Session存储				| Redis中存储		| Redis中存储			| 无Session			|
| 注销下线					| 前后端双清数据	| 前后端双清数据		| 前端清除数据		|
| 踢人下线API				| 支持			| 不支持				| 不支持				|
| 顶人下线API				| 支持			| 不支持				| 不支持				|
| 登录认证					| 支持			| 支持				| 支持				|
| 角色认证					| 支持			| 支持				| 支持				|
| 权限认证					| 支持			| 支持				| 支持				|
| timeout 有效期				| 支持			| 支持				| 支持				|
| active-timeout 有效期		| 支持			| 支持				| 不支持				|
| id反查Token				| 支持			| 支持				| 不支持				|
| 会话管理					| 支持			| 部分支持			| 不支持				|
| 注解鉴权					| 支持			| 支持				| 支持				|
| 路由拦截鉴权				| 支持			| 支持				| 支持				|
| 账号封禁					| 支持			| 支持				| 不支持				|
| 身份切换					| 支持			| 支持				| 支持				|
| 二级认证					| 支持			| 支持				| 支持				|
| 模式总结					| Token风格替换	| jwt 与 Redis 逻辑混合	| 完全舍弃Redis，只用jwt		|



### 6、扩展参数
你可以通过以下方式在登录时注入扩展参数：

``` java
// 登录10001账号，并为生成的 Token 追加扩展参数name
StpUtil.login(10001, new SaLoginParameter().setExtra("name", "zhangsan"));

// 连缀写法追加多个
StpUtil.login(10001, new SaLoginParameter()
				.setExtra("name", "zhangsan")
				.setExtra("age", 18)
				.setExtra("role", "超级管理员"));

// 获取扩展参数 
String name = StpUtil.getExtra("name");

// 获取任意 Token 的扩展参数 
String name = StpUtil.getExtra("tokenValue", "name");
```



### 7、在多账户模式中集成 jwt
sa-token-jwt 插件默认只为 `StpUtil` 注入 `StpLogicJwtFoxXxx` 实现，自定义的 `StpUserUtil` 是不会自动注入的，我们需要帮其手动注入：

``` java
/**
 * 为 StpUserUtil 注入 StpLogicJwt 实现 
 */
@PostConstruct
public void setUserStpLogic() {
	StpUserUtil.setStpLogic(new StpLogicJwtForSimple(StpUserUtil.TYPE));
}
```



### 8、自定义 SaJwtUtil 生成 token 的算法 

如果需要自定义生成 token 的算法（例如更换sign方式），直接重写 SaJwtTemplate 对象即可：

``` java
/**
 * 自定义 SaJwtUtil 生成 token 的算法 
 */
@PostConstruct
public void setSaJwtTemplate() {
	SaJwtUtil.setSaJwtTemplate(new SaJwtTemplate() {
		@Override
		public String generateToken(JWT jwt, String keyt) {
			System.out.println("------ 自定义了 token 生成算法");
			return super.generateToken(jwt, keyt);
		}
	});
}
```


### 9、注意点

##### 1、使用 jwt-simple 模式后，is-share=false 恒等于 false。

`is-share=true` 的意思是每次登录都产生一样的 token，这种策略和 [ 为每个 token 单独设定 setExtra 数据 ] 不兼容的，
为保证正确设定 Extra 数据，当使用 `jwt-simple` 模式后，`is-share` 配置项 恒等于 `false`。


##### 2、使用 jwt-mixin 模式后，is-concurrent 必须为 true。

`is-concurrent=false` 代表每次登录都把旧登录顶下线，但是 jwt-mixin 模式登录的 token 并不会记录在持久库数据中，
技术上来讲无法将其踢下线，所以此时顶人下线和踢人下线等 API 都属于不可用状态，所以此时 `is-concurrent` 配置项必须配置为 `true`。


##### 3、使用 jwt-mixin 模式后，max-try-times 恒等于 -1。

为防止框架错误判断 token 唯一性，当使用 jwt-mixin 模式后，`max-try-times` 恒等于 -1。

---

### Sa Token Doc New Docs Plugin Quick Login (`sa-token-doc-new/docs/plugin/quick-login.md`)

---
title: "Sa-Token Quick-Login快速登录插件"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,Quick-Login快速登录插件,插件"
description: "Sa-Token Quick-Login 快速登录：引入 sa-token-quick-login 即可为零代码监控页等轻量系统注入登录页与全局拦截。默认账号 sa/123456，支持 Http Basic、yml 配账号密码与拦截路径；也可打独立 jar 给静态站点加登录。页面不可定制，不适合正式登录模块。"
---

# Sa-Token-Quick-Login 快速登录认证
--- 


### 解决什么问题

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

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

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

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

细细想来，完成这个功能你需要：

1. 编写前端登录页面，手写各种表单样式
2. 寻找合适的ajax类库，`jQuery`？`Axios`？还是直接前后台不分离？
3. 寻找合适的模板引擎，比如`jsp`、`Thymeleaf`、`FreeMarker`、`Velocity`……选哪个呢？
4. 处理后台各种拦截认证逻辑，前后台接口对接
5. 你可能还会遇到令人头痛欲裂的模板引擎中`ContextPath`处理
6. ……

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

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

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


### 适用场景

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

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

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


### 集成步骤
首先我们需要创建一个SpringBoot的demo项目，比如：`sa-token-demo-quick-login`

##### 1、添加pom依赖

:::tabs
== Maven 方式

``` xml

<dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-spring-boot-starter</artifactId>
        <version>${sa.top.version}</version>
</dependency>

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-quick-login</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 启动依赖
implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'
// Sa-Token-Quick-Login 插件
implementation 'cn.dev33:sa-token-quick-login:${sa.top.version}'
```

:::




##### 2、启动类
``` java
@SpringBootApplication
public class SaTokenQuickDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(SaTokenQuickDemoApplication.class, args);
		
		System.out.println("\n------ 启动成功 ------");
		System.out.println("name: " + SaQuickManager.getConfig().getName());
		System.out.println("pwd:  " + SaQuickManager.getConfig().getPwd());
	}
}
```

##### 3、新建测试Controller
``` java
/**
 * 测试专用Controller 
 */
@RestController
public class TestController {
	// 浏览器访问测试： http://localhost:8081
	@RequestMapping({"/", "/index"})
	public String index() {
		String str = "<br />"
				+ "<h1 style='text-align: center;'>资源页 （登录后才可进入本页面） </h1>"
				+ "<hr/>"
				+ "<p style='text-align: center;'> Sa-Token " + SaTokenConsts.VERSION_NO + " </p>";
		return str;
	}
}
```

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

<img class="s-w" src="/big-file/doc/plugin/sa-quick-login.png" alt="Quick-Login 插件登录页" />

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

<img class="s-w" src="/big-file/doc/plugin/sa-quick-login-index.png" alt="Quick-Login 插件首页" />

也可以通过 Http Basic 的方式直接进行认证 (一般需要在专门的 API 测试工具下才能正常测试，浏览器会自动忽略@之前的信息)

``` url
http://sa:123456@localhost:8081/
```


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

:::tabs
== yaml 风格

``` yaml
# Sa-Token-Quick-Login 配置
sa: 
	# 登录账号
	name: sa
	# 登录密码
	pwd: 123456
	# 是否自动随机生成账号密码 (此项为true时, name与pwd失效)
	auto: false
	# 是否开启全局认证(关闭后将不再强行拦截) 
	auth: true
	# 登录页标题
	title: Sa-Token 登录
	# 是否显示底部版权信息 
	copr: true
    # 指定拦截路径 
    # include: /**
    # 指定排除路径
    # exclude: /1.jpg
```

== properties 风格

``` properties
####### Sa-Token-Quick-Login 配置 #######
# 登录账号
sa.name=sa
# 登录密码
sa.pwd=123456
# 是否自动随机生成账号密码 (此项为true时, name与pwd失效)
sa.auto=false
# 是否开启全局认证(关闭后将不再强行拦截) 
sa.auth=true
# 登录页标题
sa.title=Sa-Token 登录
# 是否显示底部版权信息 
sa.copr=true
# 指定拦截路径 
# sa.include=/**
# 指定排除路径
# sa.exclude=/1.jpg
```

:::



<br>

**注：**示例源码在`/sa-token-demo/sa-token-demo-quick-login`目录下，可结合源码查看学习。[Sa-Token 集成示例大全下载](/more/download-demos) 。



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

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


##### 打包步骤



首先放上懒人链接：[sa-quick-dist.jar](https://pan.quark.cn/s/04fd34a24928)，不想手动操作的同学可以直接点此链接下载打包后的jar文件 

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

``` xml

<modules>
	<module>sa-token-core</module>
	<module>sa-token-starter</module>
	<module>sa-token-plugin</module>
	<module>sa-token-demo\sa-token-demo-quick-login</module>
</modules>
```

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

``` cmd
mvn clean package
```

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

``` cmd
sa-token-demo-quick-login-0.0.1-SNAPSHOT.jar
```

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

``` cmd
java -jar sa-quick-dist.jar
```

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

<img class="s-w" src="/big-file/doc/plugin/sa-quick-start.png" alt="Quick-Login 插件快速开始界面" />

如果可以进入登录界面，则代表打包运行成功 <br>
当然仅仅运行成功还不够，下面我们演示一下如何使用这个jar包进行静态网站部署


### 所有功能示例

##### Case 1. 指定静态资源路径
``` cmd
java -jar sa-quick-dist.jar --sa.dir file:E:\www
```
使用dir参数指定`E:\www`目录作为资源目录进行部署 (现在我们可以通过浏览器访问`E:\www`目录下的文件了！)

##### Case 2. 指定登录名与密码
``` cmd
java -jar sa-quick-dist.jar --sa.name=zhang --sa.pwd=zhang123
```
现在，默认的账号`sa/123456`将被废弃，而是使用`zhang/zhang123`进行账号校验

##### Case 3. 指定其自动生成账号密码
``` cmd
java -jar sa-quick-dist.jar --sa.auto=true
```
每次启动时随机生成账号密码（会在启动成功时打印到控制台上）

##### Case 4. 指定登录页的标题
``` cmd
java -jar sa-quick-dist.jar --sa.title="XXX 系统登录"
```

##### Case 5. 关闭账号校验，仅作为静态资源部署使用
``` cmd
java -jar sa-quick-dist.jar --sa.auth=false
```

##### Case 6. 指定启动端口（默认8080）
``` cmd
java -jar sa-quick-dist.jar --server.port=80 
```

注：所有参数可组合使用


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

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

<img class="s-w" src="/big-file/doc/plugin/sa-quick-case.png" alt="Quick-Login 插件完整示例运行效果" />

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

---

### Sa Token Doc New Docs Plugin Spel At (`sa-token-doc-new/docs/plugin/spel-at.md`)

---
title: "Sa-Token 注解鉴权 SpEL 表达式"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,注解鉴权 SpEL 表达式,插件"
description: "Sa-Token 注解鉴权 SpEL 表达式：引入 sa-token-spring-el（依赖 Spring AOP）后可用 @SaCheckEL 写登录、权限、参数长度、SaSession 取值等条件。支持多账号根对象扩展、this 成员变量、NEED() 包装条件，并可配合 @SaIgnore 忽略鉴权。"
---

# SpEL 表达式注解鉴权

Sa-Token 提供一个 `@SaCheckEL` 鉴权注解，该注解允许你使用 SpEL 表达式进行鉴权。


### 1、引入插件

由于该注解的工作底层需要依赖 SpringAOP 切面编程，因此你需要单独引入插件包 `sa-token-spring-el` 才可以使用此注解。

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-el</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 注解鉴权使用 EL 表达式
implementation 'cn.dev33:sa-token-spring-el:${sa.top.version}'
```

:::



### 2、简单示例

以下是一些使用示例：
``` java
@RestController
@RequestMapping("/check-el/")
public class SaCheckELController {

	// 登录校验 
	@SaCheckEL("stp.checkLogin()")
	@RequestMapping("test1")
	public SaResult test1() {
		return SaResult.ok();
	}

	// 权限校验 
	@SaCheckEL("stp.checkPermission('user:edit')")
	@RequestMapping("test3")
	public SaResult test3() {
		return SaResult.ok();
	}

	// 参数长度校验 
	@SaCheckEL("NEED( #name.length() > 3 )")
	@RequestMapping("test5")
	public SaResult test5(@RequestParam(defaultValue = "") String name) {
		return SaResult.ok().set("name", name);
	}

	// SaSession 里取值校验 
	@SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )")
	@RequestMapping("test8")
	public SaResult test8() {
		return SaResult.ok();
	}

}
```


### 3、多账号体系鉴权

要在 EL 表达式中使用多账号体系鉴权模式，你需要在配置类中重写 `SaCheckELRootMap 扩展函数`，增加 EL 表达式可使用的根对象：

``` java
@Configuration
public class SaTokenConfigure {
	
    /**
     * 重写 Sa-Token 框架内部算法策略 
     */
    @PostConstruct
    public void rewriteSaStrategy() {
		// 重写 SaCheckELRootMap 扩展函数，增加注解鉴权 EL 表达式可使用的根对象
		SaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> {
			System.out.println("--------- 执行 SaCheckELRootMap 增强，目前已包含的的跟对象包括：" + rootMap.keySet());
			// 新增 stpUser 根对象，使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权
			rootMap.put("stpUser", StpUserUtil.getStpLogic());
		};
    }

}
```

然后就可以使用多账号体系鉴权模式了

``` java
// 多账号体系鉴权测试 
@SaCheckEL("stpUser.checkLogin()")
@RequestMapping("test9")
public SaResult test9() {
	return SaResult.ok();
}
```


### 4、调用本类成员变量
``` java
// 本模块需要鉴权的权限码
public String permissionCode = "article:add";

// 调用本类的成员变量 
@SaCheckEL("stp.checkPermission( this.permissionCode )")
@RequestMapping("test10")
public SaResult test10() {
	return SaResult.ok();
}
```


### 5、忽略鉴权
配合 `@SaIgnore` 注解做到忽略某接口的鉴权
``` java 
// 忽略鉴权测试 
@SaIgnore
@SaCheckEL("stp.checkPermission( 'abc' )")
@RequestMapping("test11")
public SaResult test11() {
	return SaResult.ok();
}
```


### 6、代码提示

如果在书写 SpEL 表达式时需要代码提示：

<img class="s-w" src="/big-file/doc/plugin/sa-check-el-code-tips.png" alt="IDEA 中 SpEL 表达式注解鉴权代码提示" />

可以在 idea 中安装 **SpEL Assistant** 插件，该插件由 `@ly-chn` 提供，允许为自定义注解书写 SpEL 表达式时增加代码提示功能，
开源地址：[https://github.com/ly-chn/SpEL-Assistant](https://github.com/ly-chn/SpEL-Assistant)

安装方式：直接在 idea 插件商店中搜索 “**SpEL Assistant**” 即可

<img class="s-w" src="/big-file/doc/plugin/sa-check-el-setup-plugin.png" alt="IDEA 安装 SpEL Assistant 插件" />


<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"
	target="_blank">
	本章代码示例：Sa-Token SpEL表达式注解鉴权 —— [ SaCheckELController.java ]
</a>

---

### Sa Token Doc New Docs Plugin Temp Token (`sa-token-doc-new/docs/plugin/temp-token.md`)

---
title: "Sa-Token 临时 Token 认证"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,临时 Token 认证,插件"
description: "Sa-Token 临时 Token 认证：sa-token-temp 已内嵌核心包。用 SaTempUtil.createToken 生成五分钟、半小时级短时凭证，适合公会邀请链接与一次性操作；支持业务前缀裁剪、按 value 反查 token，也可引入 sa-token-temp-jwt 以 JWT 为逻辑内核。"
---

# 临时 Token 令牌认证  

---

### 1、适用场景 

在部分业务场景，我们需要一种临时授权的能力，即：一个token的有效期并不需要像登录有效期那样需要[七天、三十天]，而是仅仅需要 [五分钟、半小时]。

举个比较明显的例子：超链接邀请机制。

::: info 业务场景

你在一个游戏中创建一个公会 `(id=10014)`，现在你想邀请你的好朋友加入这个公会，在你点击 **`[邀请]`** 按钮时，系统为你生成一个连接:

``` xml
http://xxx.com/apply?id=10014
```

接着，你的好朋友点击这个链接，加入了你的工会。

那么，系统是如何识别这个链接对应的工会是10014呢？很明显，我们可以观察出，这个链接的尾部有个id参数值为10014，这便是系统识别的关键。

此时你可能眉头一紧，就这么简单？那我如果手动更改一下尾部的参数改成10015，然后我再一点，岂不是就可以偷偷加入别人的工会了？

你想的没错，如果这个游戏的架构设计者采用上述方案完成功能的话，这个邀请机制就轻松的被你攻破了。

但是很明显，正常的商业项目一般不会拉跨到这种地步，比较常见的方案是，对这个公会id做一个token映射，最终你看到链接一般是这样的：

``` xml
http://xxx.com/apply?token=oEwQBnglXDoGraSJdGaLooPZnGrk
```

后面那一串字母是乱打出来的，目的是为了突出它的随机性，即：使用一个随机的token来代替明文显示真正的数据。

在用户点击这个链接之后，服务器便可根据这个token解析出真正公会id (10014) ，至于伪造？全是随机的你怎么伪造？你又不知道10015会随机出一个什么样的Token 。

而且为了安全性，这个token的有效期一般不会太长，给你预留五分钟、半小时的时间足够你点击它即可。

:::


### 2、创建临时 token

**[sa-token-temp 临时 token 认证模块]** 已内嵌到核心包，无需引入其它依赖即可使用：

``` java
// 根据 value 创建一个 token 
String token = SaTempUtil.createToken("10014", 200);

// 解析 token 获取 value，并转换为指定类型 
String value = SaTempUtil.parseToken(token, String.class);

// 获取指定 token 的剩余有效期，单位：秒 
SaTempUtil.getTimeout(token);

// 删除指定 token
SaTempUtil.deleteToken(token);
```


### 3、前缀拼接与裁剪

``` java
// 如果由多条业务线都需要生成临时 token，可以加个前缀进行区分
String token = SaTempUtil.createToken("shop_1001", 1200);
```

在获取时可以自行裁剪前缀，也可以调用：
``` java
// 解析 token 获取 value，并裁剪指定前缀，然后转换为指定类型
SaTempUtil.parseToken(token, "shop_", Long.class)
```

如果指定了错误的前缀，即使 token 正确，上述方法也将返回 null 


### 4、根据 value 反查 token

在创建 token 时，框架默认只会保存 `token -> value` 的映射，而不会记录 `value -> token` 的索引信息。

如果想要做到反查 token，则必须在创建 token 指定框架记录 token 索引信息：

``` java
// 在创建 token 时，指定第三个参数 true，即可让框架在保存 token 时同时记录 token 索引信息
String token1 = SaTempUtil.createToken(10004, 1200, true);
String token2 = SaTempUtil.createToken(10004, 1300, true);
String token3 = SaTempUtil.createToken(10004, -1, true);

// 获取 10004 对应的所有 token 
List<String> list = SaTempUtil.getTempTokenList(10004);
System.out.println(list);
```



### 5、集成jwt
提到 [临时Token认证]，你是不是想到一个专门干这件事的框架？对，就是JWT！

**[sa-token-temp]** 模块允许以JWT作为逻辑内核完成工作，你只需要引入以下依赖，所有上层API保持不变

:::tabs
== Maven 方式

``` xml
<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-temp-jwt</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
implementation 'cn.dev33:sa-token-temp-jwt:${sa.top.version}'
```

:::



并在配置文件中配置上jwt秘钥 **`(必填!)`**
``` yml
sa-token: 
	# sa-token-temp-jwt 模块的秘钥 （随便乱摁几个字母就行了） 
	jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk
```

---

### Sa Token Doc New Docs Plugin Thymeleaf Extend (`sa-token-doc-new/docs/plugin/thymeleaf-extend.md`)

---
title: "Sa-Token 和 Thymeleaf 集成"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,和 Thymeleaf 集成,插件"
description: "Sa-Token 整合 Thymeleaf：引入 sa-token-thymeleaf，注册 SaTokenDialect 后可在模板用 sa:login、sa:hasRole、sa:hasPermission 标签做登录与权限判断。也可注入 stp 全局变量，在页面调用 StpLogic/SaSession API。"
---

# Thymeleaf 标签方言

本插件的作用是让我们可以在 Thymeleaf 页面中使用 Sa-Token 相关API，俗称 —— 标签方言。

--- 

### 1、引入依赖 
首先我们确保项目已经引入 Thymeleaf 依赖，然后在此基础上继续添加：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-thymeleaf</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// 在 thymeleaf 标签中使用 Sa-Token
implementation 'cn.dev33:sa-token-thymeleaf:${sa.top.version}'
```

:::



### 2、注册标签方言对象 
在 SaTokenConfigure 配置类中注册 Bean 
``` java
@Configuration
public class SaTokenConfigure {
	// Sa-Token 标签方言 (Thymeleaf版)
	@Bean
	public SaTokenDialect getSaTokenDialect() {
		return new SaTokenDialect();
	}
}
```


### 3、使用标签方言 
然后我们就可以愉快的使用在 Thymeleaf 页面中使用标签方言了 

##### 3.1、登录判断 
``` html
<h2>标签方言测试页面</h2>
<p>
	登录之后才能显示：
	<span sa:login>value</span>
</p>
<p>
	不登录才能显示：
	<span sa:notLogin>value</span>
</p>
```

##### 3.2、角色判断
``` html
<p>
	具有角色 admin 才能显示：
	<span sa:hasRole="admin">value</span>
</p>
<p>
	同时具备多个角色才能显示：
	<span sa:hasRoleAnd="admin, ceo, cto">value</span>
</p>
<p>
	只要具有其中一个角色就能显示：
	<span sa:hasRoleOr="admin, ceo, cto">value</span>
</p>
<p>
	不具有角色 admin 才能显示：
	<span sa:notRole="admin">value</span>
</p>
```

##### 3.3、权限判断
``` html
<p>
	具有权限 user-add 才能显示：
	<span sa:hasPermission="user-add">value</span>
</p>
<p>
	同时具备多个权限才能显示：
	<span sa:hasPermissionAnd="user-add, user-delete, user-get">value</span>
</p>
<p>
	只要具有其中一个权限就能显示：
	<span sa:hasPermissionOr="user-add, user-delete, user-get">value</span>
</p>
<p>
	不具有权限 user-add 才能显示：
	<span sa:notPermission="user-add">value</span>
</p>
```


### 4、调用 Sa-Token 相关API  

以上的标签方言，可以满足我们大多数场景下的权限判断，然后有时候我们依然需要更加灵活的在页面中调用 Sa-Token 框架API  

首先在 SaTokenConfigure 配置类中为 Thymeleaf 配置全局对象：

``` java
@Configuration
public class SaTokenConfigure{
	// ... 其它代码
	
	// 为 Thymeleaf 注入全局变量，以便在页面中调用 Sa-Token 的方法 
	@Autowired
	private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) {
		viewResolver.addStaticVariable("stp", StpUtil.stpLogic);
	}
}
```

::: warning 注意
如果`SaTokenConfigure`继承了`WebMvcConfigurer`等类，可能会造成循环依赖，如果遇到，请新建一个其他配置类完成此项配置
:::


然后我们就可以在页面上调用 StpLogic 的 API 了，例如：
 
``` html
<p>调用 StpLogic 方法调用测试</p>
<p th:if="${stp.isLogin()}">
	从SaSession中取值：
	<span th:text="${stp.getSession().get('name')}"></span>
</p>
```


### 5、代码提示

如果想在写标签属性时增加代码提示：

<img class="s-w" src="/big-file/doc/plugin/thymeleaf-code-tips.png" alt="Thymeleaf 标签方言代码提示" />

只需在头部声明增加上对应的命名空间即可：

``` html
<!DOCTYPE html>
<html lang="zh" xmlns:sa="http://www.thymeleaf.org/extras/sa-token">
	<head>
		
	</head>
	<body>
		
	</body>
</html>
```

---

### Sa Token Doc New Docs Pro St Doc Top (`sa-token-doc-new/docs/pro/st_doc_top.md`)

---
description: "Sa-Max 统一认证商业版：文档顶部入口推荐的付费认证中心，缩短 SSO、OAuth2 从官方 Demo 到商业落地的周期，含跨 Redis、前后端分离、vue2/vue3 与开放平台。与开源 Sa-Token 互相独立，未购买不影响框架使用。Maven 中央仓代码不可撤回。跟踪参数 way=st_doc_top。"
---

# Sa-Max 统一认证商业版

### 项目介绍

根据 SSO / OAuth2 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。

然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此我们特意准备了项目：
[[ Sa-Max 统一认证商业版 ]](https://sa-max.cn?way=st_doc_top)。

项目基于 Sa-Token 搭建，集成了统一认证常见技术点，<b style="color: #FF5722;">可大大缩短您的项目接入统一授权认证的开发周期</b>：
- 支持：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 
- 支持：API Key、OAuth2.0 统一认证能力， 并提供开放平台，供第三方公司申请应用对接平台能力。

全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。





### 释疑

##### 1、Sa-Max 是收费项目吗？与 Sa-Token 有什么不同？

`Sa-Max` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。

`Sa-Max` 与 `Sa-Token` 的区别，简单来讲：
- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入
- `Sa-Max` 是一个完整项目，下载源码后可直接启动 


##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？
首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 

其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，
比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。

Sa-Max 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。

另外：即使您没有购买 `Sa-Max`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。



##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！

答案是不会。

再次强调一下：`Sa-Token` 与 `Sa-Max` 是两个独立的项目，两者互不影响。
付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。

实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。

---

### Sa Token Doc New Docs Pro St Index Top (`sa-token-doc-new/docs/pro/st_index_top.md`)

---
description: "Sa-Max 统一认证商业版：在 SSO 与 OAuth2 文档 Demo 之上提供可直接启动的认证中心，支持跨域跨 Redis、前后端分离、API Key、OAuth2.0 及第三方开放平台申请应用。全套源码可二开、不限域名与项目数。与开源框架独立。首页入口跟踪参数 way=st_index_top。"
---

# Sa-Max 统一认证商业版

### 项目介绍

根据 SSO / OAuth2 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个 SSO / OAuth2 模式的认证 Demo 示例。

然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此我们特意准备了项目：
[[ Sa-Max 统一认证商业版 ]](https://sa-max.cn?way=st_index_top)。

项目基于 Sa-Token 搭建，集成了统一认证常见技术点，<b style="color: #FF5722;">可大大缩短您的项目接入统一授权认证的开发周期</b>：
- 支持：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 
- 支持：API Key、OAuth2.0 统一认证能力， 并提供开放平台，供第三方公司申请应用对接平台能力。

全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。





### 释疑

##### 1、Sa-Max 是收费项目吗？与 Sa-Token 有什么不同？

`Sa-Max` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。

`Sa-Max` 与 `Sa-Token` 的区别，简单来讲：
- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入
- `Sa-Max` 是一个完整项目，下载源码后可直接启动 


##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？
首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 

其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，
比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。

Sa-Max 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。

另外：即使您没有购买 `Sa-Max`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。



##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！

答案是不会。

再次强调一下：`Sa-Token` 与 `Sa-Max` 是两个独立的项目，两者互不影响。
付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。

实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。

---

### Sa Token Doc New Docs Pro St Oauth2 (`sa-token-doc-new/docs/pro/st_oauth2.md`)

---
description: "Sa-Max 统一认证商业版：面向已阅读 OAuth2 模块文档、需要把授权中心做成生产项目的团队。除同域跨域、跨 Redis、vue2/vue3 等 SSO 接入外，提供 API Key、OAuth2.0 与开放平台对接。源码交付无加密 jar、不限域名。本页链接使用 way=st_oauth2 区分来源渠道。"
---

# Sa-Max 统一认证商业版

### 项目介绍

根据 OAuth2 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个 OAuth2 模式的认证 Demo 示例。

然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此我们特意准备了项目：
[[ Sa-Max 统一认证商业版 ]](https://sa-max.cn?way=st_oauth2)。

项目基于 Sa-Token 搭建，集成了统一认证常见技术点，<b style="color: #FF5722;">可大大缩短您的项目接入统一授权认证的开发周期</b>：
- 支持：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 
- 支持：API Key、OAuth2.0 统一认证能力， 并提供开放平台，供第三方公司申请应用对接平台能力。

全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。





### 释疑

##### 1、Sa-Max 是收费项目吗？与 Sa-Token 有什么不同？

`Sa-Max` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。

`Sa-Max` 与 `Sa-Token` 的区别，简单来讲：
- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入
- `Sa-Max` 是一个完整项目，下载源码后可直接启动 


##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？
首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 

其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，
比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。

Sa-Max 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。

另外：即使您没有购买 `Sa-Max`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。



##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Max 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！

答案是不会。

再次强调一下：`Sa-Token` 与 `Sa-Max` 是两个独立的项目，两者互不影响。
付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。

实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。

---

### Sa Token Doc New Docs Pro St Sso (`sa-token-doc-new/docs/pro/st_sso.md`)

---
description: "Sa-Pro 单点登录商业版：根据 SSO 模块文档与官方 Demo 延伸的完整认证中心，覆盖同域跨域、共享或跨 Redis、前后端一体或分离、vue2/vue3、Java 与非 Java。全套源码无加密 jar、可二开，不限域名与项目数。下载源码即可启动。本页入口跟踪参数为 way=st_sso。"
---

# Sa-Pro 单点登录商业版

### 项目介绍

根据 SSO 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个SSO模式的认证 Demo 示例。

然而，要真正开发一个商业级项目的统一认证中心系统，绝非一朝一夕可以搭建完毕，为此，我们特意准备了项目：
[[ Sa-Pro 单点登录商业版 ]](https://sa-max.cn?way=st_sso)。

项目基于 Sa-Token 搭建，集成了单点登录常见技术点，可解决： 
同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、java 项目、非 java 项目 等架构下的 SSO 认证需求。 
<b style="color: #FF5722;">可大大缩短您的项目接入单点登录的开发周期</b>。

全套源码交付、不包含加密 jar 、可自由二开。不限制域名、不限制项目数量。





### 释疑

##### 1、Sa-Pro 是收费项目吗？与 Sa-Token 有什么不同？

`Sa-Pro` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。

`Sa-Pro` 与 `Sa-Token` 的区别，简单来讲：
- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入
- `Sa-Pro` 是一个完整项目，下载源码后可直接启动 


##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？
首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 

其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，
比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。

Sa-Pro 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。

另外：即使您没有购买 `Sa-Pro`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。



##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Pro 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！

答案是不会。

再次强调一下：`Sa-Token` 与 `Sa-Pro` 是两个独立的项目，两者互不影响。
付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。

实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。

---

### Sa Token Doc New Docs Sso Anon Client (`sa-token-doc-new/docs/sso/anon-client.md`)

---
title: "Sa-Token 匿名 client 接入"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,匿名 client 接入,单点登录,SSO"
description: "匿名 Client：未配 sso-client 标识，授权、校验 ticket、单点注销都不带 client 参数。Server 端设 allow-anon-client、allow-url 与 secret-key，或重写 getAnonClient() 指定回调、密钥与是否单点注销。匿名走全局默认密钥。"
---

# 匿名 Client 接入

匿名 Client 就是指在客户端没有配置 `sso-client` 的应用，没有一个明确的 “Client” 标识名称。

匿名 Client 在一些关键步骤中不会构建 `client` 参数，如：“重定向至认证中心授权地址”、“校验 ticket”、“单点注销” 等。

要想匿名 client 接入，你需要做一些特殊配置。


### 1、在 sso-server 端开启匿名 client 接入

开启方式一，通过配置项方式：

:::tabs
== yaml 风格

``` yaml
# Sa-Token 配置
sa-token:
    # SSO-Server 配置
    sso-server:
        # 是否启用匿名 client (开启匿名 client 后，允许客户端接入时不提交 client 参数)
        allow-anon-client: true
        # 所有允许的授权回调地址 (匿名 client 使用)
        allow-url: "*"
        # API 接口调用秘钥 (全局默认 + 匿名 client 使用)
        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```

== properties 风格

``` properties
# SSO-Server 配置
# 是否启用匿名 client (开启匿名 client 后，允许客户端接入时不提交 client 参数)
sa-token.sso-server.allow-anon-client=true
# 所有允许的授权回调地址 (匿名 client 使用)
sa-token.sso-server.allow-url=*
# API 接口调用秘钥 (全局默认 + 匿名 client 使用)
sa-token.sso-server.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```

:::



开启方式二，通过代码重写方式：

``` java
/**
 * 重写 SaSsoServerTemplate 部分方法，增强功能
 */
@Component
public class CustomSaSsoServerTemplate extends SaSsoServerTemplate {

    /**
     * 获取配置项：是否允许匿名 client 接入
     */
    @Override
    public boolean getConfigOfAllowAnonClient() {
        return true;
    }

    /**
     * 获取匿名 client 配置信息
     */
    @Override
    public SaSsoClientModel getAnonClient() {
        SaSsoClientModel scm = new SaSsoClientModel();
        scm.setAllowUrl("*");  // 允许的授权地址
        scm.setIsSlo(true);  // 是否允许单点注销
        scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor");  // 客户端密钥
        return scm;
    }
}
```


### 2、在 sso-client 端不要配置 client 字段

然后在对应的应用端不要配置 client 字段，例如：

``` yml
# sa-token配置 
sa-token:
    # 配置一个不同的 token-name，以避免在和模式三 demo 一起测试时发生数据覆盖
    token-name: satoken-client-anon
    # sso-client 相关配置
    sso-client:
        # client 标识 匿名应用就是指不配置 client 标识的应用
        # client: sso-client3
        # sso-server 端主机地址
        server-url: http://sa-sso-server.com:9000
        # 使用 Http 请求校验ticket (模式三)
        is-http: true
        # 是否在登录时注册单点登录回调接口 (匿名应用想要参与单点注销必须打开这个)
        reg-logout-call: true
        # API 接口调用秘钥
        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```


::: tip demo
匿名 Client 接入的 Demo 示例地址：[sa-token-demo-sso3-client-anon](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-anon)
:::

这里有个值得注意的配置项：`reg-logout-call: true`，是干嘛的？

简单来讲，就是匿名应用不包含 client 字段信息，因此 sso-server 端也无法配置此 client 的消息推送地址，所以此 client 无法接受到消息推送，也就无法参与到单点注销的环路中来。

因此，新增一个配置项 `reg-logout-call: true`，代表在登录的同时把当前项目的单点注销回调地址 `/sso/logoutCall` 发送到 sso-server 端，
这样 sso-server 端有了备案，也就可以成功通知此应用发起单点注销掉了。

如果当前应用不需要单点注销可以不配置此字段。

---

### Sa Token Doc New Docs Sso Message Push (`sa-token-doc-new/docs/sso/message-push.md`)

---
title: "Sa-Token 消息推送机制"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,消息推送机制,单点登录,SSO"
description: "SSO 消息推送：Client 调 Server /sso/pushS，Server 调 Client /sso/pushC。内置 checkTicket、signout 等 msgType，参数含 timestamp、nonce 与 md5 签名。模式三靠它换会话，也可注册自定义消息处理器扩展通知。"
---

# 消息推送机制

消息推送机制简单来讲就是：sso-client 端按照特点格式构建一个 http 请求，调用 sso-server 端的 `/sso/pushS` 接口，sso-server 接收到消息后做出处理回应 sso-client 端。

消息推送是相互的，sso-server 端也可以构建 http 请求，调用 sso-client 端的 `/sso/pushC` 接口。

消息推送机制是应用与认证中心相互沟通的桥梁，ticket 校验、单点注销等行为都是依赖消息推送机制来实现的。

本篇将介绍在 Sa-Token SSO 模块中，sso-server 端和 sso-client 端分别内置了哪些消息模块，以及如何自定义消息处理器。


### 1、sso-server 端内置消息处理器

#### 1.1、checkTicket（ticket 校验）

作用：在 SSO 模式三下为 sso-client 提供 ticket 校验能力，返回 loginId 等数据

``` url
http://{sso-server主机地址}/sso/pushS
```

接收参数：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| msgType		| 是		| 消息类型，此处填 `checkTicket`							|
| ticket		| 是		| ticket 码							|
| client		| 否		| 客户端标识，可不填，代表是一个匿名应用				|
| ssoLogoutCall	| 否		| Client 端单点注销时 - 回调 URL 参数名称 (匿名 Client 时使用)			|
| timestamp		| 是		| 当前时间戳，13位									|
| nonce			| 是		| 随机字符串										|
| sign			| 是		| 签名，生成算法示例：`md5( client={client值}&msgType={checkTicket}&nonce={随机字符串}&ticket={ticket码}&timestamp={13位时间戳}&key={secretkey秘钥} )`					|

**<font color="#080" >签名算法规则：将所有参数按照字典顺序依次排列（key除外，挂在最后面），然后进行 md5 摘要。以下不再赘述。</font>**

返回值示例：
``` js
{
  "code": 200,   // 返回 200=成功，500=失败
  "msg": "ok",
  "data": "10001",
  "loginId": "10001",   // 此 ticket 对应的认证中心 loginId 
  "tokenValue": "5db12b02-9c8e-4e36-8ed9-bf295caed80e",   // 对应的认证中心会话 token 
  "deviceId": "MxOTCLWi5NXGqFQZBFdsH66Ni5YTJ8q0",   // 对应的认证中心登录设备 id
  "remainTokenTimeout": 2591999,   // token 剩余有效期
  "remainSessionTimeout": 2591999   // Access-Session 会话剩余有效期
}
```


#### 1.2、signout（单点注销）

作用：为 sso-client 提供单点注销能力

``` url
http://{sso-server主机地址}/sso/pushS
```

接收参数：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| msgType		| 是		| 消息类型，此处填 `signout`							|
| loginId		| 是		| 账号id							|
| client		| 否		| 客户端标识，可不填，代表是一个匿名应用				|
| deviceId		| 否		| 客户端设备 id 								|
| timestamp		| 是		| 当前时间戳，13位									|
| nonce			| 是		| 随机字符串										|
| sign			| 是		| 签名，生成算法示例：`md5( client={client值}&deviceId={设备id}&msgType={signout}&nonce={随机字符串}&loginId={loginId}&timestamp={13位时间戳}&key={secretkey秘钥} )`					|

返回值示例：
``` js
{
  "code": 200,   // 返回 200=成功，500=失败
  "msg": "ok",
  "data": null
}
```


### 2、sso-client 端内置消息处理器

#### 2.1、logoutCall（单点注销回调）

作用：接收来自 sso-server 的单点注销回调通知 

``` url
http://{sso-client主机地址}/sso/pushC
```

接收参数：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| msgType		| 是		| 消息类型，此处填 `logoutCall`							|
| loginId		| 是		| 账号id							|
| deviceId		| 否		| 客户端设备 id 								|
| timestamp		| 是		| 当前时间戳，13位									|
| nonce			| 是		| 随机字符串										|
| sign			| 是		| 签名，生成算法示例：`md5( deviceId={设备id}&msgType={logoutCall}&nonce={随机字符串}&loginId={loginId}&timestamp={13位时间戳}&key={secretkey秘钥} )`					|

返回值示例：
``` js
{
  "code": 200,   // 返回 200=成功，500=失败
  "msg": "单点注销回调成功",
  "data": null
}
```


### 3、认证中心自定义消息处理器

当然你也可以通过自定义消息处理器的方式，来扩展消息推送能力，这将非常有助于你完成一些应用与认证中心的自定义数据交互。

假设我们现在有如下需求：在 sso-client 获取 sso-server 端指定账号 id 的昵称、头像等信息，即：用户资料的拉取。

首先，我们需要在 sso-server 实现一个消息处理器：

``` java
@RestController
public class SsoServerController {

	// 配置SSO相关参数 
	@Autowired
	private void configSso(SaSsoServerTemplate ssoServerTemplate) {

		// 添加消息处理器：userinfo (获取用户资料) （用于为 client 端开放拉取数据的接口）
		ssoServerTemplate.messageHolder.addHandle("userinfo", (ssoTemplate, message) -> {
			System.out.println("收到消息：" + message);

			// 自定义返回结果（模拟）
			return SaResult.ok()
					.set("id", message.get("loginId"))
					.set("name", "LinXiaoYu")
					.set("sex", "女")
					.set("age", 18);
		});

	}

}
```


### 4、应用端调用消息推送接口获取数据

首先保证在配置文件里要配置上消息推送的具体地址

:::tabs
== yaml 风格

``` yaml
# sa-token配置 
sa-token:
    # sso-client 相关配置
    sso-client:
        # 应用标识
        client: sso-client3
        # sso-server 端主机地址
        server-url: http://sa-sso-server.com:9000
        # sso-server 端推送消息地址
		# 配置 server-url 后，框架可自动计算对应的 push-url 地址，也可以单独配置 push-url 地址，两者选其一即可
        # push-url: http://sa-sso-server.com:9000/sso/pushS
        # API 接口调用秘钥
        secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```

== properties 风格

``` properties
# sso-client 相关配置
# 应用标识
sa-token.sso-client.client=sso-client3
# sso-server 端主机地址
sa-token.sso-client.server-url=http://sa-sso-server.com:9000
# sso-server 端推送消息地址
# 配置 server-url 后，框架可自动计算对应的 push-url 地址，也可以单独配置 push-url 地址，两者选其一即可
sa-token.sso-client.push-url=http://sa-sso-server.com:9000/sso/pushS
# API 接口调用秘钥
sa-token.sso-client.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```

:::



然后在需要拉取资料的地方：

``` java
// 查询我的账号信息：sso-client 前端 -> sso-client 后端 -> sso-server 后端
@RequestMapping("/sso/myInfo")
public Object myInfo() {
	// 如果尚未登录
	if( ! StpUtil.isLogin()) {
		return "尚未登录，无法获取";
	}

	// 获取本地 loginId
	Object loginId = StpUtil.getLoginId();

	// 构建消息对象 
	SaSsoMessage message = new SaSsoMessage();
	message.setType("userinfo");
	message.set("loginId", loginId);
	
	// 推送至 sso-server，并接收响应数据 
	SaResult result = SaSsoClientUtil.pushMessageAsSaResult(message);

	// 返回给前端
	return result;
}
```

---

### Sa Token Doc New Docs Sso Signout (`sa-token-doc-new/docs/sso/signout.md`)

---
title: "Sa-Token 单点注销"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,单点注销,单点登录,SSO"
description: "Sa-Token 单点注销：单端走 /sso/logoutByAlone；全端走 /sso/logout，经 /sso/pushS?msgType=signout 再 /sso/pushC 通知各 Client。支持 ajax 无刷与跳页 back。登录时带 deviceId 可做单浏览器注销。需 is-slo。"
---

# 单点注销

Sa-Token SSO 提供多种注销模式：

从注销范围上可以分为：

- 单端注销：会话只在当前应用注销，其它应用和认证中心不受影响。
- 全端注销：一处注销，全端下线。也即：单点注销。
- 单浏览器注销：该账号的只在当前浏览器登录的应用注销，其它浏览器/设备不受影响。

从注销方式上可以分为：
- ajax 无刷单点注销：调用指定的 RestAPI 接口完成注销。
- 跳页面注销：跳转到指定接口进行注销，注销完成后原路返回或跳转到指定页面。

--- 

### 1、单端注销

在后端添加接口：
``` java
// 当前应用独自注销 (不退出其它应用)
@RequestMapping("/sso/logoutByAlone")
public Object logoutByAlone() {
	StpUtil.logout();
	return SaSsoClientProcessor.instance._ssoLogoutBack(SaHolder.getRequest(), SaHolder.getResponse());
}
```

在前端或跳转或 ajax 异步调用此接口即可。

如果是跳转可指定 back 参数，代表注销成功后跳转的地址，例如：`http://sso-client.com/sso/logoutByAlone?back=https://sa-token.com` 


### 2、全端注销

此处先简单看一下 Sa-Token SSO 的单点注销链路过程：

1. sso-client 的前端向 sso-client 的后端发起单点注销请求。(调用 `http://{sso-client}/sso/logout`)
2. sso-client 的后端向 sso-server 的后端发送单点注销请求。(调用 `http://{sso-server}/sso/pushS?msgType=signout`)
3. sso-server 端遍历 client 列表，逐个推送消息通知 sso-client 端下线。(`http://{sso-client}/sso/pushC?msgType=logoutCall`)
4. sso-server 端注销下线。
5. sso-server 后端响应 sso-client 后端：注销完成。
6. sso-client 后端响应 sso-client 前端：注销完成。
7. 整体完成。


<button class="show-img" img-src="/big-file/doc/sso/g3--sso3-logout.gif">加载动态演示图</button>


这些逻辑 Sa-Token 内部已经封装完毕，你只需按照文档步骤集成即可。以模式三 demo 为例：

#### 2.1、更改注销方案

单点注销是 Sa-Token SSO 内部已封装的接口，无需手动再添加，只需要在前端调用即可。

``` java
// SSO-Client端：首页
@RequestMapping("/")
public String index() {
	String str = "<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>" +
			"<p>当前会话是否登录：" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
			"<p> " +
				"<a href='/sso/login?back=/'>登录</a> - " +
				"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - " +
				"<a href='/sso/logout?back=self'>全端注销</a> " +
			"</p>";
	return str;
}
```

重点在第 9 行。

#### 2.2、启动测试 
重启项目，依次登录三个 client：
- [http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/)
- [http://sa-sso-client2.com:9003/](http://sa-sso-client2.com:9003/)
- [http://sa-sso-client3.com:9003/](http://sa-sso-client3.com:9003/)

<img class="s-w-sh" src="/big-file/doc/sso/sso-type3-client-index.png" alt="SSO 模式三 Client 已登录首页" />

在任意一个 client 里，点击 **`[注销]`** 按钮，即可单点注销成功（打开另外两个client，刷新一下页面，登录态丢失）。



<img class="s-w-sh" src="/big-file/doc/sso/sso-type3-slo-index.png" alt="SSO 模式三单点注销成功" />

PS：这里我们为了方便演示，使用的是超链接跳页面的形式，正式项目中使用 Ajax 调用接口即可做到无刷单点登录退出。

例如，我们使用 [Apifox 接口测试工具](https://www.apifox.cn/) 可以做到同样的效果：

<img class="s-w-sh" src="/big-file/doc/sso/sso-slo-apifox.png" alt="Apifox 调用 SSO 单点注销接口" />



### 3、单浏览器注销

单浏览器注销的前提是在登录时按照 `deviceId` 设备ID 参数为登录进行分组，这样在发起注销时即可格局设备ID参数做到单浏览器注销功能。

#### 3.1、sso-server 端加上设备ID参数登录

首先在 sso-server 的登录方法内，加上 deviceId 参数，例如：

``` java
@RestController
public class SsoServerController {
	
	// 其它代码，非重点，省略展示...
	
	// 配置SSO相关参数 
	@Autowired
	private void configSso(SaSsoServerTemplate ssoServerTemplate) {
		// 配置：登录处理函数 
		ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {
			// 此处仅做模拟登录，真实环境应该查询数据库进行登录
			if("sa".equals(name) && "123456".equals(pwd)) {
				String deviceId = SaHolder.getRequest().getParam("deviceId", SaFoxUtil.getRandomString(32));
				StpUtil.login(10001, new SaLoginParameter().setDeviceId(deviceId));
				return SaResult.ok("登录成功！").setData(StpUtil.getTokenValue());
			}
			return SaResult.error("登录失败！");
		};

	}
}
```

如上代码，在登录时获取前端提交的 deviceId 参数，如果前端没有提交则随机生成一个。


#### 3.2、sso-client 端发起注销时指定单设备注销参数

然后在 sso-client 发起单点注销时，加上 `singleDeviceIdLogout=true` 参数，代表按照设备 id 进行分组注销，非本设备id的会话不参与注销行为：

``` java
// SSO-Client端：首页
@RequestMapping("/")
public String index() {
	String str = "<h2>Sa-Token SSO-Client 应用端 (模式三)</h2>" +
			"<p>当前会话是否登录：" + StpUtil.isLogin() + " (" + StpUtil.getLoginId("") + ")</p>" +
			"<p> " +
				"<a href='/sso/login?back=/'>登录</a> - " +
				"<a href='/sso/logoutByAlone?back=/'>单应用注销</a> - " +
				"<a href='/sso/logout?back=self&singleDeviceIdLogout=true'>单浏览器注销</a> - " +
				"<a href='/sso/logout?back=self'>全端注销</a> " +
			"</p>";
	return str;
}
```

重点在第 9 行。


::: warning 测试注意点
在进行测试时，同时将一个浏览器双击打开两次，是不算 “不同浏览器” 的，虽然你打开了两个浏览器窗口，但是这两个浏览器的会话数据是互通的。

必须打开两个不同的浏览器来测试，或者按快捷键 `ctrl + shift + N` 打开隐私模式，才可以做到会话相互隔离。
:::


### 4、代码里指定账号单点注销

``` java
// sso-server：指定账号单点注销
SaSsoServerUtil.ssoLogout(loginId);
SaSsoServerUtil.ssoLogout(loginId, logoutParameter, ignoreClient);

// sso-client：指定账号单点注销
SaSsoClientUtil.ssoLogout(loginId);
SaSsoClientUtil.ssoLogout(loginId, logoutParameter);
```

---

### Sa Token Doc New Docs Sso Sso Apidoc (`sa-token-doc-new/docs/sso/sso-apidoc.md`)

---
title: "Sa-Token SSO-Server 认证中心开放 API 接口"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,SSO-Server 认证中心开放 API 接口,单点登录,SSO"
description: "SSO-Server 按 STS 协议开放 HTTP API：/sso/auth 授权（mode=simple|ticket）、/sso/doLogin、/sso/signout、/sso/pushS。Ticket 一次性、默认五分钟。Java 可用 SDK，非 Java 对照本页走 NoSDK 对接。"
---

# SSO-Server 认证中心开放接口

--- 

## 一、对接方式说明

在前一章节，我们成功搭建了 SSO-Server 端。SSO-Server 可以为各个子系统提供中央认证服务。

在默认代码架构下，SSO-Server 将提供一套标准 HTTP 接口对外开放。要对接 SSO-Server，有两种方式：
- SDK 方式：在你的 SSO-Client 端，也引入 Sa-Token SSO 框架，通过框架提供的 API 方法完成对接。
	
- NoSDK 方式：在你的 SSO-Client 端，不引入 Sa-Token SSO 框架，通过工具库调用 Http 接口的方式完成对接。
	

如果你的 SSO-Client 端是 java 项目，且支持引入 sa-token-sso 框架，我们强烈推荐你使用 SDK 方式对接。**你可以直接跳过本章**，开始 [SSO模式一 共享Cookie同步会话](/sso/sso-type1) 的学习。

如果你的 SSO-Client 端是 非java 项目，或不支持引入 sa-token-sso 框架，那么你可以使用 NoSDK 方式进行对接。
在之后的 [SSO整合 - NoSdk 模式对接](/sso/sso-nosdk) 章节我们会详细介绍对接步骤，下面的 API 文档将给你的对接步骤做一份参考。


## 二、STS 协议

Sa-Token SSO 模块的开放接口标准为 STS 协议。STS 协议的设计充分参考了 CAS、Spring Authorization Server、KeyClock、OAuth2 协议等经典框架/ 协议的授权流程。
定义了应用进行 单点登录、单点注销、消息推送 等动作的标准动作。
可解决：同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯 js、vue2、vue3、Sa-Token 项目、非 Sa-Token 项目、java 项目、非 java 项目 等架构下的 SSO 认证需求。

下面两节将介绍 STS 协议在 SSO-Server 端和 SSO-Client 端开放的接口标准。



## 三、SSO-Server 认证中心接口 


### 1、单点登录授权地址
``` url
http://{host}:{port}/sso/auth
```

接收参数：

| 参数			| 是否必填	| 说明																|
| :--------		| :--------	| :--------															|
| redirect		| 否		| 登录成功后的重定向地址，一般填写 location.href（从哪来回哪去），如不填，则跳转至 home-route						|
| mode			| 否		| 授权模式，取值 [simple, ticket]，simple=登录后直接重定向，ticket=带着ticket参数重定向，默认值为ticket			|
| client		| 否		| 客户端标识，可不填，代表是一个匿名应用，若填写了，则校验 ticket 时也必须是这个 client 才可以校验成功			|

访问接口后有两种情况：
- 情况一：当前会话在 SSO 认证中心未登录，会进入登录页开始登录。
- 情况二：当前会话在 SSO 认证中心已登录，会被重定向至 `redirect` 地址，并携带 `ticket` 参数。

Ticket 码具有以下特点：
1. 每次授权产生的 `ticket` 码都不一样。
2. `ticket` 码用完即废，不能二次使用。
3. 一个 `ticket` 的有效期默认为五分钟，超时自动作废。
4. 每次授权产生新 `ticket` 码，会导致旧 `ticket` 码立即作废，即使旧 `ticket` 码尚未使用。


### 2、RestAPI 登录接口
``` url
http://{host}:{port}/sso/doLogin
```

接收参数：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| name			| 是		| 用户名  |
| pwd			| 是		| 密码	 |

此接口属于 RestAPI (使用ajax访问)，会进入后端配置的 `ssoServerTemplate.strategy.doLoginHandle` 函数中，此函数的返回值即是此接口的响应值。

另外需要注意：此接口并非只能携带 name、pwd 参数，因为你可以在方法里通过 `SaHolder.getRequest().getParam("xxx")` 来获取前端提交的其它参数。 


### 3、单点注销接口
``` url
http://{host}:{port}/sso/signout
```

接收参数：

| 参数			| 是否必填	| 说明											|
| :--------		| :--------	| :--------										|
| back			| 否		| 注销成功后的重定向地址，一般填写 location.href（从哪来回哪去），也可以填写 self 字符串，含义同上			|


### 4、消息推送接口

``` url
http://{host}:{port}/sso/pushS
```

接收参数：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| client		| 否		| 客户端标识，可不填，代表是一个匿名应用				|
| timestamp		| 是		| 当前时间戳，13位									|
| msgType		| 是		| 消息类型										|
| nonce			| 是		| 随机字符串										|
| sign			| 是		| 签名，生成算法：`md5( client={client值}&msgType={消息类型}&nonce={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )`					|

此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。

返回值示例：

- 推送成功时：

``` js
{
    "code": 200,
    "msg": "ok",
    "data": "10001",	// 返回的数据 
}
```

- 推送失败时：

``` js
{
    "code": 500,    // 200表示请求成功，非200标识请求失败
    "msg": "签名无效：xxx",    // 失败原因 
    "data": null
}
```

- 也有可能消息推送成功了，但是处理消息失败，例如校验 ticket 时：

``` js
{
    "code": 500,
    "msg": "无效ticket：vESj0MtqrtSoucz4DDHJnsqU3u7AKFzbj0KH57EfJvuhkX1uAH23DuNrMYSjTnEq",
    "data": null
}
```

详细可参考：[消息推送机制](/sso/message-push)



<br>





---

## 四、SSO-Client 应用端开放接口 

### 1、登录地址
``` url
http://{host}:{port}/sso/login
```

接收参数：

| 参数			| 是否必填	| 说明																|
| :--------		| :--------	| :--------															|
| back			| 是		| 登录成功后的重定向地址，一般填写 location.href（从哪来回哪去）			|
| ticket		| 否		| 授权 ticket 码			|

此接口有两种访问方式：
- 方式一：我们需要登录操作，所以带着 back 参数主动访问此接口，框架会拼接好参数后再次将用户重定向至认证中心。
- 方式二：用户在认证中心登录成功后，带着 ticket 参数重定向而来，此为框架自动处理的逻辑，开发者无需关心。


### 2、注销地址
``` url
http://{host}:{port}/sso/logout
```

接收参数：

| 参数			| 是否必填	| 说明																|
| :--------		| :--------	| :--------															|
| back			| 否		| 注销成功后的重定向地址，一般填写 location.href（从哪来回哪去），也可以填写 self 字符串，含义同上			|

此接口有两种访问方式：
- 方式一：直接 `location.href` 网页跳转，此时可携带 back 参数。
- 方式二：使用 Ajax 异步调用（此方式不可携带 back 参数，但是需要提交会话 Token ），注销成功将返回以下内容：

``` js
{
    "code": 200,    // 200表示请求成功，非200标识请求失败
    "msg": "单点注销成功",
    "data": null
}
```


### 3、单点注销回调接口
此接口仅配置 `(reg-logout-call=true)` 时打开，且为框架回调，开发者无需关心

``` url
http://{host}:{port}/sso/logoutCall
```

接收参数：

| 参数			| 是否必填	| 说明											|
| :--------		| :--------	| :--------										|
| loginId		| 是		| 要注销的账号 id			 					|
| timestamp		| 是		| 当前时间戳，13位								|
| nonce			| 是		| 随机字符串										|
| sign			| 是		| 签名，生成算法：`md5( loginId={账号id}&nonce={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )` |
| client		| 否		| 客户端标识，如果你在登录时向 sso-server 端传递了 client 值，那么在此处 sso-server 也会给你回传过来，否则此参数无值。如果此参数有值，则此参数也要参与签名，放在 loginId 参数前面（字典顺序）		|
| autoLogout	| 否		| 是否为“登录client超过最大数量”引起的自动注销（true=超限系统自动注销，false=用户主动发起注销）。如果此参数有值，则此参数也要参与签名，放在 client 参数前面（字典顺序）		|


返回数据：

``` js
{
    "code": 200,    // 200表示请求成功，非200标识请求失败
    "msg": "单点注销回调成功",
    "data": null
}
```



### 4、消息推送接口

``` url
http://{host}:{port}/sso/pushC
```

接收参数：

| 参数			| 是否必填	| 说明													|
| :--------		| :--------	| :--------												|
| timestamp		| 是		| 当前时间戳，13位									|
| msgType		| 是		| 消息类型										|
| nonce			| 是		| 随机字符串										|
| sign			| 是		| 签名，生成算法：`md5( msgType={消息类型}&nonce={随机字符串}&timestamp={13位时间戳}&key={secretkey秘钥} )`					|

此接口可根据消息类型增加任意参数。新增加的参数要参与 sign 签名。

返回值示例：

- 推送成功时：

``` js
{
    "code": 200,
    "msg": "ok",
    "data": "10001",	// 返回的数据 
}
```

- 推送失败时：

``` js
{
    "code": 500,    // 200表示请求成功，非200标识请求失败
    "msg": "签名无效：xxx",    // 失败原因 
    "data": null
}
```

详细可参考：[消息推送机制](/sso/message-push)

---

### Sa Token Doc New Docs Sso Sso Check Domain (`sa-token-doc-new/docs/sso/sso-check-domain.md`)

---
title: "Sa-Token 配置域名校验"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,配置域名校验,单点登录,SSO"
description: "SSO 域名校验：clients.*.allow-url 白名单限制 /sso/auth 的 redirect，防 Ticket 劫持。生产禁止配 *，配到域名级安全性中等，建议写到 /sso/login 完整地址。非法 redirect 会被拒绝授权。先回传一次性 Ticket，避免长期 Token 出现在 URL。"
---

# SSO整合-配置域名校验

--- 

### 1、Ticket劫持攻击
在前面章节的 SSO-Server 示例中，配置项 `sa-token.sso-server.clients.sso-client3.allow-url=*` 意为该 client 所有允许的授权地址，不在此配置项中的 URL 将无法单点登录成功。

为了方便测试，上述代码将其配置为`*`，但是，<font color="#FF0000" >在生产环境中，此配置项绝对不能配置为 * </font>，否则会有被 Ticket 劫持的风险。

假设攻击者根据模仿我们的授权地址，巧妙的构造一个URL：

> [http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/](http://sa-sso-server.com:9000/sso/auth?client=sso-client3&redirect=https://www.baidu.com/)

当不知情的小红被诱导访问了这个URL时，它将被重定向至百度首页：

<img class="s-w-sh" src="/big-file/doc/sso/sso-ticket-jc.png" alt="SSO Ticket 劫持攻击：非法 redirect 跳转示例">

可以看到，代表着用户身份的 Ticket 码也显现到了 URL 之中，借此漏洞，攻击者完全可以构建一个URL将小红的 Ticket 码自动提交到攻击者自己的服务器，伪造小红身份登录网站

### 2、防范方法

造成此漏洞的直接原因就是SSO-Server认证中心没有对 `redirect地址` 进行任何的限制，防范的方法也很简单，就是对`redirect参数`进行校验，如果其不在指定的URL列表中时，拒绝下放ticket 

我们将其配置为一个具体的URL：
:::tabs
== yaml 风格

``` yaml
sa-token: 
	sso-server: 
		clients:
			sso-client3:
				# 配置允许单点登录的 url 
				allow-url: http://sa-sso-client1.com:9003/sso/login
```

== properties 风格

``` properties
# 配置允许单点登录的 url 
sa-token.sso-server.clients.so-client3.allow-url=http://sa-sso-client1.com:9003/sso/login
```

:::


再次访问上述链接：

<img class="s-w-sh" src="/big-file/doc/sso/sso-feifa-rf.png" alt="SSO 配置 allow-url 白名单后拒绝非法 redirect">

域名没有通过校验，拒绝授权！

### 3、配置安全性参考表

| 配置方式		| 举例										| 安全性								|  建议									|
| :--------		| :--------									| :--------							| :--------								|
| 配置为*		| `*`										| <font color="#F00" >低</font>		| **<font color="#F00" >禁止在生产环境下使用</font>**	|
| 配置到域名	| `http://sa-sso-client1.com/*`					| <font color="#F70" >中</font>		| <font color="#F70" >不建议在生产环境下使用</font>	|
| 配置到详细地址| `http://sa-sso-client1.com:9001/sso/login`	| <font color="#080" >高</font>		| <font color="#080" >可以在生产环境下使用</font>	|


### 4、疑问：为什么不直接回传 Token，而是先回传 Ticket，再用 Ticket 去查询对应的账号id？
Token 作为长时间有效的会话凭证，在任何时候都不应该直接暴露在 URL 之中（虽然 Token 直接的暴露本身不会造成安全漏洞，但会为很多漏洞提供可乘之机）

为了不让系统安全处于亚健康状态，Sa-Token-SSO 选择先回传 Ticket，再由 Ticket 获取账号id，且 Ticket 一次性用完即废，提高安全性。

---

### Sa Token Doc New Docs Sso Sso Custom Api (`sa-token-doc-new/docs/sso/sso-custom-api.md`)

---
title: "Sa-Token 自定义API路由"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,自定义API路由,单点登录,SSO"
description: "SSO 自定义 API 路由：改 ApiName 把默认 /sso/auth 换成业务路径，或把 /sso/* 拆成 auth、doLogin、pushS、signout 独立入口，调用 SaSsoServerProcessor 对应方法，以适配网关前缀与既有 URL。Client 侧路径须同步修改。"
---

# SSO整合-自定义 API 路由 

---

### 方式一：修改全局变量

在之前的章节中，我们演示了如何搭建一个SSO认证中心：
``` java
/**
 * Sa-Token-SSO Server端 Controller 
 */
@RestController
public class SsoServerController {

	// SSO-Server端：处理所有SSO相关请求 
	@RequestMapping("/sso/*")
	public Object ssoRequest() {
		return SaSsoServerProcessor.instance.dister();
	}
	
	// ... 其它代码
	
}
```

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

打开SSO模块相关源码，有关 API 的设计都定义在：
[ApiName.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/name/ApiName.java)
中，我们可以对其进行二次修改。

例如，我们可以在 Main 方法启动类或者 SSO 配置方法中修改变量值：
``` java
// 配置SSO相关参数 
@Autowired
private void configSso(SaSsoServerTemplate ssoServerTemplate) {
	// 自定义API地址
	SaSsoServerProcessor.instance.ssoServerTemplate.apiName.ssoAuth = "/sso/auth2";
	// ... 
	
}
```

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


### 方式二：拆分路由入口
根据上述路由入口：`@RequestMapping("/sso/*")`，我们给它起一个合适的名字 —— 聚合式路由。

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

``` java
/**
 * Sa-Token-SSO Server端 Controller 
 */
@RestController
public class SsoServerController {

	// SSO-Server：统一认证地址 
	@RequestMapping("/sso/auth")
	public Object ssoAuth() {
		return SaSsoServerProcessor.instance.ssoAuth();
	}

	// SSO-Server：RestAPI 登录接口 
	@RequestMapping("/sso/doLogin")
	public Object ssoDoLogin() {
		return SaSsoServerProcessor.instance.ssoDoLogin();
	}

	// SSO-Server：接收推送消息地址
	@RequestMapping("/sso/pushS")
	public Object ssoPushS() {
		return SaSsoServerProcessor.instance.ssoPushS();
	}

	// SSO-Server：单点注销 
	@RequestMapping("/sso/signout")
	public Object ssoSignout() {
		return SaSsoServerProcessor.instance.ssoSignout();
	}
	
	// ... 其它方法 
	
}
```

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


### SSO-Client 端拆分路由入口示例

``` java
/**
 * Sa-Token-SSO Client端 Controller 
 */
@RestController
public class SsoClientController {

	// SSO-Client：登录地址
	@RequestMapping("/sso/login")
	public Object ssoLogin() {
		return SaSsoClientProcessor.instance.ssoLogin();
	}

	// SSO-Client：单点注销地址
	@RequestMapping("/sso/logout")
	public Object ssoLogout() {
		return SaSsoClientProcessor.instance.ssoLogout();
	}

	// SSO-Client：单点注销回调
	@RequestMapping("/sso/logoutCall")
	public Object ssoLogoutCall() {
		return SaSsoClientProcessor.instance.ssoLogoutCall();
	}

	// SSO-Client：接收消息推送地址
	@RequestMapping("/sso/pushC")
	public Object ssoPushC() {
		return SaSsoClientProcessor.instance.ssoPushC();
	}

	// ... 其它方法 
	
}
```

---

### Sa Token Doc New Docs Sso Sso Custom Login (`sa-token-doc-new/docs/sso/sso-custom-login.md`)

---
title: "Sa-Token 定制化登录页面"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,定制化登录页面,单点登录,SSO"
description: "SSO 定制登录页：前端按钮跳 /sso/login?back=，或 SaServletFilter 拦截未登录重定向；Ajax 可返回 401 再由前端跳转。notLoginView 换登录 UI，也可按 client 标识跳不同登录页。doLoginHandle 可用 SaHolder 取额外参数。"
---

# SSO整合-定制化登录页面

---

### 1、何时引导用户去登录？

#### 方案一：前端按钮跳转 
前端页面准备一个 **`[登录]`** 按钮，当用户点击按钮时，跳转到登录接口 
``` html
<a href="javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);">登录</a>
```

#### 方案二：后端拦截重定向
在后端注册全局过滤器（或拦截器、或全局异常处理），拦截需要登录后才能访问的页面资源，将未登录的访问重定向至登录接口 
``` java
/**
 * Sa-Token 配置类 
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
	/** 注册 [Sa-Token全局过滤器] */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
        		.addInclude("/**")
        		.addExclude("/sso/*", "/favicon.ico")
        		.setAuth(obj -> {
        			if(StpUtil.isLogin() == false) {
        				String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
        				SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back));
        				SaRouter.back();
        			}
        		})
        		;
    }
}
```

#### 方案三：后端拦截 + 前端跳转 
首先，后端仍需要提供拦截，但是不直接引导用户重定向，而是返回未登录的提示信息 
```  java
/**
 * Sa-Token 配置类 
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
	/** 注册 [Sa-Token全局过滤器] */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
        		.addInclude("/**")
        		.addExclude("/sso/*", "/favicon.ico")
        		.setAuth(obj -> {
        			if(StpUtil.isLogin() == false) {
        				// 与前端约定好，code=401时代表会话未登录 
        				SaRouter.back(SaResult.ok().setCode(401));
        			}
        		})
        		;
    }
}
```

前端接受到返回结果 `code=401` 时，开始跳转至登录接口
``` js
if(res.code == 401) {
	location.href = '/sso/login?back=' + encodeURIComponent(location.href);
}
```

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




### 2、如何自定义登录视图？

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

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

``` java
// 配置：未登录时返回的View 
ssoServerTemplate.strategy.notLoginView = () -> {
	return new ModelAndView("xxx.html");
}
```


### 3、如何自定义登录API的接口地址？
根据需求点选择解决方案：

#### 3.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数？
``` java
// 在任意代码处获取前端提交的参数 
String xxx = SaHolder.getRequest().getParam("xxx");
```

#### 3.2、想完全自定义一个接口来接受前端登录请求？
``` java
// 直接定义一个拦截路由为 `/sso/doLogin` 的接口即可 
@RequestMapping("/sso/doLogin")
public SaResult ss(String name, String pwd) {
	System.out.println("------ 请求进入了自定义的API接口 ---------- ");
	if("sa".equals(name) && "123456".equals(pwd)) {
		StpUtil.login(10001);
		return SaResult.ok("登录成功！");
	}
	return SaResult.error("登录失败！");
}
```

#### 3.3、不想使用`/sso/doLogin`这个接口，想自定义一个API地址？

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


### 4、不同 client 不同登录页

如果你的不同应用覆盖的用户群体差异极大，此时你可能想针对不同的应用跳转到不同的登录页，让每个应用的用户在登录时能够看到当前应用的专属信息，怎么做呢？

首先，保证每个 sso-client 端都配置了不同的 client 标识：


:::tabs
== yaml 风格

``` yaml
sa-token:
    sso-client:
        # 当前 client 标识
        client: sso-client-shop
```

== properties 风格

``` properties
# 当前 client 标识
sa-token.sso-client.client=sso-client-shop
```

:::



然后在 `sso-server` 里为每个系统开发不同的登录页，并在 `configSso` 方法里 `notLoginView` 函数中根据 client 值，返回不同的登录视图：

``` java
// 配置SSO相关参数 
@Autowired
private void configSso(SaSsoServerTemplate ssoServerTemplate) {
	
	// 配置：未登录时返回的View 
	ssoServerTemplate.strategy.notLoginView = () -> {

		String client = SaHolder.getRequest().getParam("client");
		if("sso-client-shop".equals(client)) {
			return new ModelAndView("sa-shop-login.html");
		}
		if("sso-client-video".equals(client)) {
			return new ModelAndView("sa-video-login.html");
		}
		// 更多 ... 
		
		// 都不匹配，返回一个默认的 
		return new ModelAndView("sa-login.html");
	};
	
	// ... 
	
}
```

---

### Sa Token Doc New Docs Sso Sso Dev (`sa-token-doc-new/docs/sso/sso-dev.md`)

---
title: "Sa-Token SSO 代码 API 参考"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,SSO 代码 API 参考,单点登录,SSO"
description: "SSO 代码 API：SaSsoServerUtil 负责 Ticket 增删校验、Client 查询、重定向 URL 构建、ssoLogout 与消息推送；SaSsoClientUtil 可 buildServerAuthUrl，并构建 checkTicket、signout 消息供二次开发。两端工具类分开列出。"
---

# Sa-Token SSO Server端 二次开发用到的所有函数说明 

本篇展示一下 SSO 模块常用的工具类、方法

## Sso-Server 工具类

### Ticket 操作
``` java
// 增删改

// 删除 Ticket
SaSsoServerUtil.deleteTicket(String ticket);

// 根据参数创建一个 ticket 码，并保存
SaSsoServerUtil.createTicketAndSave(String client, Object loginId, String tokenValue);

// 查

// 查询 ticket ，如果 ticket 无效则返回 null
SaSsoServerUtil.getTicket(String ticket);

// 查询 ticket 指向的 loginId，如果 ticket 码无效则返回 null
SaSsoServerUtil.getLoginId(String ticket);

// 查询 ticket 指向的 loginId，并转换为指定类型
SaSsoServerUtil.getLoginId(String ticket, Class<T> cs);

// 校验

// 校验 Ticket，无效 ticket 会抛出异常
SaSsoServerUtil.checkTicket(String ticket);

// 校验 Ticket 码，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除
SaSsoServerUtil.checkTicketParamAndDelete(String ticket);

// 校验 Ticket，无效 ticket 会抛出异常，如果此ticket是有效的，则立即删除
SaSsoServerUtil.checkTicketParamAndDelete(String ticket, String client);

// ticket 索引

// 查询 指定 client、loginId 其所属的 ticket 值
SaSsoServerUtil.getTicketValue(String client, Object loginId);
```



### Client 信息获取 
``` java
// 获取所有 Client
SaSsoServerUtil.getClients();

// 获取应用信息，无效 client 返回 null
SaSsoServerUtil.getClient(String client);

// 获取应用信息，无效 client 则抛出异常
SaSsoServerUtil.getClientNotNull(String client);

// 获取匿名 client 信息
SaSsoServerUtil.getAnonClient();

// 获取所有需要接收消息推送的 Client
SaSsoServerUtil.getNeedPushClients();
```


### 重定向 URL 构建与校验
``` java
// 构建 URL：sso-server 端向 sso-client 下放 ticket 的地址
SaSsoServerUtil.buildRedirectUrl(String client, String redirect, Object loginId, String tokenValue);

// 校验重定向 url 合法性
SaSsoServerUtil.checkRedirectUrl(String client, String url);
```


### 单点注销
``` java
// 指定账号单点注销
SaSsoServerUtil.ssoLogout(Object loginId);

// 指定账号单点注销
SaSsoServerUtil.ssoLogout(Object loginId, SaLogoutParameter logoutParameter, String ignoreClient);
```





### 消息推送 
``` java
// 向指定 Client 推送消息
SaSsoServerUtil.pushMessage(SaSsoClientModel clientModel, SaSsoMessage message);

// 向指定 client 推送消息，并将返回值转为 SaResult
SaSsoServerUtil.pushMessageAsSaResult(SaSsoClientModel clientModel, SaSsoMessage message);

// 向指定 Client 推送消息
SaSsoServerUtil.pushMessage(String client, SaSsoMessage message);

// 向指定 client 推送消息，并将返回值转为 SaResult
SaSsoServerUtil.pushMessageAsSaResult(String client, SaSsoMessage message);

// 向所有 Client 推送消息
SaSsoServerUtil.pushToAllClient(SaSsoMessage message);

// 向所有 Client 推送消息，并忽略掉某个 client
SaSsoServerUtil.pushToAllClient(SaSsoMessage message, String ignoreClient);
```


详情请参考源码：[码云：SaSsoServerUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerUtil.java)



## Sso-Client 工具类


### 构建交互地址 
``` java
// 构建URL：Server端 单点登录授权地址 
SaSsoClientUtil.buildServerAuthUrl(String clientLoginUrl, String back);
```


### 单点注销
``` java
// 指定账号单点注销（loginId 为本地账号 id，框架会按策略转换为认证中心 id）
SaSsoClientUtil.ssoLogout(Object loginId);

// 指定账号单点注销
SaSsoClientUtil.ssoLogout(Object loginId, SaLogoutParameter logoutParameter);
```


### 消息推送 

``` java
// 向 sso-server 推送消息
SaSsoClientUtil.pushMessage(SaSsoMessage message);

// 向 sso-server 推送消息，并将返回值转为 SaResult
SaSsoClientUtil.pushMessageAsSaResult(SaSsoMessage message);

// 构建消息：校验 ticket
SaSsoClientUtil.buildCheckTicketMessage(String ticket, String ssoLogoutCallUrl);

// 构建消息：单点注销
SaSsoClientUtil.buildSignoutMessage(Object loginId, SaLogoutParameter logoutParameter);
```

详情请参考源码：[码云：SaSsoClientUtil.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoClientUtil.java)




## Sso-Server 所有可重写策略 

``` java
// 发送 Http 请求的处理函数
SaSsoServerProcessor.instance.ssoServerTemplate.strategy.sendRequest = url -> {
	// ...
}

// 使用异步模式执行一个任务
SaSsoServerProcessor.instance.ssoServerTemplate.strategy.asyncRun = fun -> {
	// ...
}

// 未登录时返回的 View
SaSsoServerProcessor.instance.ssoServerTemplate.strategy.notLoginView = () -> {
	// ...
}

// SSO-Server端：登录函数
SaSsoServerProcessor.instance.ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {
	// ...
}

//SSO-Server端：在授权重定向之前的通知
SaSsoServerProcessor.instance.ssoServerTemplate.strategy.jumpToRedirectUrlNotice = (redirectUrl) -> {
	// ...
}

// SSO-Server端：在校验 ticket 后，给 sso-client 端追加返回信息的函数
SaSsoServerProcessor.instance.ssoServerTemplate.strategy.checkTicketAppendData = (loginId, result) -> {
	// ...
}
```



## Sso-Client 所有可重写策略

``` java
// 发送 Http 请求的处理函数
SaSsoClientProcessor.instance.ssoClientTemplate.strategy.sendRequest = url -> {
	// ...
}

// 自定义校验 ticket 返回值的处理逻辑 （每次从认证中心获取校验 ticket 的结果后调用）
SaSsoClientProcessor.instance.ssoClientTemplate.strategy.ticketResultHandle = (ctr, back) -> {
	// ...
}

// 转换：认证中心 centerId > 本地 loginId
SaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertCenterIdToLoginId = (centerId) -> {
	// ...
}

// 转换：本地 loginId > 认证中心 centerId
SaSsoClientProcessor.instance.ssoClientTemplate.strategy.convertLoginIdToCenterId = (loginId) -> {
	// ...
}
```

---

### Sa Token Doc New Docs Sso Sso Diff Key (`sa-token-doc-new/docs/sso/sso-diff-key.md`)

---
title: "Sa-Token 不同 SSO Client 配置不同秘钥"
keywords: "Sa-Token,sa-token,satoken,SSO,单点登录,Client秘钥"
description: "为不同 SSO Client 配独立 secret-key：ticket 校验与单点注销 HTTP 签名按应用区分。Client 设 sso-client.client 与 sign.secret-key；Server 重写 getSignTemplate 按 client 取密钥。签名失败先核密钥是否对应。"
---

# 不同 SSO Client 配置不同秘钥

在校验 ticket、单点注销等操作发起的 http 调用时，需要配置秘钥参数，像这样：

:::tabs
== yaml 风格

``` yaml
sa-token: 
    sign:
        # API 接口调用秘钥
        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```

== properties 风格

``` properties
# 接口调用秘钥 
sa-token.sign.secret-key=kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
```

:::



如果 SSO Client 端和 SSO Server 端配置的秘钥不同，则无法调通请求，显示无效签名：
``` js
{
  "code": 500,
  "msg": "无效签名：9f1b453817bfeac56d2f772a66c01eb2",
  "data": null
}
```

如果你有多个 SSO Client，你可能想让每个应用配置不同的秘钥，让它们彼此之间不能互相“冒充”，怎么做呢？

### 1、首先在 SSO Client 端，你需要配置上不同的 Client 标识参数：

例如在 client1 我们配置上：

:::tabs
== yaml 风格

``` yaml
sa-token:
    sso-client:
        # 当前 client 标识
        client: sso-client1
        # ... 
    sign:
        # sso-client1 使用的秘钥 
        secret-key: secret-key-xxxx-1
```

== properties 风格

``` properties
# 当前 client 标识
sa-token.sso-client.client=sso-client1

# sso-client1 使用的秘钥 
sa-token.sign.secret-key=secret-key-xxxx-1
```

:::



在 client2 我们配置上：

:::tabs
== yaml 风格

``` yaml
sa-token:
    sso-client:
        # 当前 client 标识
        client: sso-client2
        # ... 
    sign:
        # sso-client2 使用的秘钥 
        secret-key: secret-key-xxxx-2
```

== properties 风格

``` properties
# 当前 client 标识
sa-token.sso-client.client=sso-client2

# sso-client2 使用的秘钥 
sa-token.sign.secret-key=secret-key-xxxx-2
```

:::



### 2、然后在 SSO Server 端，重写获取秘钥的函数

在 SSO Server 端新建 `CustomSaSsoServerTemplate.java`，继承 `SaSsoServerTemplate`，重写其 `getSignTemplate` 函数：

``` java
/**
 * 自定义 SaSsoServerTemplate 子类 
 */
@Component
public class CustomSaSsoServerTemplate extends SaSsoServerTemplate {

	// 存储所有 client 的秘钥 
    static Map<String, SaSignTemplate> signMap = new HashMap<>();
    static {
        signMap.put("sso-client1", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-1")));
        signMap.put("sso-client2", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-2")));
        signMap.put("sso-client3", new SaSignTemplate(new SaSignConfig("secret-key-xxxx-3")));
		// ... 
    }

    @Override
    public SaSignTemplate getSignTemplate(String client) {
        // 先从自定义的 signMap 中获取
        SaSignTemplate saSignTemplate = signMap.get(client);
        if (saSignTemplate != null) {
            return saSignTemplate;
        }
        // 找不到就返回全局默认的 SaSignTemplate
        return SaManager.getSaSignTemplate();
    }
}
```

至此完成。


### 3、其它注意点

有同学反馈，集成 “不同 SSO Client 配置不同秘钥” 模式后，客户端发起调用 `/sso/getData` 调用时会报如下错误：

``` 
无效签名：5a7fc42836deba12d96527d43c1301ea
```

或者：
```
参与参数签名的秘钥不可为空
```

这大概率是因为在 sso-server 端的 `/sso/getData` 接口在校验签名时忘了加 client 参数导致的，修改为如下代码即可：

``` java
// 示例：获取数据接口（用于在模式三下，为 client 端开放拉取数据的接口）
@RequestMapping("/sso/getData")
public SaResult getData(String apiType, String loginId) {
    System.out.println("---------------- 获取数据 ----------------");
    System.out.println("apiType=" + apiType);
    System.out.println("loginId=" + loginId);

    // ↓↓↓ ⚠️ 重点代码 ↓↓↓
    // 校验签名：只有拥有正确秘钥发起的请求才能通过校验  
    String client = SaHolder.getRequest().getHeader("client");
    SaSsoServerProcessor.instance.ssoServerTemplate.getSignTemplate(client).checkRequest(SaHolder.getRequest());
    // ↑↑↑ ⚠️ 重点代码 ↑↑↑

    // 自定义返回结果（模拟）
    return SaResult.ok()
            .set("id", loginId)
            .set("name", "LinXiaoYu")
            .set("sex", "女")
            .set("age", 18);
}
```

---

### Sa Token Doc New Docs Sso Sso H5 (`sa-token-doc-new/docs/sso/sso-h5.md`)

---
title: "Sa-Token 前后端分离下的整合方案"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,前后端分离下的整合方案,单点登录,SSO"
description: "SSO 前后端分离：把 /sso/login 中转放到 H5。Client 开放 /sso/isLogin、getSsoAuthUrl、doLoginByTicket，并配 CORS。前端拿 ticket 调 doLoginByTicket 换 Token，示例含原生 H5、单应用注销与全端注销。前端保存 Token。"
---

# SSO整合-前后端分离架构下的整合方案

---

## SSO-Client 前后端分离

要在前后端分离的环境中接入 SSO，思路不难，主要的工作是把后端 `/sso/login` 接口的路由中转工作拿到前端来，以`sa-token-demo-sso3-client`为例：

### 1、在 sso-client 后端新建`H5Controller`，开放接口：

``` java
/**
 * 前后台分离架构下集成 SSO 所需的代码 （SSO-Client端）
 */
@RestController
public class H5Controller {

	// 判断当前是否登录
	@RequestMapping("/sso/isLogin")
	public Object isLogin() {
		return SaResult.data(StpUtil.isLogin()).set("loginId", StpUtil.getLoginIdDefaultNull());
	}
	
	// 返回SSO认证中心登录地址 
	@RequestMapping("/sso/getSsoAuthUrl")
	public SaResult getSsoAuthUrl(String clientLoginUrl) {
		String serverAuthUrl = SaSsoClientUtil.buildServerAuthUrl(clientLoginUrl, "");
		return SaResult.data(serverAuthUrl);
	}
	
	// 根据 ticket 进行登录
	@RequestMapping("/sso/doLoginByTicket")
	public SaResult doLoginByTicket(String ticket) {
		SaCheckTicketResult ctr = SaSsoClientProcessor.instance.checkTicket(ticket);
		StpUtil.login(ctr.loginId, new SaLoginParameter()
				.setTimeout(ctr.remainTokenTimeout)
				.setDeviceId(ctr.deviceId)
		);
		return SaResult.data(StpUtil.getTokenValue());
	}

}
```


### 2、增加跨域处理策略 

``` java
/**
 * [Sa-Token 权限认证] 配置类
 */
@Configuration
public class SaTokenConfigure {

    /**
     * CORS 跨域处理策略
     */
    @Bean
    public SaCorsHandleFunction corsHandle() {
        return (req, res, sto) -> {
            res.
                    // 允许指定域访问跨域资源
                    setHeader("Access-Control-Allow-Origin", "*")
                    // 允许所有请求方式
                    .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
                    // 有效时间
                    .setHeader("Access-Control-Max-Age", "3600")
                    // 允许的header参数
                    .setHeader("Access-Control-Allow-Headers", "*");

            // 如果是预检请求，则立即返回到前端
            SaRouter.match(SaHttpMethod.OPTIONS)
                    .free(r -> System.out.println("--------OPTIONS预检请求，不做处理"))
                    .back();
        };
    }

}
```

详细参考：[解决跨域问题](/fun/cors-filter)


### 3、新建前端项目 
任意文件夹新建前端项目：`sa-token-demo-sso-client-h5`，在根目录添加测试文件：`index.html`
```
/* Detailed source-code truncated for AI context efficiency. */
```

### 4、添加单点登录登录中转页

在根目录创建文件：`sso-login.html`

``` html
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Sa-Token-SSO-Client端-登录中转页页</title>
		<style type="text/css">
		
		</style>
	</head>
	<body>
		<div class="login-box">
			加载中 ... 
		</div>
		<script src="sso-common.js"></script>
		<script type="text/javascript">
		
			var back = getParam('back', '/');
			var ticket = getParam('ticket');
			
			window.onload = function(){
				if(ticket) {
					doLoginByTicket(ticket);
				} else {
					goSsoAuthUrl();
				}
			}
			
			// 重定向至认证中心 
			function goSsoAuthUrl() {
				ajax('/sso/getSsoAuthUrl', {clientLoginUrl: location.href}, function(res) {
					location.href = res.data;
				})
			}
		
			// 根据ticket值登录 
			function doLoginByTicket(ticket) {
				ajax('/sso/doLoginByTicket', {ticket: ticket}, function(res) {
					localStorage.setItem('satoken', res.data);
					location.href = decodeURIComponent(back); 
				})
			}
			
		</script>
	</body>
</html>
```


### 5、添加公共 js文件
新建 `sso-common.js`：

```
/* Detailed source-code truncated for AI context efficiency. */
```


### 6、测试运行
先启动 Server 服务端与 Client 服务端，再随便找个能预览html的工具打开前端项目（比如[HBuilderX](https://www.dcloud.io/hbuilderx.html)），测试流程与一体版一致，暂不赘述。




::: tip 另附其它技术栈的前后端分离 demo 示例：
- sso-client 前后端分离 - 原生h5：[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-h5) 
- sso-client 前后端分离 - vue2：[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue2) 
- sso-client 前后端分离 - vue3：[源码链接](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-client-vue3) 
:::


## SSO-Server 前后端分离

解决思路与 SSO-Client 一样，我们需要把原本在 “后端处理的授权重定向逻辑” 拿到前端来实现。

由于集成代码与 Client 端类似，这里暂不贴详细代码，我们可以下载官方仓库，里面有搭建好的demo。

使用前端 ide 导入项目 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server-h5`，浏览器访问 `sso-auth.html` 页面：

<img class="s-w-sh" src="/big-file/doc/sso/sso-type2-server-h5-auth.png" alt="SSO-Server 前后端分离授权页 sso-auth.html" />

复制上述地址，将其配置到 Client 端的配置项 `sa-token.sso-client.auth-url` ，例如：

:::tabs
== yaml 风格

``` yaml
sa-token: 
	sso-client: 
        # sso-server 端主机地址
        server-url: http://sa-sso-server.com:9000
        # 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）
	    auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
```

== properties 风格

``` properties
# SSO-Server端 统一认证地址 
sa-token.sso-client.server-url=http://sa-sso-server.com:9000
# 在 sso-server 端前后端分离时需要单独配置 auth-url 参数（上面的不要注释，auth-url 配置项和 server-url 要同时存在）
sa-token.sso-client.auth-url=http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
```

:::



然后我们启动项目 sso-server 与 sso-client ，按照之前的测试步骤访问：
[http://sa-sso-client1.com:9003/](http://sa-sso-client1.com:9003/)，即可以前后端分离模式完成 SSO-Server 端的授权登录。

---

### Sa Token Doc New Docs Sso Sso Home Jump (`sa-token-doc-new/docs/sso/sso-home-jump.md`)

---
title: "Sa-Token 平台中心跳转模式"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,平台中心跳转模式,单点登录,SSO"
description: "SSO 平台中心跳转：把认证中心做成门户首页，配置 sso-server.home-route。入口写成 /sso/auth?client=&redirect=子系统/sso/login?back=首页；未登录先去 /sso/auth，HomeController 渲染入口，登录后点击即可免登录进入各业务子系统。"
---

# SSO 平台中心跳转模式，点连接跳入子系统

--- 

有的时候，我们需要把 sso-server 搭建成一个平台中心，效果图大致如下：

<img class="s-w-sh" src="/big-file/doc/sso/sso-home-jump.png" alt="SSO 平台中心首页效果图（各子系统入口链接）" />

如图所示，用户先从 sso-server 登录进入平台首页，在首页上有各个子系统的进入链接，用户点击链接进入子系统（免登录）。

怎么做到如上效果呢？当然，加个超链接跳到子系统并不难，难点在于跳转的同时我们需要让用户自动登录上子系统，从而达到：平台中心一处登录，所有子系统无障碍通行的效果。

怎么做到跳转的时候自动登录呢？直接跳转肯定是不会自动登录的，我们需要对链接改造一下：

假设子系统的地址是：

``` url
http://sa-sso-client1.com:9003/
```

那么我们改造后的地址就是：

``` url
/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/
```

格式形如：`/sso/auth?client={client标识}&redirect=${子系统首页}/sso/login?back=${子系统首页}`

--- 

### 完整代码示例：

1、在 sso-server 中配置 `home-route` 字段：

:::tabs
== yaml 风格

``` yaml
# Sa-Token 配置
sa-token:
    # SSO-Server 配置
    sso-server:
        # 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址
        home-route: /home
```

== properties 风格

``` properties
# 主页路由：在 /sso/auth 登录页不指定 redirect 参数时，默认跳转的地址
sa-token.sso-server.home-route: /home
```

:::




2、在 sso-server 中添加 `HomeController`，作为平台中心首页：

``` java
/**
 * SSO 平台中心模式示例，跳连接进入子系统 
 */
@RestController
public class HomeController {
	// 平台化首页
    @RequestMapping({"/", "/home"})
    public Object index() {
        // 如果未登录，则先去登录
        if(!StpUtil.isLogin()) {
            return SaHolder.getResponse().redirect("/sso/auth");
        }

        // 拼接各个子系统的地址，格式形如：/sso/auth?client=xxx&redirect=${子系统首页}/sso/login?back=${子系统首页}
        String link1 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client1.com:9003/sso/login?back=http://sa-sso-client1.com:9003/";
        String link2 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client2.com:9003/sso/login?back=http://sa-sso-client2.com:9003/";
        String link3 = "/sso/auth?client=sso-client3&redirect=http://sa-sso-client3.com:9003/sso/login?back=http://sa-sso-client3.com:9003/";

        // 组织网页结构返回到前端
        String title = "<h2>SSO 平台首页 (平台中心模式)</h2>";
        String client1 = "<p><a href='" + link1 + "' target='_blank'> 进入Client1系统 </a></p>";
        String client2 = "<p><a href='" + link2 + "' target='_blank'> 进入Client2系统 </a></p>";
        String client3 = "<p><a href='" + link3 + "' target='_blank'> 进入Client3系统 </a></p>";

        return title + client1 + client2 + client3;
    }
}
```


### 测试访问

启动项目，访问：[http://sa-sso-server.com:9000](http://sa-sso-server.com:9000)

首次访问，因为我们没有登录，所以会被重定向到 `/sso/auth` 登录页，我们登录上之后，便会跳转到平台中心首页：

<img class="s-w-sh" src="/big-file/doc/sso/sso-home-jump-do.png" alt="SSO 平台中心登录后首页（可点击进入子系统）" />

依次点击三个链接，便可在跳转的同时自动登录上子系统。

---

### Sa Token Doc New Docs Sso Sso Nosdk (`sa-token-doc-new/docs/sso/sso-nosdk.md`)

---
title: "Sa-Token NoSdk、ReSdk 模式与非 java 项目"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,NoSdk、ReSdk 模式与非 java 项目,单点登录,SSO"
description: "SSO NoSdk：不引 sa-token-sso，用 HTTP 调认证中心，Servlet 模拟 /sso/login、logout、pushC，示例见 sso3-client-nosdk。ReSdk 只重写关键步骤即可拿全量能力。PHP、.NET、Node 等非 Java 同样对照开放 API 对接。"
---

# SSO整合 - NoSdk、ReSdk 模式与非 java 项目

---

经常有小伙伴提问：客户端不使用 Sa-Token，能否接入 SSO 认证中心？当然是可以的。

SSO-Server 所有接口都是通过 http 协议开放的，这意味着原则上只要一个语言支持 http 请求调用就可以对接 SSO-Server，参考： [SSO 认证中心开放接口](/sso/sso-apidoc)

### NoSdk 模式

NoSdk 模式（不使用SDK）：通过 http 工具类调用接口的方式来对接 SSO-Server。

参考 demo：[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk)

该 demo 假设应用端没有使用任何“权限认证框架”，使用最基础的 ServletAPI 进行会话管理，模拟了 `/sso/login`、 `/sso/logout`、 `/sso/pushC` 三个接口的处理逻辑。

::: info NoSdk 模式优缺点
- 1、支持客户端使用任意技术栈。
- 2、代码简单易懂，流程直观清晰。
- 3、用 http 工具类模拟 Sa-Token SSO 内部实现，样版代码较多，略显冗余。
:::



### ReSdk 模式

ReSdk 模式（重写SDK部分方法）：通过重写框架关键步骤点，来对接 SSO-Server。

参考 demo：[sa-token-demo-sso3-client-resdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk)

::: info ReSdk 模式优缺点
- 1、支持客户端使用任意技术栈。
- 2、仅重写少量部分关键代码，即可完成对接。几乎可以得到 Sa-Token SSO 模块全量能力。
- 3、此模式需要对 Sa-Token SSO 内部实现较为熟悉，才可以驾驭。
:::




### 非 java 语言项目

sso-server 的所有接口均以 http 协议对外开放，因此原则上支持任何语言对接，只要这个语言支持 http 请求调用。

例如 PHP、.NET、Node.js 等语言的项目，无法集成 Sa-Token，同上，也可以通过 http 工具类调用接口的方式来对接 SSO-Server。

建议各位同学先搞懂 NoSdk 模式的对接流程，再参照 [SSO 认证中心开放接口](/sso/sso-apidoc) 章节进行对接。

---

### Sa Token Doc New Docs Sso Sso Pro (`sa-token-doc-new/docs/sso/sso-pro.md`)

---

title: "Sa-Sso-Pro 单点登录商业版"
keywords: "Sa-Sso-Pro,SSO,单点登录,Sa-Token,商业版"
noindex: true
description: "Sa-Sso-Pro 商业版：在开源 SSO 上提供账号增删改查、登录日志、用户报表与 Client 接入域名配置等企业认证中心能力。付费完整项目下载即可启动，绝大多数功能无需二次开发即可使用，可大大缩短统一认证中心接入周期。与免费 Sa-Token 框架独立，不购买也不影响开源使用，二者仍独立迭代。"
---

# Sa-Sso-Pro 单点登录商业版

### 项目介绍

根据 Sa-Token SSO 模块文档，以及官网提供的源码示例，您可以很方便的搭建一个SSO模式的认证Demo。



然而，要真正开发一个商业级项目的认证中心系统，绝非一朝一夕可以搭建完毕，其必不可少的一些功能，
比如：用户账号增删改查维护、登录日志统计、新增用户数据报表、新增 Client 应用接入域名配置……等等，
仍需要大量的开发时间。

为此，我们特意准备了项目：[[ Sa-Sso-Pro 单点登录商业版 ]](https://sa-max.cn?way=st_md)，
项目集成了单点登录常见技术点， 绝大多数功能无需二次开发，直接可用，<b style="color: #FF5722;">可大大缩短您的项目接入单点登录的开发周期</b>。


### 释疑

##### 1、Sa-Sso-Pro 是收费项目吗？与 Sa-Token 有什么不同？

`Sa-Sso-Pro` 是付费项目，暂不开放源码，如需使用需要购买项目授权，您可以在其主页了解更多详细信息。

`Sa-Sso-Pro` 与 `Sa-Token` 的区别，简单来讲：
- `Sa-Token` 是一个框架，需要在项目中通过 pom.xml 引入
- `Sa-Sso-Pro` 是一个完整项目，下载源码后可直接启动 


##### 2、Sa-Token 会不会在某一天收费？导致我们项目无法正常运行？
首先我们需要了解一点：**已经发布到 Maven 中央仓库的代码，是不可以删除的**，所以这部分代码是无法做到收费的 

其次，像中间件框架，业界没有收费的先例，也没有对应的商业模式，一般的付费项目都是一些成型的完整项目，以解决特定场景的业务需求为目的，
比如：聊天通信、刷脸认证、短信验证码、聚合支付……等等。

Sa-Sso-Pro 并非随意收费，只有当您的系统需要 **统一认证中心** 时您才会用到它，花一笔小钱节省大量开发工期，整体来看，这是非常划算的。

另外：即使您没有购买 `Sa-Sso-Pro`，也不会影响到您对 `Sa-Token` 的使用，举个例子：MySQL具有社区版与企业版，即使您没有购买其付费版，也不会影响到您对免费 MySql 的使用。



##### 3、Sa-Token 团队日后的主要精力是不是放在 Sa-Sso-Pro 上，降低对 Sa-Token 的支持？毕竟 Sa-Token 是免费的！

答案是不会。

再次强调一下：`Sa-Token` 与 `Sa-Sso-Pro` 是两个独立的项目，两者互不影响。
付费项目的出现不会降低对 `Sa-Token` 的支持，`Sa-Token`将会按照原有的发展继续升级迭代。

实际结果可能会恰恰相反：有了盈利来源，`Sa-Token`将发展的更快。

---

### Sa Token Doc New Docs Sso Sso Questions (`sa-token-doc-new/docs/sso/sso-questions.md`)

---
title: "SSO 单点登录常见问题"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,常见问题总结,单点登录,SSO"
description: "SSO 常见问题：返回 not handle 多为路由写错或 is-slo 未开；模式二 Ticket 无效先核 Client 与 Server 是否同 Redis；模式三勿重复校验 ticket；模式一检查 cookie.domain 与 is-read-cookie；Session 实体两端包名须一致。"
---

# Sa-Token-SSO整合-常见问题总结

SSO 集成常见问题整理

[[toc]]

--- 


### 问：在模式一与模式二中，Client端 必须通过 Alone-Redis 插件来访问 Redis 吗？
答：不必须，只是推荐，权限缓存与业务缓存分离后会减少 `SSO-Redis` 的访问压力，且可以避免多个 `Client端` 的缓存读写冲突。


### 问：搭建好 sso-server 或 sso-client 服务后，访问返回：`{"msg": "not handle"}`。

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

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

地址写错了，框架就不会处理这个请求，会直接返回 `{"msg": "not handle"}`，所有开放地址可参考：[SSO 开放接口](/sso/sso-apidoc)

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

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

都需要在配置文件配置：`sa-token.sso-server.is-slo=true`(client端为 `sa-token.sso-client.is-slo=true` )后，才会打开。


### 问：我参照文档搭建SSO-Client，一直提示：Ticket无效，请问怎么回事？
如果使用的是模式二，出现此异常概率最大的原因是因为 `Client` 与 `Server` 没有连接同一个Redis，SSO模式二中两者必须连接同一个 Redis 才可以登录成功。

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

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

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

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


### 问：模式一或者模式二报错：Could not write JSON: No serializer found for class com.pj.sso.SysUser and no properties discovered to create BeanSerializer 

一般是因为在 sso-server 端往 session 上写入了某个实体类（比如 User），而在 sso-client 端没有这个实体类，导致反序列化失败。

解决方案：在 sso-client 也新建上这个类，而且包名需要与 sso-server 端的一致（直接从 sso-server 把实体类复制过来就好了）



### 在测试模式一时，出现一些难以理解的现象 

测试模式一时，三种异常现象：

1、在 sso-client 端点击登录，可以成功跳转到 sso-server 端，登录后可以跳回 sso-client 端，但显示 sso-client 端未登录	
- 原因：sso-server 后端没有配置 sa-token.cookie.domain 值。

2、在 sso-client 端点击登录，可以成功跳转到 sso-server 端，登录页面刷新一下，并没有跳转回 sso-client 端，依然提示让你登录
- 可能1：sso-server 后端配置了错误的 sa-token.cookie.domain 值。	
- 可能2：sso-server 后端没有打开 sa-token.is-read-cookie 值。（测试模式二三时发生这种现象，有时候也是因为这个）

3、在 sso-client 端点击登录，页面只是闪了一下，肉眼没有观察到页面跳转，页面也没有显示登录上。

原因：sso-server 的页面存储了不带 . 的有效 Cookie，为什么会这样：常常是因为测试模式二三后，没有清除redis记录或者浏览器记录，直接再开始测试模式一，
模式二三登录成功后遗留的有效cookie，影响了模式一的行为逻辑。

解决方案：
- 1、手动清空 redis里的所有数据，
- 2、或者手动清空 sso-server 域名下的所有 Cookie 
- 3、换一个新的干净浏览器来测试。
	
	



### 问：模式三配置一堆 xxx-url ，有办法简化一下吗？
可以使用 `sa-token.sso-client.server-url` 来简化：

配置含义：配置 Server 端主机总地址，拼接在 authUrl、getDataUrl、signoutUrl、pushUrl 属性前面，用以简化各种 url 配置。

在开发 SSO 模块时，我们需要在 sso-client 配置认证中心的各种地址，特别是在模式三下，一般代码会变成这样：

``` yaml
sa-token: 
    sso-client: 
        # SSO-Server端 统一认证地址 
        auth-url: http://sa-sso-server.com:9000/sso/auth
        # 单点注销地址 
        slo-url: http://sa-sso-server.com:9000/sso/signout
        # SSO-Server端 查询数据地址 
        get-data-url: http://sa-sso-server.com:9000/sso/getData
```

一堆 xxx-url 配置比较繁琐，且含有大量重复字符，现在我们可以将其简化为：
``` yaml
sa-token: 
    sso-client: 
        server-url: http://sa-sso-server.com:9000
```

只要你配置了 `server-url` 地址，Sa-Token 就可以自动拼接出其它四个地址：

**例1，使用 server-url 简化：**
- 你配置的 server-url 值是：`http://sa-sso-server.com:9000`。
- 框架拼接出的 auth-url 值就是：`http://sa-sso-server.com:9000/sso/auth`，其它三个 url 配置项同理。

**例2，使用 server-url + auth-url 简化：**
- 你配置的 server-url 值是：`http://sa-sso-server.com:9000`，auth-url 是：`/sso/auth2`。
- 框架拼接出的 auth-url 值就是：`http://sa-sso-server.com:9000/sso/auth2`，其它三个 url 配置项同理。

**例3，auth-url 地址以 http 字符开头：**
- 你配置的 server-url 值是：`http://sa-sso-server.com:9000`，auth-url 是：`http://my-site.com/sso/auth2`。
- 此时框架只以 auth-url 值为准，得到的 auth-url 值是：`http://my-site.com/sso/auth2`，其它三个 url 配置项同理。


### 问：我接手了一个项目，里面集成了 Sa-Token SSO ，请问怎么快速分辨它用的模式几？

**方法一：看代码注释。**

如果开发这个项目的人没有写清楚注释那就 gg 了。

**方法二，根据配置项来分析，例如：**

- 先看配置项 `sa-token.cookie.domain`，如果此配置项有值，一般是在使用模式一开发，否则就是模式二或者模式三。
- 再看配置项 `sa-token.sso-client.is-http` ，如果有值且为 true，一般是在使用模式三，否则就是模式二。

**方法三，根据配置项 `sa-token.sso-client.mode` 的提示来判断**

`sa-token.sso-client.mode` 是框架预留的约定型配置项，此配置项不对代码逻辑产生任何影响，只为系统做一个标记，标注此系统用到了SSO的哪个模式。

例如你可以将其配置为 `sa-token.sso-client.mode=client-2`，代表当前系统为 sso-client 端，使用 SSO 模式二来对接。

需要注意，这个配置项不是必须的，你不写也不会对代码造成任何影响，只有在你需要为系统做一个明确的标记时才需要去配置它，方便后人阅读代码时快速分析使用的模式。

例如我们可以使用以下约定：

- `sa-token.sso-client.mode=client-2`：代表当前系统为 sso-client 端，使用 SSO 模式二来对接。
- `sa-token.sso-client.mode=client-2,h5`：代表当前系统为 sso-client 端，使用 SSO 模式二来对接，并且是前后端分离模式。
- `sa-token.sso-server.mode=server-123`：代表当前系统为 sso-server 端，同时开放了 SSO 模式一、模式二、模式三。
- `sa-token.sso-server.mode=server-2,client-2`：代表当前系统既是 sso-server 端，又是 sso-client 端，使用模式二来对接。
- 等等等等...

此配置项可以是任意字符串，你也可以自己整理一套合适的表达规则。






### 问：SSO模式二或模式三，第一个 client 登录成功之后再访问其它两个 client 不会自动登录，需要点一下登录按钮才会登录上？
答：这是正常现象，系统 1 登录成功之后，系统 2 与系统 3 需要点击登录按钮，才会登录成功。

> 第一个系统，需要：点击 [登录] 按钮 -> 跳转到登录页 -> 输账号密码 -> 登录成功 <br>
> 第二个系统，需要：点击 [登录] 按钮 -> 登录成功 <br>
> 第三个系统，需要：点击 [登录] 按钮 -> 登录成功 
> 
> （系统二、三 免去重复跳转登录页输入账号密码的步骤）


### 追问：那我是否可以设计成不需要点登录按钮的，只要访问页面，它就能登录成功？
可以的。

其实思路很简单，我们只需要给 client 项目加个过滤器，拦截所有请求，只要检测到未登录就将其重定向至登录页面：

``` java
/**
 * Sa-Token 配置类
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    /** 注册 [Sa-Token全局过滤器] */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                .addInclude("/**")
                .addExclude("/sso/*", "/favicon.ico")   // 这里需要注意排除掉 /sso/* 相关请求不拦截，否则就会触发无限重定向
                .setAuth(obj -> {
                    /*
                     * 这里会被分为两种情况：
                     *  情况1：这个请求在当前 client 已经登录，此时会顺利进入网站
                     *  情况2：这个请求在当前 client 尚未登录，此时会被拦截，重定向至当前系统的 /sso/login?back=当前地址
                     *
                     *  情况2会带领着用户继续重定向至 sso-server 认证中心，此时又分为两种情况：
                     *      情况2.1：此用户在 sso-server 尚未登录，此时会停留在登录页面，开始输入账号密码进行登录
                     *      情况2.2：此用户在 sso-server 已经登录（这证明此用户已经在其它至少一个 sso-client 处完成了登录）
                     *              此时用户会继续重定向回当前 client，并携带 ticket 参数，完成登录。
                     */
                    if(StpUtil.isLogin() == false) {
                        String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
                        SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back));
                        SaRouter.back();
                    }
                })
                ;
    }
}
```


更多登录姿势可以参考 [[何时引导用户去登录]](/sso/sso-custom-login) 给出的建议进行设计。



### 问：Client 信息可以做成从数据库读取的吗？
可以，自定义 `SaSsoServerTemplate` 实现类，重写 `getClient` 与 `getClients` 方法即可：
``` java
/**
 * 重写 SaSsoServerTemplate 部分方法，增强功能
 */
@Component
public class CustomSaSsoServerTemplate extends SaSsoServerTemplate {

    // 获取指定 client 的配置信息
    @Override
    public SaSsoClientModel getClient(String client) {
        if("sso-client1".equals(client)) {
            SaSsoClientModel scm = new SaSsoClientModel();
            scm.setAllowUrl("sso-client1");
            scm.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor");
            return scm;
        }

        // ...

        return null;
    }

    // 返回所有 client 信息
    @Override
    public List<SaSsoClientModel> getClients() {
        // 模拟示例代码，真实项目可改为从数据查询

        SaSsoClientModel scm1 = new SaSsoClientModel();
        scm1.setAllowUrl("sso-client1");
        scm1.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor");

        SaSsoClientModel scm2 = new SaSsoClientModel();
        scm2.setAllowUrl("sso-client2");
        scm2.setSecretKey("kQwIOrYvnXmSDkwEiFngrKidMcdrgKor");

        // ...

        return Arrays.asList(scm1, scm2);
    }

}
```


### 问：如果 sso-client 端我没有集成 sa-token-sso，如何对接？
需要手动调用 http 请求来对接 sso-server 开放的接口，参考示例：
[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk)


### 问：如果 sso-client 端不是 java语言，可以对接吗？
可以，只不过有点麻烦，基本思路和上个问题一致，需要手动调用 http 请求来对接 sso-server 开放的接口，参考：
[SSO-Server 认证中心开放接口](/sso/sso-apidoc)


### 问：将旧有系统改造为单点登录时，应该注意哪些？
答：建议不要把其中一个系统改造为SSO服务端，而是新起一个项目作为 SSO-Server 端，所有旧有项目全部作为 Client 端与此对接。


### 问：怎么在一个项目里同时搭建 sso-server 和 sso-client？

难点在于解决两边的路由冲突，示例代码：

``` java
// Sa-Token SSO Controller 
@RestController
public class SsoController {
	
	// 处理 SSO-Server 端所有请求 
	@RequestMapping({"/sso/auth", "/sso/doLogin", "/sso/signout", "/sso/pushS"})
	public Object ssoServerRequest() {
		return SaSsoServerProcessor.instance.dister();
	}
	
	// 处理 SSO-Client 端所有请求 
	@RequestMapping({"/sso/login", "/sso/logout", "/sso/logoutCall", "/sso/pushC"})
	public Object ssoClientRequest() {
		return SaSsoClientProcessor.instance.dister();
	}
	
	// 配置SSO相关参数 
	@Autowired
	private void configSsoServer(SaSsoServerTemplate ssoServerTemplate) {
		// SSO Server 配置代码，参考文档前几章 ... 
	}
	@Autowired
	private void configSsoClient(SaSsoClientTemplate ssoClientTemplate) {
		// SSO Client 配置代码，参考文档前几章 ... 
	}
	
}
```


### 问：我一个项目里有两套账号体系，都需要单点登录，怎么在一个项目里同时搭建两个 sso-server 服务？

首先推荐你不要在一个项目里同时搭建两个 sso-server，建议创建两个项目，分别搭建各自的 sso-server 服务。

如果一定要在一个项目中搭建两套 sso-server 服务，参考方案如下：

第一套，还是用前面几章文档给出的示例代码。

第二套，修改一些参数属性，使之与第一套不产生冲突，参考代码如下：

```
/* Detailed source-code truncated for AI context efficiency. */
```



<br/>

--- 

<details>
<summary>还有其它问题？</summary>
<p>
可以加群反馈一下，比较典型的问题我们解决之后都会提交到此页面方便大家快速排查
</p>
</details>

---

### Sa Token Doc New Docs Sso Sso Server (`sa-token-doc-new/docs/sso/sso-server.md`)

---
title: "Sa-Token 搭建统一认证中心：SSO-Server"
keywords: "Sa-Token,sa-token,satoken,Sa-Token文档,搭建统一认证中心：SSO-Server,单点登录,SSO"

description: "搭建 Sa-Token SSO-Server：引入 sa-token-sso、Redis、Thymeleaf 与 Forest（模式三推送）。开放 /sso/*，配置 notLoginView 与 doLoginHandle。官方 demo 是三种 SSO 模式对接前必须先完成的认证中心，依赖可测完再删减。"
---

# 搭建统一认证中心 SSO-Server  

在开始SSO三种模式的对接之前，我们必须先搭建一个 SSO-Server 认证中心 

::: tip demo
搭建示例在官方仓库的 `/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`，如遇到难点可结合源码进行测试学习。[Sa-Token 集成示例大全下载](/more/download-demos) ，demo里有制作好的登录页面 
:::

--- 

### 1、添加依赖 
创建 SpringBoot 项目 `sa-token-demo-sso-server`，在引入 SpringBoot 依赖的基础上，继续引入：

:::tabs
== Maven 方式

``` xml 

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-boot-starter</artifactId>
	<version>${sa.top.version}</version>
</dependency>


<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-sso</artifactId>
	<version>${sa.top.version}</version>
</dependency>
        

<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-redis-template</artifactId>
	<version>${sa.top.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>


<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>


<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-forest</artifactId>
	<version>${sa.top.version}</version>
</dependency>
```

== Gradle 方式

``` gradle
// Sa-Token 权限认证，在线文档：https://sa-token.com
implementation 'cn.dev33:sa-token-spring-boot-starter:${sa.top.version}'

// Sa-Token 插件：整合SSO
implementation 'cn.dev33:sa-token-sso:${sa.top.version}'

// Sa-Token 整合 RedisTemplate
implementation 'cn.dev33:sa-token-redis-template:${sa.top.version}'
implementation 'org.apache.commons:commons-pool2'

// 视图引擎（在前后端不分离模式下提供视图支持）
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// Sa-Token 插件：整合 Forest 请求工具 (模式三需要通过 http 请求推送消息)
implementation 'cn.dev33:sa-token-forest:${sa.top.version}'
```

:::


> 也可选用 `sa-token-rest-template`（Spring Boot 2/3/4）或 `sa-token-rest-client`（Spring Boot 3.2+）作为 HTTP 请求处理器，详见：[HTTP 请求扩展](/plugin/http-extend)。


::: info 引包简化
除了 `sa-token-spring-boot-starter` 和 `sa-token-sso` 以外，其它包都是可选的：

- 在 SSO 模式三时 Redis 相关包是可选的。
- 在前后端分离模式下可以删除 thymeleaf 相关包。
- 在不需要 SSO 模式三单点注销的情况下可以删除 http 工具包。

建议先完整测试三种模式之后再对 pom 依赖进行酌情删减。
:::


### 2、开放认证接口  
新建 `SsoServerController`，用于对外开放接口：

``` java
/**
 * Sa-Token-SSO Server端 Controller 
 */
@RestController
public class SsoServerController {

	/**
	 * SSO-Server端：处理所有SSO相关请求 
	 * 		http://{host}:{port}/sso/auth			-- 单点登录授权地址
	 * 		http://{host}:{port}/sso/doLogin		-- 账号密码登录接口，接受参数：name、pwd
	 * 		http://{host}:{port}/sso/signout		-- 单点注销地址（isSlo=true时打开）
	 */
	@RequestMapping("/sso/*")
	public Object ssoRequest() {
		return SaSsoServerProcessor.instance.dister();
	}

	/**
	 * 配置SSO相关参数 
	 */
	@Autowired
	private void configSso(SaSsoServerTemplate ssoServerTemplate) {
		// 配置：未登录时返回的View 
		ssoServerTemplate.strategy.notLoginView = () -> {
			// 简化模拟表单
			String doLoginCode =
					"fetch(`/sso/doLogin?name=${document.querySelector('#name').value}&pwd=${document.querySelector('#pwd').value}`) " +
					" .then(res => res.json()) " +
					" .then(res => { if(res.code === 200) { location.reload() } else { alert(res.msg) } } )";
			String res =
					"<h2>当前客户端在 SSO-Server 认证中心尚未登录，请先登录</h2>" +
					"用户：<input id='name' /> <br> " +
					"密码：<input id='pwd' /> <br>" +
					"<button onclick=\"" + doLoginCode + "\">登录</button>";
			return res;
		};
		
		// 配置：登录处理函数 
		ssoServerTemplate.strategy.doLoginHandle = (name, pwd) -> {
			// 此处仅做模拟登录，真实环境应该查询数据库进行登录 
			if("sa".equals(name) && "123456".equals(pwd)) {
				StpUtil.login(10001);
				return SaResult.ok("登录成功！").setData(StpUtil.getTokenValue());
			}
			return SaResult.error("登录失败！");
		};
	}
	
}
```

注意：在`doLoginHandle`函数里如果要获取 name, pwd 以外的参数，可通过`SaHolder.getRequest().getParam("xxx")`来获取。


全局异常处理：
``` java
@RestControllerAdvice
public class GlobalExceptionHandler {
	// 全局异常拦截 
	@ExceptionHandler
	public SaResult handlerException(Exception e) {
		e.printStackTrace(); 
		return SaResult.error(e.getMessage());
	}
}
```


### 3、application.yml配置

:::tabs
== yaml 风格

``` yml
# 端口
server:
    port: 9000

# Sa-Token 配置
sa-token: 
    # 打印操作日志
    is-log: true
	
    # SSO-模式一相关配置  (非模式一不需要配置) 
    # cookie: 
        # 配置 Cookie 作用域 
        # domain: stp.com 
        
    # SSO-Server 配置
    sso-server:
        # Ticket有效期 (单位: 秒)，默认五分钟 
        ticket-timeout: 300
        # 应用列表：配置接入的应用信息
        clients:
            # 应用 sso-client1：采用模式一对接 (同域、同Redis)
            sso-client1:
                client: sso-client1
                allow-url: "*"
            # 应用 sso-client2：采用模式二对接 (跨域、同Redis)
            sso-client2:
                client: sso-client2
                allow-url: "*"
                secret-key: SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
            # 应用 sso-client3：采用模式三对接 (跨域、跨Redis)
            sso-client3:
                # 应用名称
                client: sso-client3
                # 允许授权地址
                allow-url: "*"
                # 是否接收消息推送
                is-push: true
                # 消息推送地址
                push-url: http://sa-sso-client1.com:9003/sso/pushC
                # 接口调用秘钥 (如果不配置则使用全局默认秘钥)
                secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
        
spring: 
    # Redis配置 （SSO模式一和模式二使用Redis来同步会话）
    redis:
        # Redis数据库索引（默认为0）
        database: 1
        # Redis服务器地址
        host: 127.0.0.1
        # Redis服务器连接端口
        port: 6379
        # Redis服务器连接密码（默认为空）
        password: 
```

== properties 风格

``` properties
# 端口
server.port=9000

################## Sa-Token 配置 ##################
# 打印操作日志
sa-token.is-log=true

# SSO-模式一相关配置  (非模式一不需要配置) 
# 配置 Cookie 作用域 
# sa-token.cookie.domain=stp.com

# SSO-Server 配置
# Ticket有效期 (单位: 秒)，默认五分钟 
sa-token.sso-server.ticket-timeout=300

# 应用列表：配置接入的应用信息
# 应用 sso-client1：采用模式一对接 (同域、同Redis)
sa-token.sso-server.clients.sso-client1.client=sso-client1
sa-token.sso-server.clients.sso-client1.allow-url=*

# 应用 sso-client2：采用模式二对接 (跨域、同Redis)
sa-token.sso-server.clients.sso-client2.client=sso-client2
sa-token.sso-server.clients.sso-client2.allow-url=*
sa-token.sso-server.clients.sso-client2.secret-key=SSO-C2-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor

# 应用 sso-client3：采用模式三对接 (跨域、跨Redis)
# 应用名称
sa-token.sso-server.clients.sso-client3.client=sso-client3
# 允许授权地址
sa-token.sso-server.clients.sso-client3.allow-url=*
# 是否接收消息推送
sa-token.sso-server.clients.sso-client3.is-push=true
# 消息推送地址
sa-token.sso-server.clients.sso-client3.push-url=http://sa-sso-client1.com:9003/sso/pushC
# 接口调用秘钥 (如果不配置则使用全局默认秘钥)
sa-token.sso-server.clients.sso-client3.secret-key=SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor

# Redis配置 （SSO模式一和模式二使用Redis来同步会话）
# Redis数据库索引（默认为0）
spring.redis.database=1
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码（默认为空）
spring.redis.password=
```

:::


注意点：`sa-token.sso-server.clients.xxx.allow-url`为了方便测试配置为`*`，线上生产环境一定要配置为详细 URL 地址 （之后的章节我们会详细阐述此配置项）


### 4、创建启动类
``` java 
@SpringBootApplication
public class SaSsoServerApplication {
	public static void main(String[] args) {
		SpringApplication.run(SaSsoServerApplication.class, args);

		System.out.println();
		System.out.println("---------------------- Sa-Token SSO 统一认证中心启动成功 ----------------------");
		System.out.println("配置信息：" + SaSsoManager.getServerConfig());
		System.out.println("统一认证登录地址：http://sa-sso-server.com:9000/sso/auth");
		System.out.println("测试前需要根据官网文档修改 hosts 文件，测试账号密码：sa / 123456");
		System.out.println();
	}
}
```

启动项目，不出意外的情况下我们将看到如下输出：

<img class="s-w-sh" src="/big-file/doc/sso/sso-server-start.png" alt="SSO-Server 启动成功控制台输出">

访问统一授权地址（仅测试 SSO-Server 是否部署成功，暂时还不需要点击登录）：
- [http://localhost:9000/sso/auth](http://localhost:9000/sso/auth)

<img class="s-w-sh" src="/big-file/doc/sso/sso-server-init-login--v43.png" alt="SSO 统一授权地址 /sso/auth 初始页面">

可以看到这个页面目前非常简陋，这是因为我们以上的代码示例，主要目标是为了带大家从零搭建一个可用的SSO认证服务端，所以就对一些不太必要的步骤做了简化。

大家可以下载运行一下官方仓库里的示例`/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/`，里面有制作好的登录页面：

<img class="s-w-sh" src="/big-file/doc/sso/sso-server-init-login2.png" alt="SSO-Server 官方示例登录页面">

默认账号密码为：`sa / 123456`，先别着急点击登录，因为我们还没有搭建对应的 Client 端项目，
真实项目中我们一般不会直接从浏览器访问 `/sso/auth` 授权地址的，我们需要在 Client 端点击登录按钮重定向而来。


---

现在我们先来看看除了 `/sso/auth` 统一授权地址，这个 SSO-Server 认证中心还开放了哪些API：[SSO-Server 认证中心开放接口](/sso/sso-apidoc)。

---

--- METRICS ---
- Files Extracted: 120
- Estimated Token Budget: ~127020 tokens
- Canonical Reference: https://codewiki.google/github.com/dromara/Sa-Token
