feat: 新增 SSE 中使用 Sa-Token 示例

This commit is contained in:
click33
2025-04-11 18:17:35 +08:00
parent 308f81be31
commit 895941b5c8
12 changed files with 440 additions and 0 deletions

View File

@@ -38,6 +38,7 @@
<module>sa-token-demo-springboot-low-version</module>
<module>sa-token-demo-springboot-redis</module>
<module>sa-token-demo-springboot-redisson</module>
<module>sa-token-demo-sse</module>
<module>sa-token-demo-ssm</module>
<module>sa-token-demo-sso/sa-token-demo-sso-server</module>
<module>sa-token-demo-sso/sa-token-demo-sso1-client</module>

View File

@@ -0,0 +1,59 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-demo-sse</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<!-- 定义 Sa-Token 版本号 -->
<properties>
<sa-token.version>1.42.0</sa-token.version>
<java.run.main.class>com.pj.SaTokenSseApplication</java.run.main.class>
</properties>
<dependencies>
<!-- SpringBoot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档https://sa-token.cc/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
</dependency>
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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();
};
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, SseEmitter> 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<String> 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<String> tokenList = StpUtil.getTokenValueListByLoginId(uid);
for (String token : tokenList) {
closeByToken(token);
}
}
}

View File

@@ -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