临时提交

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

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