From 24c7bfb7563c25650b9729740124b4f45c68a20d Mon Sep 17 00:00:00 2001 From: lin <648540858@qq.com> Date: Wed, 22 Oct 2025 10:55:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/JwtAuthenticationFilter.java | 113 +---------- .../gb28181/dao/CommonGBChannelMapper.java | 4 +- .../iot/vmp/gb28181/dao/GroupMapper.java | 3 +- .../gb28181/dao/provider/ChannelProvider.java | 24 ++- .../iot/vmp/gb28181/event/EventPublisher.java | 11 +- .../gb28181/event/channel/ChannelEvent.java | 11 +- .../service/impl/GbChannelServiceImpl.java | 34 +++- .../iot/vmp/media/zlm/ZLMRESTfulUtils.java | 31 +-- .../service/impl/StreamPushServiceImpl.java | 1 + .../vmp/vmanager/server/ServerController.java | 2 +- .../web/custom/CameraChannelController.java | 7 +- .../conf/CachedBodyHttpServletRequest.java | 122 ++++++++++++ .../custom/conf/SignAuthenticationFilter.java | 176 ++++++++++++++++++ .../custom/service/CameraChannelService.java | 37 ++-- .../vmp/web/custom/service/SyServiceImpl.java | 2 + web/src/views/map/index.vue | 3 +- 16 files changed, 418 insertions(+), 163 deletions(-) create mode 100644 src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java create mode 100644 src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java index c7a459044..19fd4f3df 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java @@ -1,16 +1,9 @@ package com.genersoft.iot.vmp.conf.security; -import cn.hutool.core.util.CharsetUtil; -import cn.hutool.core.util.HexUtil; -import cn.hutool.crypto.SmUtil; -import cn.hutool.crypto.symmetric.SM4; -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONObject; import com.genersoft.iot.vmp.conf.UserSetting; import com.genersoft.iot.vmp.conf.security.dto.JwtUser; import com.genersoft.iot.vmp.storager.dao.dto.Role; import com.genersoft.iot.vmp.storager.dao.dto.User; -import com.genersoft.iot.vmp.web.custom.conf.SyTokenManager; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -18,7 +11,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -26,11 +18,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingRequestWrapper; import java.io.IOException; -import java.time.Instant; import java.util.ArrayList; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; /** * jwt token 过滤器 @@ -48,30 +36,19 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - + protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + ContentCachingRequestWrapper request = new ContentCachingRequestWrapper(servletRequest); // 忽略登录请求的token验证 String requestURI = request.getRequestURI(); if ((requestURI.startsWith("/doc.html") || requestURI.startsWith("/swagger-ui") ) && !userSetting.getDocEnable()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } + if (requestURI.equalsIgnoreCase("/api/user/login")) { chain.doFilter(request, response); return; } - if (requestURI.startsWith("/api/sy")) { - - // 包装原始请求,缓存请求体 - ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); - if (signCheck(wrappedRequest)) { - // 使用参数签名方式校验 - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>() ); - SecurityContextHolder.getContext().setAuthentication(token); - chain.doFilter(wrappedRequest, response); - return; - } - } if (!userSetting.getInterfaceAuthentication()) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>() ); @@ -134,88 +111,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request, response); } - - private boolean signCheck(ContentCachingRequestWrapper request) { - try { - String sign = request.getParameter("sign"); - String appKey = request.getParameter("appKey"); - String accessToken = request.getParameter("accessToken"); - String timestampStr = request.getParameter("timestamp"); - - if (sign == null || appKey == null || accessToken == null || timestampStr == null) { - log.info("[SY-接口验签] 缺少关键参数:sign/appKey/accessToken/timestamp "); - return false; - } - if (SyTokenManager.INSTANCE.appMap.get(appKey) == null) { - log.info("[SY-接口验签] appKey {} 对应的 secret 不存在", appKey); - return false; - } - - Map parameterMap = request.getParameterMap(); - // 参数排序 - Set paramKeys = new TreeSet<>(parameterMap.keySet()); - - // 拼接签名信息 - // 参数拼接 - StringBuilder beforeSign = new StringBuilder(); - for (String paramKey : paramKeys) { - if (paramKey.equals("sign")) { - continue; - } - beforeSign.append(paramKey).append(parameterMap.get(paramKey)[0]); - } - // 如果是post请求的json消息,拼接body字符串 - if (request.getContentLength() > 0 - && request.getMethod().equalsIgnoreCase("POST") - && request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) { - // 读取body内容 - byte[] requestBodyBytes = request.getContentAsByteArray(); - - if (requestBodyBytes.length > 0) { - String requestBody = new String(requestBodyBytes, request.getCharacterEncoding()); - beforeSign.append(requestBody); - } - } - beforeSign.append(SyTokenManager.INSTANCE.appMap.get(appKey)); - // 生成签名 - String buildSign = SmUtil.sm3(beforeSign.toString()); - if (!buildSign.equals(sign)) { - log.info("[SY-接口验签] 失败, 加密前内容: {}", beforeSign); - return false; - } - // 验证请求时间戳 - long timestamp = Long.parseLong(timestampStr); - Instant timeInstant = Instant.ofEpochMilli(timestamp + SyTokenManager.INSTANCE.expires * 60 * 1000); - if (timeInstant.isBefore(Instant.now())) { - log.info("[SY-接口验签] 时间戳已经过期"); - return false; - } - // accessToken校验 - if (accessToken.equals(SyTokenManager.INSTANCE.adminToken)) { - log.info("[SY-接口验签] adminToken已经默认放行"); - return true; - }else { - // 对token进行解密 - SM4 sm4 = SmUtil.sm4(HexUtil.decodeHex(SyTokenManager.INSTANCE.sm4Key)); - String decryptStr = sm4.decryptStr(accessToken, CharsetUtil.CHARSET_UTF_8); - if (decryptStr == null) { - log.info("[SY-接口验签] accessToken解密失败"); - return false; - } - JSONObject jsonObject = JSON.parseObject(decryptStr); - Long expirationTime = jsonObject.getLong("expirationTime"); - if (expirationTime < System.currentTimeMillis()) { - log.info("[SY-接口验签] accessToken 已经过期"); - return false; - } - } - }catch (Exception e) { - log.info("[SY-接口验签] 读取body失败", e); - return false; - } - return true; - - - } - } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java index 45436d83d..795301584 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java @@ -614,7 +614,7 @@ public interface CommonGBChannelMapper { @SelectProvider(type = ChannelProvider.class, method = "queryGbChannelByChannelDeviceIdAndGbDeviceId") - CameraChannel queryGbChannelByChannelDeviceIdAndGbDeviceId(@Param("channelDeviceId") String channelDeviceId, @Param("gbDeviceId") String gbDeviceId); + List queryGbChannelByChannelDeviceIdAndGbDeviceId(@Param("channelDeviceId") String channelDeviceId, @Param("gbDeviceId") String gbDeviceId); @SelectProvider(type = ChannelProvider.class, method = "queryListByDeviceIds") List queryListByDeviceIds(List deviceIds); @@ -659,4 +659,6 @@ public interface CommonGBChannelMapper { @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelByIds") List queryCameraChannelByIds(List ids); + + } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java index 2b15cffe4..365d2f9f8 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/GroupMapper.java @@ -309,13 +309,14 @@ public interface GroupMapper { @Select(" ") List queryCountWithChild(List groupList); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java index d7cd71eb9..fd7976407 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java @@ -216,10 +216,6 @@ public class ChannelProvider { return BASE_SQL + " where channel_type = 0 and data_type = #{dataType} and data_device_id = #{dataDeviceId}"; } - public String queryCameraChannelById(Map params ){ - return BASE_SQL + " where id = #{gbId}"; - } - public String queryListByCivilCode(Map params ){ StringBuilder sqlBuild = new StringBuilder(); sqlBuild.append(BASE_SQL); @@ -548,12 +544,12 @@ public class ChannelProvider { public String queryListForSy(Map params ){ StringBuilder sqlBuild = new StringBuilder(); sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); - sqlBuild.append(" where wdc.channel_type = 0 AND (wdc.gb_ptz_type is null || wdc.gb_ptz_type != 99) AND coalesce(gb_parent_id, parent_id) = #{groupDeviceId}"); + sqlBuild.append(" where wdc.channel_type = 0 AND (wdc.gb_ptz_type is null || wdc.gb_ptz_type != 99) AND coalesce(wdc.gb_parent_id, wdc.parent_id) = #{groupDeviceId}"); if (params.get("online") != null && (Boolean)params.get("online")) { - sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'"); + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'ON'"); } if (params.get("online") != null && !(Boolean)params.get("online")) { - sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'"); + sqlBuild.append(" AND coalesce(wdc.gb_status, wdc.status) = 'OFF'"); } return sqlBuild.toString(); @@ -851,7 +847,17 @@ public class ChannelProvider { } public String queryListForSyMobile(Map params ){ - return BASE_SQL_FOR_CAMERA_DEVICE + - " WHERE wdc.gb_ptz_type = 99 AND coalesce(gb_business_group_id, business_group_id) = #{business}"; + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); + sqlBuild.append(" WHERE wdc.gb_ptz_type = 99 "); + if (params.get("business") != null) { + sqlBuild.append(" AND coalesce(gb_business_group_id, business_group_id) = #{business}"); + } + return sqlBuild.toString(); + } + + + public String queryCameraChannelById(Map params ){ + return BASE_SQL_FOR_CAMERA_DEVICE + " where wdc.id = #{gbId}"; } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java index 84b3b1fce..058ac5d89 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java @@ -60,11 +60,16 @@ public class EventPublisher { applicationEventPublisher.publishEvent(outEvent); } - public void channelEventPublish(CommonGBChannel deviceChannel, ChannelEvent.ChannelEventMessageType type) { - catalogEventPublish(Collections.singletonList(deviceChannel), type); + public void channelEventPublish(CommonGBChannel commonGBChannel, ChannelEvent.ChannelEventMessageType type) { + channelEventPublish(Collections.singletonList(commonGBChannel), type); } - private void catalogEventPublish(List channelList, ChannelEvent.ChannelEventMessageType type) { + public void channelEventPublishForUpdate(CommonGBChannel commonGBChannel, CommonGBChannel deviceChannelForOld) { + ChannelEvent channelEvent = ChannelEvent.getInstanceForUpdate(this, Collections.singletonList(commonGBChannel), Collections.singletonList(deviceChannelForOld)); + applicationEventPublisher.publishEvent(channelEvent); + } + + public void channelEventPublish(List channelList, ChannelEvent.ChannelEventMessageType type) { ChannelEvent channelEvent = ChannelEvent.getInstance(this, type, channelList); applicationEventPublisher.publishEvent(channelEvent); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java index 275ef39e9..a591b5d72 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/channel/ChannelEvent.java @@ -6,7 +6,6 @@ import lombok.Setter; import org.springframework.context.ApplicationEvent; import java.io.Serial; -import java.util.Collections; import java.util.List; /** @@ -26,6 +25,8 @@ public class ChannelEvent extends ApplicationEvent { private List channels; + private List oldChannels; + private ChannelEventMessageType messageType; @@ -41,4 +42,12 @@ public class ChannelEvent extends ApplicationEvent { return channelEvent; } + public static ChannelEvent getInstanceForUpdate(Object source, List channelList, List channelListForOld) { + ChannelEvent channelEvent = new ChannelEvent(source); + channelEvent.setMessageType(ChannelEventMessageType.UPDATE); + channelEvent.setChannels(channelList); + channelEvent.setOldChannels(channelListForOld); + return channelEvent; + } + } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java index 261d71281..33879da3c 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java @@ -9,6 +9,7 @@ import com.genersoft.iot.vmp.gb28181.dao.GroupMapper; import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper; import com.genersoft.iot.vmp.gb28181.dao.RegionMapper; import com.genersoft.iot.vmp.gb28181.event.EventPublisher; +import com.genersoft.iot.vmp.gb28181.event.channel.ChannelEvent; import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent; import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; @@ -75,7 +76,7 @@ public class GbChannelServiceImpl implements IGbChannelService { int result = commonGBChannelMapper.insert(commonGBChannel); try { // 发送通知 - eventPublisher.catalogEventPublish(null, commonGBChannel, CatalogEvent.ADD); + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.ADD); } catch (Exception e) { log.warn("[通道移除通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); } @@ -97,7 +98,7 @@ public class GbChannelServiceImpl implements IGbChannelService { commonGBChannelMapper.delete(gbId); try { // 发送通知 - eventPublisher.catalogEventPublish(null, channel, CatalogEvent.DEL); + eventPublisher.channelEventPublish(channel, ChannelEvent.ChannelEventMessageType.DELETE); } catch (Exception e) { log.warn("[通道移除通知] 发送失败,{}", channel.getGbDeviceId(), e); } @@ -139,15 +140,31 @@ public class GbChannelServiceImpl implements IGbChannelService { if (channels.size() > 1) { throw new ControllerException(ErrorCode.ERROR100.getCode(), "国标编号重复,请修改编号后保存"); } + CommonGBChannel oldChannel = commonGBChannelMapper.queryById(commonGBChannel.getGbId()); commonGBChannel.setUpdateTime(DateUtil.getNow()); int result = commonGBChannelMapper.update(commonGBChannel); if (result > 0) { try { // 发送通知 - eventPublisher.catalogEventPublish(null, commonGBChannel, CatalogEvent.UPDATE); + eventPublisher.channelEventPublishForUpdate(commonGBChannel, oldChannel); } catch (Exception e) { log.warn("[更新通道通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); } + MobilePosition mobilePosition = new MobilePosition(); + mobilePosition.setLongitude(commonGBChannel.getGbLongitude()); + mobilePosition.setLatitude(commonGBChannel.getGbLatitude()); + mobilePosition.setCreateTime(DateUtil.getNow()); + mobilePosition.setDeviceId(commonGBChannel.getGbDeviceId()); + mobilePosition.setTime(DateUtil.getNow()); + mobilePosition.setAltitude(0.0); + mobilePosition.setDirection(0.0); + mobilePosition.setSpeed(0.0); + mobilePosition.setChannelId(commonGBChannel.getGbId()); + try { + eventPublisher.mobilePositionEventPublish(mobilePosition); + }catch (Exception e) { + log.error("[向上级转发移动位置失败] ", e); + } } return result; } @@ -162,7 +179,7 @@ public class GbChannelServiceImpl implements IGbChannelService { if (result > 0) { try { // 发送通知 - eventPublisher.catalogEventPublish(null, commonGBChannel, CatalogEvent.OFF); + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.OFFLINE); } catch (Exception e) { log.warn("[通道离线通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); } @@ -194,7 +211,7 @@ public class GbChannelServiceImpl implements IGbChannelService { if (result > 0) { try { // 发送catalog - eventPublisher.catalogEventPublish(null, commonGBChannelList, CatalogEvent.OFF); + eventPublisher.channelEventPublish(commonGBChannelList, ChannelEvent.ChannelEventMessageType.OFFLINE); } catch (Exception e) { log.warn("[多个通道离线] 发送失败,数量:{}", commonGBChannelList.size(), e); } @@ -212,7 +229,7 @@ public class GbChannelServiceImpl implements IGbChannelService { if (result > 0) { try { // 发送通知 - eventPublisher.catalogEventPublish(null, commonGBChannel, CatalogEvent.ON); + eventPublisher.channelEventPublish(commonGBChannel, ChannelEvent.ChannelEventMessageType.ONLINE); } catch (Exception e) { log.warn("[通道上线通知] 发送失败,{}", commonGBChannel.getGbDeviceId(), e); } @@ -243,7 +260,7 @@ public class GbChannelServiceImpl implements IGbChannelService { } try { // 发送catalog - eventPublisher.catalogEventPublish(null, commonGBChannelList, CatalogEvent.ON); + eventPublisher.channelEventPublish(commonGBChannelList, ChannelEvent.ChannelEventMessageType.ONLINE); } catch (Exception e) { log.warn("[多个通道上线] 发送失败,数量:{}", commonGBChannelList.size(), e); } @@ -274,7 +291,7 @@ public class GbChannelServiceImpl implements IGbChannelService { } try { // 发送catalog - eventPublisher.catalogEventPublish(null, commonGBChannels, CatalogEvent.ADD); + eventPublisher.channelEventPublish(commonGBChannels, ChannelEvent.ChannelEventMessageType.ADD); } catch (Exception e) { log.warn("[多个通道新增] 发送失败,数量:{}", commonGBChannels.size(), e); } @@ -306,6 +323,7 @@ public class GbChannelServiceImpl implements IGbChannelService { try { // 发送通知 eventPublisher.catalogEventPublish(null, commonGBChannels, CatalogEvent.UPDATE); +// eventPublisher.channelEventPublishForUpdate(commonGBChannels, ChannelEvent.ChannelEventMessageType.ADD); } catch (Exception e) { log.warn("[更新多个通道] 发送失败,{}个", commonGBChannels.size(), e); } diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java index 3414eacc7..962f376d9 100755 --- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java @@ -258,22 +258,27 @@ public class ZLMRESTfulUtils { param.put("schema",schema); } param.put("vhost","__defaultVhost__"); - String response = sendPost(mediaServer, "getMediaList",param, (responseStr -> { - if (callback == null) { - return; - } - if (responseStr == null) { - callback.run(ZLMResult.getFailForMediaServer()); - }else { - ZLMResult zlmResult = JSON.parseObject(responseStr, new TypeReference>() {}); - if (zlmResult == null) { + RequestCallback requestCallback = null; + if (callback != null) { + requestCallback = (responseStr -> { + if (callback == null) { + return; + } + if (responseStr == null) { callback.run(ZLMResult.getFailForMediaServer()); }else { - callback.run(zlmResult); - } + ZLMResult zlmResult = JSON.parseObject(responseStr, new TypeReference>() {}); + if (zlmResult == null) { + callback.run(ZLMResult.getFailForMediaServer()); + }else { + callback.run(zlmResult); + } - } - })); + } + }); + } + + String response = sendPost(mediaServer, "getMediaList",param, requestCallback); if (response == null) { return ZLMResult.getFailForMediaServer(); }else { diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java index 1090d3c58..36ef95b6e 100755 --- a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushServiceImpl.java @@ -549,6 +549,7 @@ public class StreamPushServiceImpl implements IStreamPushService { } @Override + @Transactional public void batchUpdate(List streamPushItemForUpdate) { streamPushMapper.batchUpdate(streamPushItemForUpdate); List commonGBChannels = new ArrayList<>(); diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java index b64025ace..375e0bb01 100755 --- a/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java @@ -81,7 +81,7 @@ public class ServerController { private IStreamProxyService proxyService; - @Autowired + @Autowired(required = false) private IMapService mapService; @Value("${server.port}") diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java b/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java index cba453a67..0b61b2c58 100644 --- a/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/CameraChannelController.java @@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.*; @@ -36,6 +37,7 @@ import java.util.List; @Slf4j @RestController @RequestMapping(value = "/api/sy") +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") public class CameraChannelController { @Autowired @@ -81,6 +83,7 @@ public class CameraChannelController { @Parameter(name = "geoCoordSys", description = "坐标系类型:WGS84,GCJ02、BD09") @Parameter(name = "status", description = "摄像头状态") public PageInfo queryListWithChild(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, + @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, @RequestParam(required = false) String query, @RequestParam(required = false) String sortName, @@ -292,7 +295,7 @@ public class CameraChannelController { @Parameter(name = "topGroupAlias", description = "分组别名") public PageInfo queryListForMobile(@RequestParam(required = false, value = "page", defaultValue = "1" )Integer page, @RequestParam(required = false, value = "count", defaultValue = "100")Integer count, - String topGroupAlias){ + @RequestParam(required = false) String topGroupAlias){ return channelService.queryListForMobile(page, count, topGroupAlias); } @@ -313,7 +316,7 @@ public class CameraChannelController { if (streamAuthorityInfo == null || streamAuthorityInfo.getCallId() == null || !streamAuthorityInfo.getCallId().equals(callId)) { - throw new ControllerException(ErrorCode.ERROR400.getCode(), "播放地址鉴权失败"); + throw new ControllerException(ErrorCode.ERROR100.getCode(), "播放地址鉴权失败"); } String host; try { diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java new file mode 100644 index 000000000..50a294de8 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/CachedBodyHttpServletRequest.java @@ -0,0 +1,122 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * 自定义请求包装器,用于缓存请求体内容 + * 解决流只能读取一次的问题 + */ +@Slf4j +public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { + + private byte[] cachedBody; + private String cachedBodyString; + + public CachedBodyHttpServletRequest(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (cachedBody == null) { + cacheInputStream(); + } + return new CachedBodyServletInputStream(cachedBody); + } + + @Override + public BufferedReader getReader() throws IOException { + if (cachedBodyString == null) { + if (cachedBody == null) { + cacheInputStream(); + } + cachedBodyString = new String(cachedBody, StandardCharsets.UTF_8); + } + return new BufferedReader(new StringReader(cachedBodyString)); + } + + /** + * 获取缓存的请求体内容 + */ + public String getCachedBody() { + if (cachedBodyString == null) { + if (cachedBody == null) { + try { + cacheInputStream(); + } catch (IOException e) { + log.warn("缓存请求体失败: {}", e.getMessage()); + return ""; + } + } + cachedBodyString = new String(cachedBody, StandardCharsets.UTF_8); + } + return cachedBodyString; + } + + /** + * 获取缓存的请求体字节数组 + */ + public byte[] getCachedBodyBytes() { + if (cachedBody == null) { + try { + cacheInputStream(); + } catch (IOException e) { + log.warn("缓存请求体失败: {}", e.getMessage()); + return new byte[0]; + } + } + return cachedBody; + } + + private void cacheInputStream() throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream inputStream = super.getInputStream()) { + + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + cachedBody = baos.toByteArray(); + log.debug("成功缓存请求体,长度: {}", cachedBody.length); + } + } + + /** + * 自定义 ServletInputStream 实现 + */ + private static class CachedBodyServletInputStream extends ServletInputStream { + private final ByteArrayInputStream inputStream; + + public CachedBodyServletInputStream(byte[] body) { + this.inputStream = new ByteArrayInputStream(body); + } + + @Override + public boolean isFinished() { + return inputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + // 不需要实现 + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java new file mode 100644 index 000000000..2e476e952 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SignAuthenticationFilter.java @@ -0,0 +1,176 @@ +package com.genersoft.iot.vmp.web.custom.conf; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.symmetric.SM4; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.sip.message.Response; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * sign token 过滤器 + */ + +@Slf4j +@Component +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") +public class SignAuthenticationFilter extends OncePerRequestFilter { + + private final static String WSHeader = "sec-websocket-protocol"; + + + @Autowired + private UserSetting userSetting; + + + @Override + protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + // 忽略登录请求的token验证 + String requestURI = servletRequest.getRequestURI(); + // 包装原始请求,缓存请求体 + CachedBodyHttpServletRequest request = new CachedBodyHttpServletRequest(servletRequest); + if (!requestURI.startsWith("/api/sy")) { + chain.doFilter(request, response); + return; + } + // 设置响应内容类型 + response.setContentType("application/json;charset=UTF-8"); + + try { + String sign = request.getParameter("sign"); + String appKey = request.getParameter("appKey"); + String accessToken = request.getParameter("accessToken"); + String timestampStr = request.getParameter("timestamp"); + + if (sign == null || appKey == null || accessToken == null || timestampStr == null) { + log.info("[SY-接口验签] 缺少关键参数:sign/appKey/accessToken/timestamp, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(6017, "缺少关键参数")); + out.close(); + return; + } + if (SyTokenManager.INSTANCE.appMap.get(appKey) == null) { + log.info("[SY-接口验签] appKey {} 对应的 secret 不存在, 请求地址: {} ", appKey, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(6017, "缺少关键参数")); + out.close(); + return; + } + + Map parameterMap = request.getParameterMap(); + // 参数排序 + Set paramKeys = new TreeSet<>(parameterMap.keySet()); + + // 拼接签名信息 + // 参数拼接 + StringBuilder beforeSign = new StringBuilder(); + for (String paramKey : paramKeys) { + if (paramKey.equals("sign")) { + continue; + } + beforeSign.append(paramKey).append(parameterMap.get(paramKey)[0]); + } + // 如果是post请求的json消息,拼接body字符串 + if (request.getContentLength() > 0 + && request.getMethod().equalsIgnoreCase("POST") + && request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) { + // 读取body内容 - 使用自定义缓存机制 + String requestBody = request.getCachedBody(); + if (!ObjectUtils.isEmpty(requestBody)) { + beforeSign.append(requestBody); + log.debug("[SY-接口验签] 读取到请求体内容,长度: {}", requestBody.length()); + } else { + log.warn("[SY-接口验签] 请求体内容为空"); + } + } + beforeSign.append(SyTokenManager.INSTANCE.appMap.get(appKey)); + // 生成签名 + String buildSign = SmUtil.sm3(beforeSign.toString()); + if (!buildSign.equals(sign)) { + log.info("[SY-接口验签] 失败,加密前内容: {}, 请求地址: {} ", beforeSign, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(6017, "接口鉴权失败")); + out.close(); + return; + } + // 验证请求时间戳 + long timestamp = Long.parseLong(timestampStr); + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis > SyTokenManager.INSTANCE.expires * 60 * 1000 + timestamp ) { + log.info("[SY-接口验签] 时间戳已经过期, 请求时间戳:{}, 当前时间: {}, 过期时间: {}, 请求地址: {} ", timestamp, currentTimeMillis, timestamp + SyTokenManager.INSTANCE.expires * 60 * 1000, requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(6016, "接口过期")); + out.close(); + return; + } + // accessToken校验 + if (accessToken.equals(SyTokenManager.INSTANCE.adminToken)) { + log.info("[SY-接口验签] adminToken已经默认放行, 请求地址: {} ", requestURI); + chain.doFilter(request, response); + return; + }else { + // 对token进行解密 + SM4 sm4 = SmUtil.sm4(HexUtil.decodeHex(SyTokenManager.INSTANCE.sm4Key)); + String decryptStr = sm4.decryptStr(accessToken, CharsetUtil.CHARSET_UTF_8); + if (decryptStr == null) { + log.info("[SY-接口验签] accessToken解密失败, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(6017, "接口鉴权失败")); + out.close(); + return; + } + JSONObject jsonObject = JSON.parseObject(decryptStr); + Long expirationTime = jsonObject.getLong("expirationTime"); + if (expirationTime < System.currentTimeMillis()) { + log.info("[SY-接口验签] accessToken 已经过期, 请求地址: {} ", requestURI); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(6018, "Token已过期")); + out.close(); + return; + } + } + }catch (Exception e) { + log.info("[SY-接口验签] 读取body失败, 请求地址: {} ", requestURI, e); + response.setStatus(Response.OK); + PrintWriter out = response.getWriter(); + out.println(getErrorResult(6017, "接口鉴权异常")); + out.close(); + return; + } + chain.doFilter(request, response); + } + + private String getErrorResult(Integer code, String message) { + WVPResult wvpResult = new WVPResult<>(); + wvpResult.setCode(code); + wvpResult.setMsg(message); + return JSON.toJSONString(wvpResult); + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java b/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java index a36f53be4..7a3b41dc1 100644 --- a/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/service/CameraChannelService.java @@ -26,6 +26,7 @@ import com.github.xiaoymin.knife4j.core.util.Assert; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.event.EventListener; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -34,6 +35,7 @@ import java.util.*; @Slf4j @Service +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") public class CameraChannelService implements CommandLineRunner { private final String REDIS_GPS_MESSAGE = "VM_MSG_MOBILE_GPS"; @@ -176,6 +178,8 @@ public class CameraChannelService implements CommandLineRunner { jsonObject.put("altitude", mobilePosition.getAltitude()); jsonObject.put("direction", mobilePosition.getDirection()); jsonObject.put("speed", mobilePosition.getSpeed()); + jsonObject.put("topGroupGAlias", cameraChannel.getTopGroupGAlias()); + jsonObject.put("groupAlias", cameraChannel.getGroupAlias()); log.debug("[redis发送通知] 发送 移动设备位置信息移动位置 {}: {}", REDIS_GPS_MESSAGE, jsonObject.toString()); redisTemplate.convertAndSend(REDIS_GPS_MESSAGE, jsonObject); } @@ -308,9 +312,9 @@ public class CameraChannelService implements CommandLineRunner { } public CameraChannel queryOne(String deviceId, String deviceCode, String geoCoordSys) { - CameraChannel channel = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); - Assert.notNull(channel, "通道不存在"); - + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); if (geoCoordSys != null && channel.getGbLongitude() != null && channel.getGbLatitude() != null && channel.getGbLongitude() > 0 && channel.getGbLatitude() > 0) { if (geoCoordSys.equalsIgnoreCase("GCJ02")) { @@ -337,8 +341,9 @@ public class CameraChannelService implements CommandLineRunner { * @param callback 点播结果的回放 */ public void play(String deviceId, String deviceCode, ErrorCallback callback) { - CommonGBChannel channel = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); - Assert.notNull(channel, "通道不存在"); + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); channelPlayService.play(channel, null, userSetting.getRecordSip(), (code, msg, data) -> { callback.run(code, msg, new CameraStreamInfo(channel, data)); }); @@ -350,14 +355,16 @@ public class CameraChannelService implements CommandLineRunner { * @param deviceCode 通道对应的国标设备的编号 */ public void stopPlay(String deviceId, String deviceCode) { - CommonGBChannel channel = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); - Assert.notNull(channel, "通道不存在"); + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); channelPlayService.stopPlay(channel); } public void ptz(String deviceId, String deviceCode, String command, Integer speed, ErrorCallback callback) { - CommonGBChannel channel = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); - Assert.notNull(channel, "通道不存在"); + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel channel = cameraChannels.get(0); if (speed == null) { speed = 50; @@ -412,8 +419,9 @@ public class CameraChannelService implements CommandLineRunner { } public void updateCamera(String deviceId, String deviceCode, String name, Double longitude, Double latitude, String geoCoordSys) { - CommonGBChannel commonGBChannel = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); - Assert.notNull(commonGBChannel, "通道不存在"); + List cameraChannels = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode); + Assert.isTrue(cameraChannels.isEmpty(), "通道不存在"); + CameraChannel commonGBChannel = cameraChannels.get(0); commonGBChannel.setGbName(name); if (geoCoordSys != null && longitude != null && latitude != null && longitude > 0 && latitude > 0) { @@ -540,12 +548,15 @@ public class CameraChannelService implements CommandLineRunner { public PageInfo queryListForMobile(Integer page, Integer count, String topGroupAlias) { CameraGroup cameraGroup = groupMapper.queryGroupByAlias(topGroupAlias); - Assert.notNull(cameraGroup, "组织结构不存在"); + String business = null; + if (cameraGroup != null) { + business = cameraGroup.getDeviceId(); + } // 构建分页 PageHelper.startPage(page, count); + List all = channelMapper.queryListForSyMobile(business); - List all = channelMapper.queryListForSyMobile(cameraGroup.getDeviceId()); PageInfo groupPageInfo = new PageInfo<>(all); List list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), null); groupPageInfo.setList(list); diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java index 11fc91a28..9c2c994ee 100644 --- a/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/web/custom/service/SyServiceImpl.java @@ -6,6 +6,7 @@ import com.genersoft.iot.vmp.service.IMapService; import com.genersoft.iot.vmp.vmanager.bean.MapConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -17,6 +18,7 @@ import java.util.List; */ @Slf4j @Service +@ConditionalOnProperty(value = "sy.enable", havingValue = "true") public class SyServiceImpl implements IMapService { @Autowired diff --git a/web/src/views/map/index.vue b/web/src/views/map/index.vue index c12120c56..419f9b095 100755 --- a/web/src/views/map/index.vue +++ b/web/src/views/map/index.vue @@ -35,7 +35,7 @@ -
+