diff --git a/backend/pom.xml b/backend/pom.xml index 3e8ff70..4c6eb6e 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -60,7 +60,7 @@ 3.5.3.1 - + org.springframework.boot @@ -68,7 +68,7 @@ 3.0.2 - + io.jsonwebtoken @@ -92,6 +92,22 @@ runtime + + + + org.springframework.boot + spring-boot-starter-websocket + 3.0.2 + + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.24 + + org.springframework.boot diff --git a/backend/src/main/java/com/kob/backend/config/SecurityConfig.java b/backend/src/main/java/com/kob/backend/config/SecurityConfig.java index c31d787..2d6f357 100644 --- a/backend/src/main/java/com/kob/backend/config/SecurityConfig.java +++ b/backend/src/main/java/com/kob/backend/config/SecurityConfig.java @@ -11,6 +11,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; @@ -25,7 +26,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; -// 配置密码加密方式 + // 配置密码加密方式 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -36,10 +37,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } -/* - .antMatchers("/user/account/token/", "/user/account/register/").permitAll() - 用于配置公开链接 -*/ + + /* + .antMatchers("/user/account/token/", "/user/account/register/").permitAll() + 用于配置公开链接 + */ @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() @@ -52,4 +54,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } + + // 用于配置 websocket:放行所有的 websocket/ 链接 + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/websocket/**"); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/kob/backend/config/WebSocketConfig.java b/backend/src/main/java/com/kob/backend/config/WebSocketConfig.java new file mode 100644 index 0000000..f4a09f9 --- /dev/null +++ b/backend/src/main/java/com/kob/backend/config/WebSocketConfig.java @@ -0,0 +1,15 @@ +package com.kob.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + + +@Configuration +public class WebSocketConfig { + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java b/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java index 605b038..670d63d 100644 --- a/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java +++ b/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java @@ -43,8 +43,12 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { token = token.substring(7); + + +// 核心验证逻辑 String userid; try { +// 将 token 解析,如果能成功解析出 userid 表示合法,否则表示不合法 Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { @@ -53,6 +57,7 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { User user = userMapper.selectById(Integer.parseInt(userid)); +// User user = userMapper.selectById(new JwtAuthenticationUtil().getUserId(token)); if (user == null) { throw new RuntimeException("用户名未登录"); } diff --git a/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java b/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java new file mode 100644 index 0000000..0e1566f --- /dev/null +++ b/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java @@ -0,0 +1,165 @@ +package com.kob.backend.consumer; + +// WebSocket用于前后端通信 + +import com.alibaba.fastjson2.JSONObject; +import com.kob.backend.consumer.utils.JwtAuthenticationUtil; +import com.kob.backend.mapper.UserMapper; +import com.kob.backend.pojo.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.websocket.*; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +@Component +@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾 +public class WebSocketServer { + // 后端向前端发送信息,首先需要创建一个 session + private Session session = null; + + // 用户信息:定义一个成员变量 + private User user; + + /* + 存储所有链接:对所有 websocket 可见的全局变量,存储为 static 静态变量 + 因为每个 websocket 实例都在一个独立的线程里,所以该公共变量应该是线程安全的 + 使用线程安全的 Hash 表 ConcurrentHashMap<>(), + 将 userId 映射到 webSocketServer + */ + private static final ConcurrentHashMap users = new ConcurrentHashMap<>(); + +// 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool + private static final CopyOnWriteArraySet matchPool = new CopyOnWriteArraySet<>(); + + + //在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 + private static UserMapper userMapper; + + // 注入方法 + @Autowired + public void setUserMapper(UserMapper userMapper) { +// 静态变量 userMapper 访问需要使用类名 WebSocketServer 访问 + WebSocketServer.userMapper = userMapper; + } + + + // @OnOpen 创建链接时自动触发这个函数 + @OnOpen + public void onOpen(Session session, @PathParam("token") String token) throws IOException { + // 建立链接时需要将 session 存下来 + this.session = session; +// 成功建立连接时,输出 connected! + System.out.println("backend connected!"); +// 将 userId 取出来,通过 userId 将 user 找出来 +// int userId = Integer.parseInt(token); + int userId = JwtAuthenticationUtil.getUserId(token); + this.user = userMapper.selectById(userId); + +// 如果 user 存在, 表示用户登录成功, 用户信息是存在的 + if (this.user != null) { + // 将 user 存到 users HashMap里 + users.put(userId, this); + + // (测试)后台输出看用户信息 +// System.out.println(user); + } +// 否则,断开连接(这里需要抛出异常) + else { + this.session.close(); + } + + + } + + @OnClose + public void onClose() { + // 关闭链接 + System.out.println("backend disconnected!"); +// 断开连接时,需要将 user 从 users 里面删掉 + if (this.user != null) { + users.remove(this.user.getId()); +// 将匹配池数组删掉 + matchPool.remove(this.user); + } + } + + // @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分 + @OnMessage + public void onMessage(String message, Session session) { + // 从 Client 接收消息 + System.out.println("receive message"); +// 解析从前端接收到的请求 + JSONObject data = JSONObject.parseObject(message); + String event = data.getString("event"); +// System.out.println(event); + +// 匹配状态函数调用 + if ("start-matching".equals(event)) { + startMatching(); + } else if ("stop-matching".equals(event)) { + stopMatching(); + } + } + + @OnError + public void onError(Session session, Throwable error) { + error.printStackTrace(); + } + + // 从后端向前端发送信息 + public void sendMessage(String message) { +// 异步通信过程,先加一个锁 + synchronized (this.session) { + try { +// 将 message 发送到前端 + this.session.getBasicRemote().sendText(message); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + // 开始匹配的逻辑部分 + private void startMatching() { + System.out.println("start matching"); + matchPool.add(this.user); + + while (matchPool.size()>=2){ +// 迭代器用于枚举前两个人进行匹配 + Iterator it = matchPool.iterator(); + User a = it.next(), b = it.next(); +// 取出两个人之后,从匹配池中将他们删除 + matchPool.remove(a); + matchPool.remove(b); + +// 将 a 配对成功的消息传回客户端 + JSONObject respA = new JSONObject(); + respA.put("event","start-matching"); + respA.put("opponent_username",b.getUsername()); + respA.put("opponent_photo",b.getPhoto()); +// 获取 a 的链接,并通过 sendMessage 将消息传给前端 + users.get(a.getId()).sendMessage(respA.toJSONString()); + +// 同理,将 b 的匹配成功信息传回给前端 + JSONObject respB = new JSONObject(); + respB.put("event","start-matching"); + respB.put("opponent_username",a.getUsername()); + respB.put("opponent_photo",a.getPhoto()); + users.get(b.getId()).sendMessage(respB.toJSONString()); + } + } + + // 取消匹配的逻辑部分 + private void stopMatching() { + System.out.println("stop matching"); + matchPool.remove(this.user); + } +} + + diff --git a/backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthenticationUtil.java b/backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthenticationUtil.java new file mode 100644 index 0000000..9e7065c --- /dev/null +++ b/backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthenticationUtil.java @@ -0,0 +1,21 @@ +package com.kob.backend.consumer.utils; + +import com.kob.backend.utils.JwtUtil; +import io.jsonwebtoken.Claims; + +//Jwt 验证配置工具类 +public class JwtAuthenticationUtil { + public static Integer getUserId(String token) { +// 核心验证逻辑 +// 默认 userid 赋值为 -1 ,表示不存在 + int userId = -1; + try { +// 将 token 解析,如果能成功解析出 userid 表示合法,否则表示不合法 + Claims claims = JwtUtil.parseJWT(token); + userId = Integer.parseInt(claims.getSubject()); + } catch (Exception e) { + throw new RuntimeException(e); + } + return userId; + } +} diff --git a/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java b/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java index 5b4f69d..6bcd64b 100644 --- a/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java +++ b/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java @@ -74,7 +74,7 @@ public class RegisterServiceImpl implements RegisterService { // 对密码进行加密 String encodedPassword = new BCryptPasswordEncoder().encode(password); // 默认头像 - String photo = "https://cdn.acwing.com/media/user/profile/photo/253652_lg_e3d8435b66.jpg"; + String photo = "https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202302251824054.png"; // id 是数据库自增,这里生成新用户只需要将 id 参数写为 null 即可 User user = new User(null, username, encodedPassword, photo); userMapper.insert(user); diff --git a/sql/record.sql b/sql/record.sql new file mode 100644 index 0000000..b630606 --- /dev/null +++ b/sql/record.sql @@ -0,0 +1,17 @@ +create table record +( + id int auto_increment + primary key, + a_id int null, + a_sx int null, + a_sy int null, + b_id int null, + b_sx int null, + b_sy int null, + a_steps varchar(1000) null, + b_steps varchar(1000) null, + map varchar(1000) null, + loser varchar(10) null, + createtime datetime null +); + diff --git a/web/src/components/MatchGround.vue b/web/src/components/MatchGround.vue new file mode 100644 index 0000000..7d45b4f --- /dev/null +++ b/web/src/components/MatchGround.vue @@ -0,0 +1,118 @@ +// 定义匹配页面 + + + + + diff --git a/web/src/components/NavBar.vue b/web/src/components/NavBar.vue index fc40a02..37ab42f 100644 --- a/web/src/components/NavBar.vue +++ b/web/src/components/NavBar.vue @@ -99,9 +99,9 @@ export default { let route_name = computed(() => route.name); const logout = () => { - console.log("退出前:" + store.state.user.token); + // console.log("退出前:" + store.state.user.token); store.dispatch("logout"); - console.log("退出后:" + store.state.user.token); + // console.log("退出后:" + store.state.user.token); }; return { diff --git a/web/src/main.js b/web/src/main.js index a92f228..2de0f06 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -1,6 +1,7 @@ import { createApp } from 'vue' import App from './App.vue' import router from './router' +// 全局挂载 store , 后面写的 vue 页面都可以通过 $store 来调用 import store from './store' createApp(App).use(store).use(router).mount('#app') diff --git a/web/src/store/index.js b/web/src/store/index.js index 4c3044f..02e4263 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -1,6 +1,7 @@ import { createStore } from "vuex"; import ModuleUser from "./user"; import ModuleBot from './bot'; +import ModulePk from './pk'; export default createStore({ state: {}, @@ -10,5 +11,6 @@ export default createStore({ modules: { user: ModuleUser, bot: ModuleBot, + pk: ModulePk, }, }); diff --git a/web/src/store/pk.js b/web/src/store/pk.js new file mode 100644 index 0000000..1d1c980 --- /dev/null +++ b/web/src/store/pk.js @@ -0,0 +1,29 @@ +export default { + state: { + // 当前状态:用于判断时匹配中还是已经匹配完成: matching 表示匹配中(匹配界面), playing 表示匹配完成(对战界面) + status: "matching", + // socket 信息 + socket: null, + // 对手信息 + opponent_username: "", + opponent_photo: "", + }, + getters: {}, + mutations: { + // 更新 socket 信息 + updateSocket(state, socket) { + state.socket = socket; + }, + // 更新对手信息 + updateOpponent(state, opponent) { + state.opponent_username = opponent.username; + state.opponent_photo = opponent.photo; + }, + // 更新匹配状态信息 + updateStatus(state, status) { + state.status = status; + }, + }, + actions: {}, + module: {}, +}; diff --git a/web/src/views/pk/PkIndexView.vue b/web/src/views/pk/PkIndexView.vue index 9dcb296..dfd54be 100644 --- a/web/src/views/pk/PkIndexView.vue +++ b/web/src/views/pk/PkIndexView.vue @@ -1,14 +1,84 @@