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 redisTemplate; @Autowired - private RedisTemplate redisTemplate; + private RedisTemplate redisTemplateForString; @Autowired private IGbChannelPlayService channelPlayService; @@ -64,10 +56,53 @@ public class CameraChannelService implements CommandLineRunner { @Autowired private UserSetting userSetting; - @Override - public void run(String... args) throws Exception { - // 启动时获取全局token + @Autowired + private DynamicTask dynamicTask; + @Override + public void run(String... args) { + // 启动时获取全局token + String taskKey = UUID.randomUUID().toString(); + if (!refreshToken()) { + log.info("[SY-读取Token]失败,30秒后重试"); + dynamicTask.startDelay(taskKey, ()->{ + this.run(args); + }, 30000); + }else { + log.info("[SY-读取Token] 成功"); + } + } + + private boolean refreshToken() { + String adminToken = redisTemplateForString.opsForValue().get("SYSTEM_ACCESS_TOKEN"); + if (adminToken == null) { + log.warn("[SY读取TOKEN] SYSTEM_ACCESS_TOKEN 读取失败"); + return false; + } + SyTokenManager.INSTANCE.adminToken = adminToken; + + String sm4Key = redisTemplateForString.opsForValue().get("SYSTEM_SM4_KEY"); + if (sm4Key == null) { + log.warn("[SY读取TOKEN] SYSTEM_SM4_KEY 读取失败"); + return false; + } + SyTokenManager.INSTANCE.sm4Key = sm4Key; + + JSONObject appJson = (JSONObject)redisTemplate.opsForValue().get("SYSTEM_APPKEY"); + if (appJson == null) { + log.warn("[SY读取TOKEN] SYSTEM_APPKEY 读取失败"); + return false; + } + SyTokenManager.INSTANCE.appMap.put(appJson.getString("appKey"), appJson.getString("appSecret")); + + JSONObject timeJson = (JSONObject)redisTemplate.opsForValue().get("sys_INTERFACE_VALID_TIME"); + if (timeJson == null) { + log.warn("[SY读取TOKEN] sys_INTERFACE_VALID_TIME 读取失败"); + return false; + } + SyTokenManager.INSTANCE.expires = timeJson.getLong("systemValue"); + + return true; }