临时提交

This commit is contained in:
lin
2025-10-22 10:55:15 +08:00
parent d48e0cc031
commit 24c7bfb756
16 changed files with 418 additions and 163 deletions

View File

@@ -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<String, String[]> parameterMap = request.getParameterMap();
// 参数排序
Set<String> 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;
}
}

View File

@@ -614,7 +614,7 @@ public interface CommonGBChannelMapper {
@SelectProvider(type = ChannelProvider.class, method = "queryGbChannelByChannelDeviceIdAndGbDeviceId")
CameraChannel queryGbChannelByChannelDeviceIdAndGbDeviceId(@Param("channelDeviceId") String channelDeviceId, @Param("gbDeviceId") String gbDeviceId);
List<CameraChannel> queryGbChannelByChannelDeviceIdAndGbDeviceId(@Param("channelDeviceId") String channelDeviceId, @Param("gbDeviceId") String gbDeviceId);
@SelectProvider(type = ChannelProvider.class, method = "queryListByDeviceIds")
List<CameraChannel> queryListByDeviceIds(List<String> deviceIds);
@@ -659,4 +659,6 @@ public interface CommonGBChannelMapper {
@SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelByIds")
List<CameraChannel> queryCameraChannelByIds(List<Integer> ids);
}

View File

@@ -309,13 +309,14 @@ public interface GroupMapper {
@Select(" <script>" +
" SELECT " +
" coalesce( wdc.gb_parent_id, wdc.parent_id) as deviceId," +
" ANY_VALUE(coalesce( wdc.gb_parent_id, wdc.parent_id)) as deviceId," +
" COUNT(*) AS allCount," +
" SUM(CASE WHEN coalesce( wdc.gb_status, wdc.status) = 'ON' THEN 1 ELSE 0 END) AS onlineCount" +
" FROM " +
" wvp_device_channel wdc " +
" where coalesce( wdc.gb_parent_id, wdc.parent_id) in " +
" <foreach collection='groupList' item='item' open='(' separator=',' close=')' > #{item.deviceId}</foreach>" +
" GROUP BY coalesce(wdc.gb_parent_id, wdc.parent_id)" +
"</script>")
List<CameraCount> queryCountWithChild(List<CameraGroup> groupList);
}

View File

@@ -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<String, Object> params ){
return BASE_SQL + " where id = #{gbId}";
}
public String queryListByCivilCode(Map<String, Object> params ){
StringBuilder sqlBuild = new StringBuilder();
sqlBuild.append(BASE_SQL);
@@ -548,12 +544,12 @@ public class ChannelProvider {
public String queryListForSy(Map<String, Object> 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<String, Object> 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<String, Object> params ){
return BASE_SQL_FOR_CAMERA_DEVICE + " where wdc.id = #{gbId}";
}
}

View File

@@ -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<CommonGBChannel> 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<CommonGBChannel> channelList, ChannelEvent.ChannelEventMessageType type) {
ChannelEvent channelEvent = ChannelEvent.getInstance(this, type, channelList);
applicationEventPublisher.publishEvent(channelEvent);
}

View File

@@ -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<CommonGBChannel> channels;
private List<CommonGBChannel> oldChannels;
private ChannelEventMessageType messageType;
@@ -41,4 +42,12 @@ public class ChannelEvent extends ApplicationEvent {
return channelEvent;
}
public static ChannelEvent getInstanceForUpdate(Object source, List<CommonGBChannel> channelList, List<CommonGBChannel> channelListForOld) {
ChannelEvent channelEvent = new ChannelEvent(source);
channelEvent.setMessageType(ChannelEventMessageType.UPDATE);
channelEvent.setChannels(channelList);
channelEvent.setOldChannels(channelListForOld);
return channelEvent;
}
}

View File

@@ -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);
}

View File

@@ -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<JSONArray> zlmResult = JSON.parseObject(responseStr, new TypeReference<ZLMResult<JSONArray>>() {});
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<JSONArray> zlmResult = JSON.parseObject(responseStr, new TypeReference<ZLMResult<JSONArray>>() {});
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 {

View File

@@ -549,6 +549,7 @@ public class StreamPushServiceImpl implements IStreamPushService {
}
@Override
@Transactional
public void batchUpdate(List<StreamPush> streamPushItemForUpdate) {
streamPushMapper.batchUpdate(streamPushItemForUpdate);
List<CommonGBChannel> commonGBChannels = new ArrayList<>();

View File

@@ -81,7 +81,7 @@ public class ServerController {
private IStreamProxyService proxyService;
@Autowired
@Autowired(required = false)
private IMapService mapService;
@Value("${server.port}")

View File

@@ -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<CameraChannel> 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<CameraChannel> 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 {

View File

@@ -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();
}
}
}

View File

@@ -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<String, String[]> parameterMap = request.getParameterMap();
// 参数排序
Set<String> 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<Object> wvpResult = new WVPResult<>();
wvpResult.setCode(code);
wvpResult.setMsg(message);
return JSON.toJSONString(wvpResult);
}
}

View File

@@ -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<CameraChannel> 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<CameraStreamInfo> callback) {
CommonGBChannel channel = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode);
Assert.notNull(channel, "通道不存在");
List<CameraChannel> 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<CameraChannel> 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<String> callback) {
CommonGBChannel channel = channelMapper.queryGbChannelByChannelDeviceIdAndGbDeviceId(deviceId, deviceCode);
Assert.notNull(channel, "通道不存在");
List<CameraChannel> 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<CameraChannel> 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<CameraChannel> 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<CameraChannel> all = channelMapper.queryListForSyMobile(business);
List<CameraChannel> all = channelMapper.queryListForSyMobile(cameraGroup.getDeviceId());
PageInfo<CameraChannel> groupPageInfo = new PageInfo<>(all);
List<CameraChannel> list = addIconPathAndPositionForCameraChannelList(groupPageInfo.getList(), null);
groupPageInfo.setList(list);

View File

@@ -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