mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-10-07 23:24:24 +08:00
feat: 新增 SSE 中使用 Sa-Token 示例
This commit is contained in:
@@ -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>
|
||||
|
59
sa-token-demo/sa-token-demo-sse/pom.xml
Normal file
59
sa-token-demo/sa-token-demo-sse/pom.xml
Normal 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>
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user