From 895941b5c86433786888c9c7ebcecb61272d4d88 Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Fri, 11 Apr 2025 18:17:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20SSE=20=E4=B8=AD?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20Sa-Token=20=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mvn clean.bat | 1 + sa-token-demo/pom.xml | 1 + sa-token-demo/sa-token-demo-sse/pom.xml | 59 ++++++++ .../java/com/pj/SaTokenSseApplication.java | 23 +++ .../java/com/pj/current/GlobalException.java | 20 +++ .../java/com/pj/satoken/SaTokenConfigure.java | 52 +++++++ .../java/com/pj/test/LoginController.java | 43 ++++++ .../java/com/pj/test/SseAdminController.java | 29 ++++ .../main/java/com/pj/test/SseController.java | 23 +++ .../java/com/pj/util/SseEmitterHolder.java | 139 ++++++++++++++++++ .../src/main/resources/application.yml | 49 ++++++ sa-token-doc/start/download.md | 1 + 12 files changed, 440 insertions(+) create mode 100644 sa-token-demo/sa-token-demo-sse/pom.xml create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/SaTokenSseApplication.java create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/current/GlobalException.java create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/satoken/SaTokenConfigure.java create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/LoginController.java create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseAdminController.java create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/test/SseController.java create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java create mode 100644 sa-token-demo/sa-token-demo-sse/src/main/resources/application.yml 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认证中心