From b13207db92631fe7682113b7d9a93db663dc1d45 Mon Sep 17 00:00:00 2001
From: lin <648540858@qq.com>
Date: Fri, 10 Oct 2025 17:08:32 +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
---
pom.xml | 12 ++
.../security/JwtAuthenticationFilter.java | 126 ++++++++++++++++--
.../service/impl/GroupServiceImpl.java | 8 +-
.../request/impl/InviteRequestProcessor.java | 1 +
.../redisMsg/RedisGroupMsgListener.java | 35 +++--
.../vmp/web/custom/conf/SyTokenManager.java | 31 +++++
.../custom/service/CameraChannelService.java | 67 +++++++---
7 files changed, 232 insertions(+), 48 deletions(-)
create mode 100644 src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java
diff --git a/pom.xml b/pom.xml
index 60a9f8333..5cea886c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -380,6 +380,18 @@
1.0.10
+
+ cn.hutool
+ hutool-all
+ 5.8.38
+
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ 1.78.1
+
+
org.springframework.boot
spring-boot-starter-test
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 fb10eb033..4ed301954 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,27 +1,42 @@
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 org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.stereotype.Component;
-import org.springframework.web.filter.OncePerRequestFilter;
-
+import com.genersoft.iot.vmp.web.custom.conf.SyTokenManager;
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.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;
+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 过滤器
*/
+@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@@ -37,7 +52,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 忽略登录请求的token验证
String requestURI = request.getRequestURI();
- if ((requestURI.startsWith("/doc.html") || requestURI.startsWith("/swagger-ui") ) && !userSetting.getDocEnable()) {
+ if ((requestURI.startsWith("/doc.html") || requestURI.startsWith("/swagger-ui") ) && !userSetting.getDocEnable()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
@@ -45,6 +60,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
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<>() );
@@ -53,8 +80,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
return;
}
-
-
String jwt = request.getHeader(JwtUtils.getHeader());
// 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
// 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
@@ -110,4 +135,85 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
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();
+ parameterMap.remove("sign");
+ // 参数排序
+ Set paramKeys = new TreeSet<>(parameterMap.keySet());
+
+ // 拼接签名信息
+ // 参数拼接
+ StringBuilder beforeSign = new StringBuilder();
+ for (String paramKey : paramKeys) {
+ 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.getLong(timestampStr);
+ Instant timeInstant = Instant.ofEpochMilli(timestamp + SyTokenManager.INSTANCE.expires * 60 * 1000);
+ if (timeInstant.isAfter(Instant.now())) {
+ log.info("[SY-接口验签] 时间戳已经过期");
+ return false;
+ }
+ // accessToken校验
+ if (accessToken.equals(SyTokenManager.INSTANCE.adminToken)) {
+ log.info("[SY-接口验签] 时间戳已经过期");
+ 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/service/impl/GroupServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java
index 6135b0c10..f308438f2 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GroupServiceImpl.java
@@ -44,12 +44,12 @@ public class GroupServiceImpl implements IGroupService {
@Override
public void add(Group group) {
Assert.notNull(group, "参数不可为NULL");
- Assert.notNull(group.getDeviceId(), "设备编号不可为NULL");
- Assert.isTrue(group.getDeviceId().trim().length() == 20, "设备编号必须为20位");
- Assert.notNull(group.getName(), "设备编号不可为NULL");
+ Assert.notNull(group.getDeviceId(), "分组编号不可为NULL");
+ Assert.isTrue(group.getDeviceId().trim().length() == 20, "分组编号必须为20位");
+ Assert.notNull(group.getName(), "分组名称不可为NULL");
GbCode gbCode = GbCode.decode(group.getDeviceId());
- Assert.notNull(gbCode, "设备编号不满足国标定义");
+ Assert.notNull(gbCode, "分组编号不满足国标定义");
// 查询数据库中已经存在的.
List groupListInDb = groupManager.queryInGroupListByDeviceId(Lists.newArrayList(group));
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
index 894d13f31..751a9f9f4 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
@@ -277,6 +277,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
(channelIdArrayFromSub != null? channelIdArrayFromSub[0]: null);
String requesterId = SipUtils.getUserIdFromFromHeader(request);
CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
+
if (requesterId == null || channelId == null) {
log.warn("[解析INVITE消息] 无法从请求中获取到来源id,返回400错误");
throw new InviteDecodeException(Response.BAD_REQUEST, "request decode fail");
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java
index c5ff20dda..cb1352801 100755
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGroupMsgListener.java
@@ -27,8 +27,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
* @Auther: JiangFeng
* @Date: 2022/8/16 11:32
* @Description: 接收redis发送的推流设备列表更新通知
- * 监听: SUBSCRIBE VM_MSG_PUSH_STREAM_LIST_CHANGE
- * 发布 PUBLISH VM_MSG_PUSH_STREAM_LIST_CHANGE '[{"app":1000,"stream":10000000,"gbId":"12345678901234567890","name":"A6","status":false},{"app":1000,"stream":10000021,"gbId":"24212345671381000021","name":"终端9273","status":false},{"app":1000,"stream":10000022,"gbId":"24212345671381000022","name":"终端9434","status":true},{"app":1000,"stream":10000025,"gbId":"24212345671381000025","name":"华为M10","status":false},{"app":1000,"stream":10000051,"gbId":"11111111111381111122","name":"终端9720","status":false}]'
+ * 监听: SUBSCRIBE VM_MSG_GROUP_LIST_RESPONSE
+ * 发布 PUBLISH VM_MSG_GROUP_LIST_RESPONSE '[{"groupName":"研发AAS","topGroupGAlias":"6","groupAlias":"6"},{"groupName":"测试AAS","topGroupGAlias":"5","groupAlias":"5"},{"groupName":"研发2","topGroupGAlias":"4","groupAlias":"4"},{"groupName":"啊实打实的","topGroupGAlias":"4","groupAlias":"100000009"},{"groupName":"测试域","topGroupGAlias":"3","groupAlias":"3"},{"groupName":"结构1","topGroupGAlias":"3","groupAlias":"100000000"},{"groupName":"结构1-1","topGroupGAlias":"3","parentGroupGAlias":"100000000","groupAlias":"100000001"},{"groupName":"结构2-2","topGroupGAlias":"3","groupAlias":"100000002"},{"groupName":"结构1-2","topGroupGAlias":"3","parentGroupGAlias":"100000001","groupAlias":"100000003"},{"groupName":"结构1-3","topGroupGAlias":"3","parentGroupGAlias":"100000003","groupAlias":"100000004"},{"groupName":"研发1","topGroupGAlias":"3","groupAlias":"100000005"},{"groupName":"研发3","topGroupGAlias":"3","parentGroupGAlias":"100000005","groupAlias":"100000006"},{"groupName":"测试42","topGroupGAlias":"3","parentGroupGAlias":"100000000","groupAlias":"100000010"},{"groupName":"测试2","topGroupGAlias":"3","parentGroupGAlias":"100000000","groupAlias":"100000011"},{"groupName":"测试3","topGroupGAlias":"3","parentGroupGAlias":"100000000","groupAlias":"100000007"},{"groupName":"测试4","topGroupGAlias":"3","parentGroupGAlias":"100000007","groupAlias":"100000008"}]'
*/
@Slf4j
@Component
@@ -120,10 +120,11 @@ public class RedisGroupMsgListener implements MessageListener {
String deviceId = buildGroupDeviceId(isTop);
group.setDeviceId(deviceId);
group.setAlias(groupMessage.getGroupAlias());
+ group.setName(groupMessage.getGroupName());
if (!isTop) {
- if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias()) || ObjectUtils.isEmpty(groupMessage.getParentGAlias())) {
- log.info("[REDIS消息-业务分组同步回复] 消息缺失业务分组别名或者父节点别名, {}", groupMessage.toString());
+ if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) {
+ log.info("[REDIS消息-业务分组同步回复] 消息缺失业务分组别名, {}", groupMessage.toString());
continue;
}
@@ -132,15 +133,16 @@ public class RedisGroupMsgListener implements MessageListener {
log.info("[REDIS消息-业务分组同步回复] 业务分组信息未入库, {}", groupMessage.toString());
continue;
}
- group.setBusinessGroup(groupMessage.getTopGroupGbId());
- Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias());
- if (parentGroup == null) {
- log.info("[REDIS消息-业务分组同步回复] 虚拟组织父节点信息未入库, {}", groupMessage.toString());
- continue;
+ group.setBusinessGroup(topGroup.getDeviceId());
+ if (groupMessage.getParentGAlias() != null) {
+ Group parentGroup = groupService.queryGroupByAlias(groupMessage.getParentGAlias());
+ if (parentGroup == null) {
+ log.info("[REDIS消息-业务分组同步回复] 虚拟组织父节点信息未入库, {}", groupMessage.toString());
+ continue;
+ }
+ group.setParentId(parentGroup.getId());
+ group.setParentDeviceId(parentGroup.getDeviceId());
}
- group.setParentId(parentGroup.getId());
- group.setParentDeviceId(parentGroup.getDeviceId());
-
}
group.setCreateTime(DateUtil.getNow());
group.setUpdateTime(DateUtil.getNow());
@@ -148,13 +150,10 @@ public class RedisGroupMsgListener implements MessageListener {
}else {
boolean isTop = groupMessage.getTopGroupGAlias().equals(groupMessage.getGroupAlias());
- String deviceId = buildGroupDeviceId(isTop);
- group.setDeviceId(deviceId);
- group.setAlias(groupMessage.getGroupAlias());
-
+ group.setName(groupMessage.getGroupName());
if (!isTop) {
- if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias()) || ObjectUtils.isEmpty(groupMessage.getParentGAlias())) {
- log.info("[REDIS消息-业务分组同步回复] 消息缺失业务分组别名或者父节点别名, {}", groupMessage.toString());
+ if (ObjectUtils.isEmpty(groupMessage.getTopGroupGAlias())) {
+ log.info("[REDIS消息-业务分组同步回复] 消息缺失业务分组别名, {}", groupMessage.toString());
continue;
}
diff --git a/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java
new file mode 100644
index 000000000..0d98244db
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/web/custom/conf/SyTokenManager.java
@@ -0,0 +1,31 @@
+package com.genersoft.iot.vmp.web.custom.conf;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum SyTokenManager {
+ INSTANCE;
+
+ /**
+ * 普通用户 app Key 和 secret
+ */
+ public final Map appMap = new HashMap<>();
+
+
+ /**
+ * 管理员专属token
+ */
+ public String adminToken;
+
+ /**
+ * sm4密钥
+ */
+ public String sm4Key;
+
+ /**
+ * 接口有效时长,单位分钟
+ */
+ public Long expires;
+
+
+}
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 df8e068a0..bb03e84f1 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
@@ -3,38 +3,33 @@ package com.genersoft.iot.vmp.web.custom.service;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.conf.exception.ControllerException;
import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
import com.genersoft.iot.vmp.gb28181.bean.FrontEndControlCodeForPTZ;
import com.genersoft.iot.vmp.gb28181.bean.Group;
import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
-import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper;
import com.genersoft.iot.vmp.gb28181.dao.GroupMapper;
import com.genersoft.iot.vmp.gb28181.service.IGbChannelControlService;
import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
-import com.genersoft.iot.vmp.gb28181.service.IGroupService;
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
import com.genersoft.iot.vmp.utils.Coordtransform;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import com.genersoft.iot.vmp.web.custom.bean.CameraChannel;
import com.genersoft.iot.vmp.web.custom.bean.CameraGroup;
import com.genersoft.iot.vmp.web.custom.bean.Point;
-import com.genersoft.iot.vmp.web.custom.bean.PolygonQueryParam;
+import com.genersoft.iot.vmp.web.custom.conf.SyTokenManager;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.github.xiaoymin.knife4j.core.util.Assert;
-import com.google.common.base.CaseFormat;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
@Slf4j
@Service
@@ -43,17 +38,14 @@ public class CameraChannelService implements CommandLineRunner {
@Autowired
private CommonGBChannelMapper channelMapper;
- @Autowired
- private DeviceMapper deviceMapper;
-
@Autowired
private GroupMapper groupMapper;
@Autowired
- private IGroupService groupService;
+ private RedisTemplate