diff --git a/mvn clean.bat b/mvn clean.bat
index 95a67a72..8ff63ec5 100644
--- a/mvn clean.bat
+++ b/mvn clean.bat
@@ -27,6 +27,7 @@ cd sa-token-demo-springboot3-redis & call mvn clean & cd ..
cd sa-token-demo-springboot-low-version & call mvn clean & cd ..
cd sa-token-demo-springboot-redis & call mvn clean & cd ..
cd sa-token-demo-springboot-redisson & call mvn clean & cd ..
+cd sa-token-demo-sse & call mvn clean & cd ..
cd sa-token-demo-ssm & call mvn clean & cd ..
cd sa-token-demo-test & call mvn clean & cd ..
cd sa-token-demo-thymeleaf & call mvn clean & cd ..
diff --git a/sa-token-demo/pom.xml b/sa-token-demo/pom.xml
index c2a6664a..bb34b4f8 100644
--- a/sa-token-demo/pom.xml
+++ b/sa-token-demo/pom.xml
@@ -38,6 +38,7 @@
sa-token-demo-springboot-low-version
sa-token-demo-springboot-redis
sa-token-demo-springboot-redisson
+ sa-token-demo-sse
sa-token-demo-ssm
sa-token-demo-sso/sa-token-demo-sso-server
sa-token-demo-sso/sa-token-demo-sso1-client
diff --git a/sa-token-demo/sa-token-demo-sse/pom.xml b/sa-token-demo/sa-token-demo-sse/pom.xml
new file mode 100644
index 00000000..8ab0162e
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/pom.xml
@@ -0,0 +1,59 @@
+
+ 4.0.0
+ cn.dev33
+ sa-token-demo-sse
+ 0.0.1-SNAPSHOT
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.18
+
+
+
+
+
+ 1.42.0
+ com.pj.SaTokenSseApplication
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ cn.dev33
+ sa-token-spring-boot-starter
+ ${sa-token.version}
+
+
+
+ cn.hutool
+ hutool-all
+ 5.8.36
+
+
+
+
+ cn.dev33
+ sa-token-redis-template
+ ${sa-token.version}
+
+
+
+
+ org.apache.commons
+ commons-pool2
+
+
+
+
+
\ No newline at end of file
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java
new file mode 100644
index 00000000..753ff384
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java
@@ -0,0 +1,23 @@
+package com.pj;
+
+import cn.dev33.satoken.SaManager;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+/**
+ * Sa-Token 测试
+ * @author click33
+ *
+ */
+@SpringBootApplication
+public class SaTokenSseApplication {
+
+ // SSE 连接测试在线工具:https://toolshu.com/sse
+
+ public static void main(String[] args) {
+ SpringApplication.run(SaTokenSseApplication.class, args);
+ System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig());
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java
new file mode 100644
index 00000000..750fc780
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java
@@ -0,0 +1,20 @@
+package com.pj.current;
+
+import cn.dev33.satoken.util.SaResult;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 全局异常处理
+ */
+@RestControllerAdvice
+public class GlobalException {
+
+ // 全局异常拦截(拦截项目中的所有异常)
+ @ExceptionHandler
+ public SaResult handlerException(Exception e) {
+ e.printStackTrace();
+ return SaResult.error(e.getMessage());
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java
new file mode 100644
index 00000000..bc714c9e
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java
@@ -0,0 +1,52 @@
+package com.pj.satoken;
+
+import cn.dev33.satoken.fun.strategy.SaCorsHandleFunction;
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.dev33.satoken.router.SaHttpMethod;
+import cn.dev33.satoken.router.SaRouter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+
+/**
+ * [Sa-Token 权限认证] 配置类
+ * @author click33
+ *
+ */
+@Configuration
+public class SaTokenConfigure implements WebMvcConfigurer {
+
+ /**
+ * 注册 Sa-Token 拦截器打开注解鉴权功能
+ */
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
+ }
+
+ /**
+ * CORS 跨域处理
+ */
+ @Bean
+ public SaCorsHandleFunction corsHandle() {
+ return (req, res, sto) -> {
+ res.
+ // 允许指定域访问跨域资源
+ setHeader("Access-Control-Allow-Origin", "*")
+ // 允许所有请求方式
+ .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
+ // 有效时间
+ .setHeader("Access-Control-Max-Age", "3600")
+ // 允许的header参数
+ .setHeader("Access-Control-Allow-Headers", "*");
+
+ // 如果是预检请求,则立即返回到前端
+ SaRouter.match(SaHttpMethod.OPTIONS)
+ .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
+ .back();
+ };
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java
new file mode 100644
index 00000000..ebbc225a
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java
@@ -0,0 +1,43 @@
+package com.pj.test;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+import com.pj.util.SseEmitterHolder;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 登录测试
+ * @author click33
+ *
+ */
+@RestController
+@RequestMapping("/acc/")
+public class LoginController {
+
+ // 测试登录 ---- http://localhost:8081/acc/doLogin?uid=10001
+ @RequestMapping("doLogin")
+ public SaResult doLogin(@RequestParam(defaultValue = "10001") long uid) {
+ StpUtil.login(uid);
+ return SaResult.data(StpUtil.getTokenInfo());
+ }
+
+ // 查询登录状态 ---- http://localhost:8081/acc/isLogin
+ @RequestMapping("isLogin")
+ public SaResult isLogin() {
+ return SaResult.ok("是否登录:" + StpUtil.isLogin());
+ }
+
+ // 测试注销 ---- http://localhost:8081/acc/logout
+ @RequestMapping("logout")
+ public SaResult logout() {
+ if(StpUtil.isLogin()) {
+ long uid = StpUtil.getLoginIdAsLong();
+ SseEmitterHolder.closeByUid(uid);
+ StpUtil.logout();
+ }
+ return SaResult.ok();
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java
new file mode 100644
index 00000000..f352fd73
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java
@@ -0,0 +1,29 @@
+package com.pj.test;
+
+import cn.dev33.satoken.util.SaResult;
+import com.pj.util.SseEmitterHolder;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * SSE 推送
+ */
+@RestController
+public class SseAdminController {
+
+ // 推送消息 --- http://localhost:8081/sse/send?uid=10001&message=hello123
+ @RequestMapping(value = "/sse/send")
+ public SaResult sendMessage(long uid, String message) {
+ SseEmitterHolder.sendMessageByUid(uid, message);
+ return SaResult.ok();
+ }
+
+ // 断开 --- http://localhost:8081/sse/close?uid=10001
+ @RequestMapping(value = "/sse/close")
+ public SaResult close(long uid){
+ SseEmitterHolder.closeByUid(uid);
+ return SaResult.ok();
+ }
+
+}
+
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java
new file mode 100644
index 00000000..be52d225
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java
@@ -0,0 +1,23 @@
+package com.pj.test;
+
+import com.pj.util.SseEmitterHolder;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+/**
+ * SSE 连接
+ */
+@RestController
+public class SseController {
+
+
+ // 创建连接 --- http://localhost:8081/sse?satoken=d8a8e1c7-62a4-4656-8b54-cc14e6348ceb
+ @RequestMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public SseEmitter createSse(String satoken) {
+ return SseEmitterHolder.createSse(satoken);
+ }
+
+}
+
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java
new file mode 100644
index 00000000..291d30e3
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java
@@ -0,0 +1,139 @@
+package com.pj.util;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaFoxUtil;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * SSE 连接管理器
+ *
+ * @author click33
+ * @since 2025/4/11
+ */
+public class SseEmitterHolder {
+
+ public static final Map sseEmitterMap = new ConcurrentHashMap<>();
+
+ /**
+ * 创建客户端
+ */
+ public static SseEmitter createSse(String satoken) {
+
+ Object loginId = StpUtil.getLoginIdByToken(satoken);
+ if(loginId == null) {
+ throw new NotLoginException("无效 token", StpUtil.TYPE, NotLoginException.INVALID_TOKEN);
+ }
+ long uid = SaFoxUtil.getValueByType(loginId, Long.class);
+
+ // 默认 30 秒超时,设置为 0L 则永不超时
+ SseEmitter sseEmitter = new SseEmitter(600 * 1000L);
+ sseEmitterMap.put(satoken, sseEmitter);
+ System.out.println("连接成功:satoken=" + satoken + ",uid=" + uid);
+
+ // 完成后回调
+ sseEmitter.onCompletion(() -> {
+ System.out.println("结束连接:satoken=" + satoken + ",uid=" + uid);
+ sseEmitterMap.remove(satoken);
+ });
+
+ //超时回调
+ sseEmitter.onTimeout(() -> {
+ System.out.println("连接超时:satoken=" + satoken + ",uid=" + uid);
+ });
+
+ //异常回调
+ sseEmitter.onError( e -> {
+// try {
+ System.out.println("连接异常:satoken=" + satoken + ",uid=" + uid);
+ System.err.println(e.getMessage());
+// sseEmitter.send(SseEmitter.event()
+// .id(String.valueOf(uid))
+// .name("发生异常!")
+// .data("发生异常请重试!")
+// .reconnectTime(3000));
+// sseEmitterMap.put(uid, sseEmitter);
+// } catch (IOException ee) {
+// ee.printStackTrace();
+// }
+ });
+ try {
+ sseEmitter.send(SseEmitter.event().reconnectTime(5000));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return sseEmitter;
+ }
+
+ /**
+ * 给指定 token 客户端发送消息
+ *
+ */
+ public static void sendMessageByToken(String satoken, String message) {
+ SseEmitter sseEmitter = sseEmitterMap.get(satoken);
+ if (sseEmitter == null) {
+ System.out.println("该 token 暂未建立连接:" + satoken);
+ return;
+ }
+ try {
+ sseEmitter.send(SseEmitter.event().reconnectTime(60 * 1000L).data(message));
+ System.out.println("消息推送成功,token=" + satoken + ", message=" + message);
+ }catch (Exception e) {
+ e.printStackTrace();
+// sseEmitterMap.remove(uid);
+// log.info("用户{},消息id:{},推送异常:{}", uid,messageId, e.getMessage());
+// sseEmitter.complete();
+ }
+ }
+
+ /**
+ * 给指定 用户 所有客户端发送消息
+ *
+ */
+ public static void sendMessageByUid(long uid, String message) {
+ List tokenList = StpUtil.getTokenValueListByLoginId(uid);
+ for (String token : tokenList) {
+ sendMessageByToken(token, message);
+ }
+ }
+
+ /**
+ * 指定 token 断开连接
+ *
+ */
+ public static void closeByToken(String satoken) {
+ SseEmitter sseEmitter = sseEmitterMap.get(satoken);
+ if (sseEmitter == null) {
+ System.out.println("该 token 暂未建立连接:" + satoken);
+ return;
+ }
+ try {
+ sendMessageByToken(satoken, "连接已断开!");
+ sseEmitter.complete();
+ System.out.println("连接已断开,token=" + satoken);
+ }catch (Exception e) {
+ e.printStackTrace();
+ // sseEmitterMap.remove(uid);
+ // log.info("用户{},消息id:{},推送异常:{}", uid,messageId, e.getMessage());
+ // sseEmitter.complete();
+ }
+ }
+
+ /**
+ * 指定 uid 断开连接
+ *
+ */
+ public static void closeByUid(long uid) {
+ List tokenList = StpUtil.getTokenValueListByLoginId(uid);
+ for (String token : tokenList) {
+ closeByToken(token);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml
new file mode 100644
index 00000000..ac474574
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml
@@ -0,0 +1,49 @@
+# 端口
+server:
+ port: 8081
+
+############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
+sa-token:
+ # token 名称 (同时也是 cookie 名称)
+ token-name: satoken
+ # token 有效期(单位:秒) 默认30天,-1 代表永久有效
+ timeout: 2592000
+ # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
+ active-timeout: -1
+ # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
+ is-concurrent: true
+ # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
+ is-share: false
+ # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
+ token-style: uuid
+ # 是否输出操作日志
+ is-log: true
+
+spring:
+ # redis配置
+ redis:
+ # Redis数据库索引(默认为0)
+ database: 0
+ # Redis服务器地址
+ host: 127.0.0.1
+ # Redis服务器连接端口
+ port: 6379
+ # Redis服务器连接密码(默认为空)
+ password:
+ # 连接超时时间
+ timeout: 10s
+ lettuce:
+ pool:
+ # 连接池最大连接数
+ max-active: 200
+ # 连接池最大阻塞等待时间(使用负值表示没有限制)
+ max-wait: -1ms
+ # 连接池中的最大空闲连接
+ max-idle: 10
+ # 连接池中的最小空闲连接
+ min-idle: 0
+
+
+
+
+
\ No newline at end of file
diff --git a/sa-token-doc/start/download.md b/sa-token-doc/start/download.md
index fad64fdd..5ddad3bf 100644
--- a/sa-token-doc/start/download.md
+++ b/sa-token-doc/start/download.md
@@ -250,6 +250,7 @@ Maven依赖一直无法加载成功?[参考解决方案](https://sa-token.cc/d
├── sa-token-demo-springboot-low-version // [示例] Sa-Token 整合 SpringBoot2 低版本
├── sa-token-demo-springboot-redis // [示例] Sa-Token 整合 SpringBoot 整合 Redis
├── sa-token-demo-springboot-redisson // [示例] Sa-Token 整合 SpringBoot 整合 redisson
+ ├── sa-token-demo-sse // [示例] 在 SSE 中使用 Sa-Token
├── sa-token-demo-ssm // [示例] 在 SSM 中使用 Sa-Token
├── sa-token-demo-sso // [示例] Sa-Token 集成 SSO 单点登录
├── sa-token-demo-sso-server // [示例] Sa-Token 集成 SSO单点登录-Server认证中心