diff --git a/sa-token-demo/sa-token-demo-websocket-spring/.gitignore b/sa-token-demo/sa-token-demo-websocket-spring/.gitignore new file mode 100644 index 00000000..99a6e767 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/.gitignore @@ -0,0 +1,12 @@ +target/ + +node_modules/ +bin/ +.settings/ +unpackage/ +.classpath +.project + +.idea/ + +.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket-spring/pom.xml b/sa-token-demo/sa-token-demo-websocket-spring/pom.xml new file mode 100644 index 00000000..201500ae --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-websocket-spring + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + + + 1.29.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token-version} + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java new file mode 100644 index 00000000..431fab28 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/SaTokenWebSocketSpringApplication.java @@ -0,0 +1,32 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +/** + * Sa-Token 整合 WebSocket 鉴权示例 + * @author kong + * + */ +@SpringBootApplication +public class SaTokenWebSocketSpringApplication { + + /* + * 1、访问登录接口,拿到会话Token: + * http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + * + * 2、找一个WebSocket在线测试页面进行连接, + * 例如: + * https://www.bejson.com/httputil/websocket/ + * 然后连接地址: + * ws://localhost:8081/ws-connect?satoken=2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5 + */ + + public static void main(String[] args) { + SpringApplication.run(SaTokenWebSocketSpringApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 00000000..11abbe3c --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author kong + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java new file mode 100644 index 00000000..990cc39d --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/MyWebSocketHandler.java @@ -0,0 +1,83 @@ +package com.pj.ws; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * 处理 WebSocket 连接 + * + * @author kong + * @date: 2022-2-11 + */ +public class MyWebSocketHandler extends TextWebSocketHandler { + + /** + * 固定前缀 + */ + private static final String USER_ID = "user_id_"; + + /** + * 存放Session集合,方便推送消息 + */ + private static ConcurrentHashMap webSocketSessionMaps = new ConcurrentHashMap<>(); + + // 监听:连接开启 + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + + // put到集合,方便后续操作 + String userId = session.getAttributes().get("userId").toString(); + webSocketSessionMaps.put(USER_ID + userId, session); + + + // 给个提示 + String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId; + System.out.println(tips); + sendMessage(session, tips); + } + + // 监听:连接关闭 + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + // 从集合移除 + String userId = session.getAttributes().get("userId").toString(); + webSocketSessionMaps.remove(USER_ID + userId); + + // 给个提示 + String tips = "Web-Socket 连接关闭,sid=" + session.getId() + ",userId=" + userId; + System.out.println(tips); + } + + // 收到消息 + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { + System.out.println("sid为:" + session.getId() + ",发来:" + message); + } + + // ----------- + + // 向指定客户端推送消息 + public static void sendMessage(WebSocketSession session, String message) { + try { + System.out.println("向sid为:" + session.getId() + ",发送:" + message); + session.sendMessage(new TextMessage(message)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 向指定用户推送消息 + public static void sendMessage(long userId, String message) { + WebSocketSession session = webSocketSessionMaps.get(USER_ID + userId); + if(session != null) { + sendMessage(session, message); + } + } + +} + diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java new file mode 100644 index 00000000..186b52be --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketConfig.java @@ -0,0 +1,30 @@ +package com.pj.ws; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +/** + * WebSocket 相关配置 + * + * @author kong + * @date: 2022-2-11 + */ +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + // 注册 WebSocket 处理器 + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { + webSocketHandlerRegistry + // WebSocket 连接处理器 + .addHandler(new MyWebSocketHandler(), "/ws-connect") + // WebSocket 拦截器 + .addInterceptors(new WebSocketInterceptor()) + // 允许跨域 + .setAllowedOrigins("*"); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java new file mode 100644 index 00000000..3643c1a5 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/java/com/pj/ws/WebSocketInterceptor.java @@ -0,0 +1,45 @@ +package com.pj.ws; + +import java.util.Map; + +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import cn.dev33.satoken.stp.StpUtil; + +/** + * WebSocket 握手的前置拦截器 + * + * @author kong + * @date: 2022-2-11 + */ +public class WebSocketInterceptor implements HandshakeInterceptor { + + // 握手之前触发 (return true 才会握手成功 ) + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, + Map attr) { + + System.out.println("---- 握手之前触发 " + StpUtil.getTokenValue()); + + // 未登录情况下拒绝握手 + if(StpUtil.isLogin() == false) { + System.out.println("---- 未授权客户端,连接失败"); + return false; + } + + // 标记 userId,握手成功 + attr.put("userId", StpUtil.getLoginIdAsLong()); + return true; + } + + // 握手之后触发 + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Exception exception) { + System.out.println("---- 握手之后触发 "); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml new file mode 100644 index 00000000..cb63067a --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket-spring/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +# sa-token配置 +sa-token: + # token名称 (同时也是cookie名称) + token-name: satoken + # token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 + activity-timeout: -1 + # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) + is-share: true + # token风格 + token-style: uuid + # 是否输出操作日志 + is-log: false + +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-demo/sa-token-demo-websocket/.gitignore b/sa-token-demo/sa-token-demo-websocket/.gitignore new file mode 100644 index 00000000..99a6e767 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/.gitignore @@ -0,0 +1,12 @@ +target/ + +node_modules/ +bin/ +.settings/ +unpackage/ +.classpath +.project + +.idea/ + +.factorypath \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket/pom.xml b/sa-token-demo/sa-token-demo-websocket/pom.xml new file mode 100644 index 00000000..0b52bac6 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + cn.dev33 + sa-token-demo-websocket + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + + + 1.29.0 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token-version} + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java new file mode 100644 index 00000000..6450ede8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/SaTokenWebSocketApplication.java @@ -0,0 +1,32 @@ +package com.pj; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import cn.dev33.satoken.SaManager; + +/** + * Sa-Token 整合 WebSocket 鉴权示例 + * @author kong + * + */ +@SpringBootApplication +public class SaTokenWebSocketApplication { + + /* + * 1、访问登录接口,拿到会话Token: + * http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + * + * 2、找一个WebSocket在线测试页面进行连接, + * 例如: + * https://www.bejson.com/httputil/websocket/ + * 然后连接地址: + * ws://localhost:8081/ws-connect/2e6db38f-1e78-40bc-aa8f-e8f1f77fbef5 + */ + + public static void main(String[] args) { + SpringApplication.run(SaTokenWebSocketApplication.class, args); + System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig()); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java new file mode 100644 index 00000000..11abbe3c --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/test/LoginController.java @@ -0,0 +1,48 @@ +package com.pj.test; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaResult; + +/** + * 登录测试 + * @author kong + * + */ +@RestController +@RequestMapping("/acc/") +public class LoginController { + + // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 + @RequestMapping("doLogin") + public SaResult doLogin(String name, String pwd) { + // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 + if("zhang".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功").set("token", StpUtil.getTokenValue()); + } + return SaResult.error("登录失败"); + } + + // 查询登录状态 ---- http://localhost:8081/acc/isLogin + @RequestMapping("isLogin") + public SaResult isLogin() { + return SaResult.ok("是否登录:" + StpUtil.isLogin()); + } + + // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo + @RequestMapping("tokenInfo") + public SaResult tokenInfo() { + return SaResult.data(StpUtil.getTokenInfo()); + } + + // 测试注销 ---- http://localhost:8081/acc/logout + @RequestMapping("logout") + public SaResult logout() { + StpUtil.logout(); + return SaResult.ok(); + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java new file mode 100644 index 00000000..356393ef --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConfig.java @@ -0,0 +1,18 @@ +package com.pj.ws; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * 开启WebSocket支持 + */ +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } + +} \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java new file mode 100644 index 00000000..c99e701f --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java @@ -0,0 +1,102 @@ +package com.pj.ws; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +import org.springframework.stereotype.Component; + +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * WebSocket 连接测试 + */ +@Component +@ServerEndpoint("/ws-connect/{satoken}") +public class WebSocketConnect { + + /** + * 固定前缀 + */ + private static final String USER_ID = "user_id_"; + + /** + * 存放Session集合,方便推送消息 (javax.websocket.Session) + */ + private static ConcurrentHashMap sessionMap = new ConcurrentHashMap<>(); + + // 监听:连接成功 + @OnOpen + public void onOpen(Session session, @PathParam("satoken") String satoken) throws IOException { + + // 根据 token 获取对应的 userId + Object loginId = StpUtil.getLoginIdByToken(satoken); + if(loginId == null) { + session.close(); + throw new SaTokenException("连接失败,无效Token:" + satoken); + } + + // put到集合,方便后续操作 + long userId = SaFoxUtil.getValueByType(loginId, long.class); + sessionMap.put(USER_ID + userId, session); + + // 给个提示 + String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId; + System.out.println(tips); + sendMessage(session, tips); + } + + // 监听: 连接关闭 + @OnClose + public void onClose(Session session) { + System.out.println("连接关闭,sid=" + session.getId()); + for (String key : sessionMap.keySet()) { + if(sessionMap.get(key).getId().equals(session.getId())) { + sessionMap.remove(key); + } + } + } + + // 监听:收到客户端发送的消息 + @OnMessage + public void onMessage(Session session, String message) { + System.out.println("sid为:" + session.getId() + ",发来:" + message); + } + + // 监听:发生异常 + @OnError + public void onError(Session session, Throwable error) { + System.out.println("sid为:" + session.getId() + ",发生错误"); + error.printStackTrace(); + } + + // --------- + + // 向指定客户端推送消息 + public static void sendMessage(Session session, String message) { + try { + System.out.println("向sid为:" + session.getId() + ",发送:" + message); + session.getBasicRemote().sendText(message); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 向指定用户推送消息 + public static void sendMessage(long userId, String message) { + Session session = sessionMap.get(USER_ID + userId); + if(session != null) { + sendMessage(session, message); + } + } + +} diff --git a/sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml new file mode 100644 index 00000000..cb63067a --- /dev/null +++ b/sa-token-demo/sa-token-demo-websocket/src/main/resources/application.yml @@ -0,0 +1,49 @@ +# 端口 +server: + port: 8081 + +# sa-token配置 +sa-token: + # token名称 (同时也是cookie名称) + token-name: satoken + # token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 + activity-timeout: -1 + # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) + is-share: true + # token风格 + token-style: uuid + # 是否输出操作日志 + is-log: false + +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/doc/start/download.md b/sa-token-doc/doc/start/download.md index a6921673..c11de14b 100644 --- a/sa-token-doc/doc/start/download.md +++ b/sa-token-doc/doc/start/download.md @@ -120,6 +120,8 @@ implementation 'cn.dev33:sa-token-core:${sa.top.version}' ├── sa-token-demo-sso-client-h5 // [示例] Sa-Token 集成 SSO单点登录-client应用端 (前后端分离) ├── sa-token-demo-oauth2-server // [示例] Sa-Token 集成 OAuth2.0 (服务端) ├── sa-token-demo-oauth2-client // [示例] Sa-Token 集成 OAuth2.0 (客户端) + ├── sa-token-demo-websocket // [示例] Sa-Token 集成 Web-Socket 鉴权示例 + ├── sa-token-demo-websocket-spring // [示例] Sa-Token 集成 Web-Socket(Spring封装版) 鉴权示例 ├── sa-token-test // [测试] Sa-Token 单元测试合集 ├── sa-token-core-test // [测试] Sa-Token Core核心包单元测试 ├── sa-token-springboot-test // [测试] Sa-Token SpringBoot 整合测试