From f0566a59699a919cc217816b99fdd6b5b814e638 Mon Sep 17 00:00:00 2001 From: chendaofei <857448963@qq.com> Date: Tue, 24 Jun 2025 08:50:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- event_smart/pom.xml | 25 +++ .../controller/EventGateController.java | 44 ++++ .../LargeScreenCountController.java | 12 + .../transientes/filter/JwtRequestFilter.java | 68 ++++++ .../transientes/security/AuthController.java | 63 ++++++ .../security/MyUserDetailsService.java | 36 +++ .../transientes/security/SecurityConfig.java | 56 +++++ .../event/transientes/utils/JwtUtil.java | 66 ++++++ .../websocket/WebSocketServer.java | 211 +++++++----------- 9 files changed, 452 insertions(+), 129 deletions(-) create mode 100644 event_smart/src/main/java/com/njcn/gather/event/transientes/controller/EventGateController.java create mode 100644 event_smart/src/main/java/com/njcn/gather/event/transientes/filter/JwtRequestFilter.java create mode 100644 event_smart/src/main/java/com/njcn/gather/event/transientes/security/AuthController.java create mode 100644 event_smart/src/main/java/com/njcn/gather/event/transientes/security/MyUserDetailsService.java create mode 100644 event_smart/src/main/java/com/njcn/gather/event/transientes/security/SecurityConfig.java create mode 100644 event_smart/src/main/java/com/njcn/gather/event/transientes/utils/JwtUtil.java diff --git a/event_smart/pom.xml b/event_smart/pom.xml index 20e192b6..b62b5466 100644 --- a/event_smart/pom.xml +++ b/event_smart/pom.xml @@ -64,6 +64,31 @@ 21.6.0.0 + + + org.springframework.boot + spring-boot-starter-security + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/controller/EventGateController.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/controller/EventGateController.java new file mode 100644 index 00000000..45df1e8b --- /dev/null +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/controller/EventGateController.java @@ -0,0 +1,44 @@ +package com.njcn.gather.event.transientes.controller; + +import cn.hutool.json.JSONObject; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.gather.event.transientes.websocket.WebSocketServer; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * @Author: cdf + * @CreateTime: 2025-06-23 + * @Description: + */ +@Api(tags = "暂降接收") +@RequestMapping("accept") +@RestController +@RequiredArgsConstructor +public class EventGateController extends BaseController { + + private final WebSocketServer webSocketServer; + + + @OperateInfo + @GetMapping("/eventMsg") + @ApiOperation("接收远程推送的暂态事件") + @ApiImplicitParam(name = "eventMsg", value = "暂态事件json字符", required = true) + public HttpResult eventMsg(@RequestParam("msg") String msg) { + String methodDescribe = getMethodDescribe("eventMsg"); + System.out.println(msg); + + JSONObject jsonObject = new JSONObject(msg); + webSocketServer.sendMessageToUser("bbb",jsonObject.toString()); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, jsonObject, methodDescribe); + } + + +} diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/controller/LargeScreenCountController.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/controller/LargeScreenCountController.java index b68d53ed..c53b0ebe 100644 --- a/event_smart/src/main/java/com/njcn/gather/event/transientes/controller/LargeScreenCountController.java +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/controller/LargeScreenCountController.java @@ -78,4 +78,16 @@ public class LargeScreenCountController extends BaseController { } + + + + + + + + + + + + } diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/filter/JwtRequestFilter.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/filter/JwtRequestFilter.java new file mode 100644 index 00000000..5e823eb6 --- /dev/null +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/filter/JwtRequestFilter.java @@ -0,0 +1,68 @@ +package com.njcn.gather.event.transientes.filter; + +import com.njcn.gather.event.transientes.utils.JwtUtil; +import io.jsonwebtoken.ExpiredJwtException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +@Slf4j +public class JwtRequestFilter extends OncePerRequestFilter { + + private final UserDetailsService userDetailsService; + private final JwtUtil jwtUtil; + + public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) { + this.userDetailsService = userDetailsService; + this.jwtUtil = jwtUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + final String authorizationHeader = request.getHeader("Authorization"); + + String username = null; + String jwt = null; + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + try { + username = jwtUtil.extractUsername(jwt); + } catch (Exception e) { + // 可以在这里处理令牌过期的情况 + log.info("JWT Token 校验异常,可能过期了"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Token expired"); + return; // 终止请求处理 + } + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + if (jwtUtil.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken + .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/security/AuthController.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/security/AuthController.java new file mode 100644 index 00000000..6b93ee72 --- /dev/null +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/security/AuthController.java @@ -0,0 +1,63 @@ +package com.njcn.gather.event.transientes.security; + +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.event.transientes.utils.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AuthController { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private JwtUtil jwtUtil; + + @PostMapping("/cn_authenticate") + public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception { + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()) + ); + } catch (BadCredentialsException e) { + e.printStackTrace(); + throw new BusinessException("Incorrect username or password"); + } + final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername()); + final String jwt = jwtUtil.generateToken(userDetails); + return jwt; + } +} + +// 认证请求类 +class AuthRequest { + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/security/MyUserDetailsService.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/security/MyUserDetailsService.java new file mode 100644 index 00000000..3b5a9172 --- /dev/null +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/security/MyUserDetailsService.java @@ -0,0 +1,36 @@ +package com.njcn.gather.event.transientes.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +@Service +@RequiredArgsConstructor +public class MyUserDetailsService implements UserDetailsService { + + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 这里应该从数据库中获取用户信息,本示例使用硬编码用户 + if ("cdf".equals(username)) { + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String encodedPassword = passwordEncoder.encode("@#001njcnpqs"); + return new User("cdf", encodedPassword, + new ArrayList<>()); + }else if("screen".equals(username)){ + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String encodedPassword = passwordEncoder.encode("@#001njcnpqs"); + return new User("screen", encodedPassword, + new ArrayList<>()); + } else { + throw new UsernameNotFoundException("User not found with username: " + username); + } + } +} \ No newline at end of file diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/security/SecurityConfig.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/security/SecurityConfig.java new file mode 100644 index 00000000..7e85b470 --- /dev/null +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/security/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.njcn.gather.event.transientes.security; + +import com.njcn.gather.event.transientes.filter.JwtRequestFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +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; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private JwtRequestFilter jwtRequestFilter; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .authorizeRequests() + .antMatchers("/cn_authenticate").permitAll() // 允许访问认证接口 + .anyRequest().authenticated() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 使用无状态会话 + + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/utils/JwtUtil.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/utils/JwtUtil.java new file mode 100644 index 00000000..47941473 --- /dev/null +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/utils/JwtUtil.java @@ -0,0 +1,66 @@ +package com.njcn.gather.event.transientes.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); + private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 100000L; // 10小时 + + // 生成JWT令牌 + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return createToken(claims, userDetails.getUsername()); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .signWith(SECRET_KEY, SignatureAlgorithm.HS256) + .compact(); + } + + // 验证令牌 + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + // 提取用户名 + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + // 提取过期时间 + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } +} \ No newline at end of file diff --git a/event_smart/src/main/java/com/njcn/gather/event/transientes/websocket/WebSocketServer.java b/event_smart/src/main/java/com/njcn/gather/event/transientes/websocket/WebSocketServer.java index d81f3c8c..969f3528 100644 --- a/event_smart/src/main/java/com/njcn/gather/event/transientes/websocket/WebSocketServer.java +++ b/event_smart/src/main/java/com/njcn/gather/event/transientes/websocket/WebSocketServer.java @@ -1,6 +1,7 @@ package com.njcn.gather.event.transientes.websocket; +import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -12,6 +13,9 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; /** * Description: @@ -22,156 +26,105 @@ import java.util.concurrent.ConcurrentHashMap; */ @Slf4j @Component -@ServerEndpoint(value ="/api/pushMessage/{userIdAndLineIdAndDevId}") +@ServerEndpoint(value = "/ws/{userId}") public class WebSocketServer { - /** - * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 - */ - private static int onlineCount = 0; - /** - * concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。 - */ - private static ConcurrentHashMap webSocketMap = new ConcurrentHashMap<>(); - /** - * 与某个客户端的连接会话,需要通过它来给客户端发送数据 - */ - private Session session; - /** - * 接收userId - */ - private String userIdAndLineIdAndDevId = ""; + private static final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + private final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(1); - /** - * 连接建立成 - * 功调用的方法 - */ @OnOpen - public void onOpen(Session session, @PathParam("userIdAndLineIdAndDevId") String userIdAndLineIdAndDevId) { - this.session = session; - this.userIdAndLineIdAndDevId = userIdAndLineIdAndDevId; - if (webSocketMap.containsKey(userIdAndLineIdAndDevId)) { - webSocketMap.remove(userIdAndLineIdAndDevId); - //加入set中 - webSocketMap.put(userIdAndLineIdAndDevId, this); + public void onOpen(Session session, @PathParam("userId") String userId) { + if (StrUtil.isNotBlank(userId)) { + sessions.put(userId, session); + sendMessage(session, "连接成功"); + System.out.println("用户 " + userId + " 已连接"); + + // 启动心跳检测 + startHeartbeat(session, userId); } else { - //加入set中 - webSocketMap.put(userIdAndLineIdAndDevId, this); - //在线数加1 - addOnlineCount(); + try { + session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "用户ID不能为空")); + } catch (IOException e) { + e.printStackTrace(); + } } - sendMessage("连接成功"); } - /** - * 连接关闭 - * 调用的方法 - */ - @OnClose - public void onClose() { - if (webSocketMap.containsKey(userIdAndLineIdAndDevId)) { - webSocketMap.remove(userIdAndLineIdAndDevId); - //从set中删除 - subOnlineCount(); - } - log.info("监测点退出:" + userIdAndLineIdAndDevId + ",当前在线监测点数为:" + getOnlineCount()); - } - - /** - * 收到客户端消 - * 息后调用的方法 - * - * @param message 客户端发送过来的消息 - **/ @OnMessage - public void onMessage(String message, Session session) { - //会每30s发送请求1次 - log.info("监测点消息:" + userIdAndLineIdAndDevId + ",报文:" + message); - log.info("监测点连接:" + userIdAndLineIdAndDevId + ",当前在线监测点数为:" + getOnlineCount()); - + public void onMessage(String message, Session session, @PathParam("userId") String userId) { + if ("ping".equalsIgnoreCase(message)) { + // 处理心跳请求 + sendMessage(session, "pong"); + } else { + // 处理业务消息 + System.out.println("收到用户 " + userId + " 的消息: " + message); + // TODO: 处理业务逻辑 + } } + @OnClose + public void onClose(Session session, CloseReason closeReason, @PathParam("userId") String userId) { + sessions.remove(userId); + System.out.println("用户 " + userId + " 已断开连接,状态码: " + closeReason.getReasonPhrase()); + } - /** - * @param session - * @param error - */ @OnError - public void onError(Session session, Throwable error) { - - log.error("监测点错误:" + this.userIdAndLineIdAndDevId + ",原因:" + error.getMessage()); - error.printStackTrace(); - } - - /** - * 实现服务 - * 器主动推送 - */ - public void sendMessage(String message) { + public void onError(Session session, Throwable throwable, @PathParam("userId") String userId) { + System.out.println("用户 " + userId + " 发生错误: " + throwable.getMessage()); try { - this.session.getBasicRemote().sendText(message); + session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "发生错误")); } catch (IOException e) { e.printStackTrace(); } } - /** - * 发送自定 - * 义消息 - **/ - public static void sendInfo(String message, String lineId) { - log.info("发送消息到:" + lineId + ",报文:" + message); - if (StringUtils.isNotBlank(lineId)) { - Map stringStringMap = WebSocketServer.filterMapByKey(webSocketMap, lineId); - stringStringMap.forEach((k,v)->{ - webSocketMap.get(k).sendMessage(message); - - }); - } else { - log.error("监测点" + lineId + ",不在线!"); - } - } - - /** - * 获得此时的 - * 在线监测点 - * - * @return - */ - public static synchronized int getOnlineCount() { - return onlineCount; - } - - /** - * 在线监测点 - * 数加1 - */ - public static synchronized void addOnlineCount() { - WebSocketServer.onlineCount++; - } - - /** - * 在线监测点 - * 数减1 - */ - public static synchronized void subOnlineCount() { - WebSocketServer.onlineCount--; - } - - /** - * 过滤所有键包含指定字符串的条目 - * @param map 原始的Map - * @param substring 要检查的子字符串 - * @return 过滤的Map - */ - public static Map filterMapByKey(ConcurrentHashMap map, String substring) { - Map result = new HashMap<>(); - for (Map.Entry entry : map.entrySet()) { - if (entry.getKey().contains(substring)) { - result.put(entry.getKey(), entry.getValue().toString()); + public void sendMessageToUser(String userId, String message) { + Session session = sessions.get(userId); + if (session != null && session.isOpen()) { + try { + session.getBasicRemote().sendText(message); + } catch (IOException e) { + System.out.println("发送消息给用户 " + userId + " 失败: " + e.getMessage()); } + } else { + System.out.println("用户 " + userId + " 不在线或会话已关闭"); } - return result; } + public void sendMessageToAll(String message) { + sessions.forEach((userId, session) -> { + if (session.isOpen()) { + try { + session.getBasicRemote().sendText(message); + } catch (IOException e) { + System.out.println("发送消息给用户 " + userId + " 失败: " + e.getMessage()); + } + } + }); + } + + private void sendMessage(Session session, String message) { + try { + session.getBasicRemote().sendText(message); + } catch (IOException e) { + System.out.println("发送消息失败: " + e.getMessage()); + } + } + + private void startHeartbeat(Session session, String userId) { + heartbeatExecutor.scheduleAtFixedRate(() -> { + if (session.isOpen()) { + try { + session.getBasicRemote().sendText("ping"); + } catch (IOException e) { + System.out.println("发送心跳给用户 " + userId + " 失败: " + e.getMessage()); + try { + session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "心跳失败")); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + }, 10, 10, TimeUnit.SECONDS); // 每10秒发送一次心跳 + } } \ No newline at end of file