Commit f78657473e85487c09cd3c10112db48a0c4a7c4e

Authored by xiaoQQya
1 parent 8d7d751d

fix(报警推送): 修复报警推送功能无效的问题

该问题原因为 com.genersoft.iot.vmp.conf.GlobalResponseAdvice 类改变了 sse 响应体的数据结构,导致前端无法正确解析 sse 数据,调试后未发现 GlobalResponseAdvice 如何修改的 sse 数据结构,故根据 sse 消息体结构自定义实现了 sse 连接
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java
1 1 package com.genersoft.iot.vmp.conf.security;
2 2  
3 3 import com.genersoft.iot.vmp.conf.UserSetting;
4   -import org.springframework.core.annotation.Order;
5 4 import org.slf4j.Logger;
6 5 import org.slf4j.LoggerFactory;
7 6 import org.springframework.beans.factory.annotation.Autowired;
8 7 import org.springframework.context.annotation.Bean;
9 8 import org.springframework.context.annotation.Configuration;
  9 +import org.springframework.core.annotation.Order;
10 10 import org.springframework.security.authentication.AuthenticationManager;
11 11 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
12 12 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
... ... @@ -28,6 +28,7 @@ import java.util.Arrays;
28 28  
29 29 /**
30 30 * 配置Spring Security
  31 + *
31 32 * @author lin
32 33 */
33 34 @Configuration
... ... @@ -75,6 +76,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
75 76 matchers.add("/js/**");
76 77 matchers.add("/api/device/query/snap/**");
77 78 matchers.add("/record_proxy/*/**");
  79 + matchers.add("/api/emit");
78 80 matchers.addAll(userSetting.getInterfaceAuthenticationExcludes());
79 81 // 可以直接访问的静态数据
80 82 web.ignoring().antMatchers(matchers.toArray(new String[0]));
... ... @@ -83,6 +85,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
83 85  
84 86 /**
85 87 * 配置认证方式
  88 + *
86 89 * @param auth
87 90 * @throws Exception
88 91 */
... ... @@ -111,7 +114,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
111 114 .authorizeRequests()
112 115 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
113 116 .antMatchers(userSetting.getInterfaceAuthenticationExcludes().toArray(new String[0])).permitAll()
114   - .antMatchers("/api/user/login","/index/hook/**","/zlm_Proxy/FhTuMYqB2HeCuNOb/record/t/1/2023-03-25/16:35:07-16:35:16-9353.mp4").permitAll()
  117 + .antMatchers("/api/user/login", "/index/hook/**").permitAll()
115 118 .anyRequest().authenticated()
116 119 // 异常处理器
117 120 .and()
... ... @@ -124,7 +127,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
124 127  
125 128 }
126 129  
127   - CorsConfigurationSource configurationSource(){
  130 + CorsConfigurationSource configurationSource() {
128 131 // 配置跨域
129 132 CorsConfiguration corsConfiguration = new CorsConfiguration();
130 133 corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
... ... @@ -135,7 +138,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
135 138 corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader()));
136 139  
137 140 UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource();
138   - url.registerCorsConfiguration("/**",corsConfiguration);
  141 + url.registerCorsConfiguration("/**", corsConfiguration);
139 142 return url;
140 143 }
141 144  
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java
1 1 package com.genersoft.iot.vmp.gb28181.event.alarm;
2 2  
  3 +import org.jetbrains.annotations.NotNull;
  4 +import org.slf4j.Logger;
  5 +import org.slf4j.LoggerFactory;
3 6 import org.springframework.context.ApplicationListener;
4 7 import org.springframework.stereotype.Component;
5   -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
6   -import java.io.IOException;
7   -import java.util.Hashtable;
  8 +
  9 +import java.io.PrintWriter;
8 10 import java.util.Iterator;
9 11 import java.util.Map;
10   -
11   -import org.slf4j.Logger;
12   -import org.slf4j.LoggerFactory;
  12 +import java.util.concurrent.ConcurrentHashMap;
13 13  
14 14 /**
15   - * @description: 报警事件监听
16   - * @author: lawrencehj
17   - * @data: 2021-01-20
  15 + * 报警事件监听器.
  16 + *
  17 + * @author lawrencehj
  18 + * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
  19 + * @since 2021/01/20
18 20 */
19   -
20 21 @Component
21 22 public class AlarmEventListener implements ApplicationListener<AlarmEvent> {
22 23  
23   - private final static Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
  24 + private static final Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
24 25  
25   - private static Map<String, SseEmitter> sseEmitters = new Hashtable<>();
  26 + private static final Map<String, PrintWriter> SSE_CACHE = new ConcurrentHashMap<>();
26 27  
27   - public void addSseEmitters(String browserId, SseEmitter sseEmitter) {
28   - sseEmitters.put(browserId, sseEmitter);
  28 + public void addSseEmitter(String browserId, PrintWriter writer) {
  29 + SSE_CACHE.put(browserId, writer);
  30 + logger.info("SSE 在线数量: {}", SSE_CACHE.size());
  31 + }
  32 +
  33 + public void removeSseEmitter(String browserId, PrintWriter writer) {
  34 + SSE_CACHE.remove(browserId, writer);
  35 + logger.info("SSE 在线数量: {}", SSE_CACHE.size());
29 36 }
30 37  
31 38 @Override
32   - public void onApplicationEvent(AlarmEvent event) {
  39 + public void onApplicationEvent(@NotNull AlarmEvent event) {
33 40 if (logger.isDebugEnabled()) {
34   - logger.debug("设备报警事件触发,deviceId:" + event.getAlarmInfo().getDeviceId() + ", "
35   - + event.getAlarmInfo().getAlarmDescription());
  41 + logger.debug("设备报警事件触发, deviceId: {}, {}", event.getAlarmInfo().getDeviceId(), event.getAlarmInfo().getAlarmDescription());
36 42 }
37   - String msg = "<strong>设备编码:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
38   - + "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
39   - + "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>"
40   - + "<br><strong>报警位置:</strong> <i>" + event.getAlarmInfo().getLongitude() + "</i>"
41   - + ", <i>" + event.getAlarmInfo().getLatitude() + "</i>";
42   -
43   - for (Iterator<Map.Entry<String, SseEmitter>> it = sseEmitters.entrySet().iterator(); it.hasNext();) {
44   - Map.Entry<String, SseEmitter> emitter = it.next();
45   - logger.info("推送到SSE连接,浏览器ID: " + emitter.getKey());
  43 +
  44 + String msg = "<strong>设备编号:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
  45 + + "<br><strong>通道编号:</strong> <i>" + event.getAlarmInfo().getChannelId() + "</i>"
  46 + + "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
  47 + + "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>";
  48 +
  49 + for (Iterator<Map.Entry<String, PrintWriter>> it = SSE_CACHE.entrySet().iterator(); it.hasNext(); ) {
  50 + Map.Entry<String, PrintWriter> response = it.next();
  51 + logger.info("推送到 SSE 连接, 浏览器 ID: {}", response.getKey());
46 52 try {
47   - emitter.getValue().send(msg);
48   - } catch (IOException | IllegalStateException e) {
49   - if (logger.isDebugEnabled()) {
50   - logger.debug("SSE连接已关闭");
  53 + PrintWriter writer = response.getValue();
  54 +
  55 + if (writer.checkError()) {
  56 + it.remove();
  57 + continue;
51 58 }
52   - // 移除已关闭的连接
  59 +
  60 + String sseMsg = "event:message\n" +
  61 + "data:" + msg + "\n" +
  62 + "\n";
  63 + writer.write(sseMsg);
  64 + writer.flush();
  65 + } catch (Exception e) {
53 66 it.remove();
54 67 }
55 68 }
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java deleted 100755 → 0
1   -package com.genersoft.iot.vmp.vmanager.gb28181.SseController;
2   -
3   -import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEventListener;
4   -
5   -import io.swagger.v3.oas.annotations.tags.Tag;
6   -import org.springframework.beans.factory.annotation.Autowired;
7   -import org.springframework.stereotype.Controller;
8   -import org.springframework.web.bind.annotation.CrossOrigin;
9   -import org.springframework.web.bind.annotation.GetMapping;
10   -import org.springframework.web.bind.annotation.RequestMapping;
11   -import org.springframework.web.bind.annotation.RequestParam;
12   -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
13   -
14   -/**
15   - * @description: SSE推送
16   - * @author: lawrencehj
17   - * @data: 2021-01-20
18   - */
19   -@Tag(name = "SSE推送")
20   -
21   -@Controller
22   -@RequestMapping("/api")
23   -public class SseController {
24   - @Autowired
25   - AlarmEventListener alarmEventListener;
26   -
27   - @GetMapping("/emit")
28   - public SseEmitter emit(@RequestParam String browserId) {
29   - final SseEmitter sseEmitter = new SseEmitter(0L);
30   - try {
31   - alarmEventListener.addSseEmitters(browserId, sseEmitter);
32   - }catch (Exception e){
33   - sseEmitter.completeWithError(e);
34   - }
35   - return sseEmitter;
36   - }
37   -}
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/sse/SseController.java 0 → 100644
  1 +package com.genersoft.iot.vmp.vmanager.gb28181.sse;
  2 +
  3 +import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEventListener;
  4 +import io.swagger.v3.oas.annotations.tags.Tag;
  5 +import org.springframework.web.bind.annotation.GetMapping;
  6 +import org.springframework.web.bind.annotation.RequestMapping;
  7 +import org.springframework.web.bind.annotation.RequestParam;
  8 +import org.springframework.web.bind.annotation.RestController;
  9 +
  10 +import javax.annotation.Resource;
  11 +import javax.servlet.http.HttpServletResponse;
  12 +import java.io.IOException;
  13 +import java.io.PrintWriter;
  14 +
  15 +
  16 +/**
  17 + * SSE 推送.
  18 + *
  19 + * @author lawrencehj
  20 + * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
  21 + * @since 2021/01/20
  22 + */
  23 +@Tag(name = "SSE 推送")
  24 +@RestController
  25 +@RequestMapping("/api")
  26 +public class SseController {
  27 +
  28 + @Resource
  29 + private AlarmEventListener alarmEventListener;
  30 +
  31 + /**
  32 + * SSE 推送.
  33 + *
  34 + * @param response 响应
  35 + * @param browserId 浏览器ID
  36 + * @throws IOException IOEXCEPTION
  37 + * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
  38 + * @since 2023/11/06
  39 + */
  40 + @GetMapping("/emit")
  41 + public void emit(HttpServletResponse response, @RequestParam String browserId) throws IOException, InterruptedException {
  42 + response.setContentType("text/event-stream");
  43 + response.setCharacterEncoding("utf-8");
  44 +
  45 + PrintWriter writer = response.getWriter();
  46 + alarmEventListener.addSseEmitter(browserId, writer);
  47 +
  48 + while (!writer.checkError()) {
  49 + Thread.sleep(1000);
  50 + writer.write(":keep alive\n\n");
  51 + writer.flush();
  52 + }
  53 + alarmEventListener.removeSseEmitter(browserId, writer);
  54 + }
  55 +}
... ...
web_src/src/layout/UiHeader.vue
... ... @@ -37,7 +37,6 @@
37 37 </template>
38 38  
39 39 <script>
40   -
41 40 import changePasswordDialog from '../components/dialog/changePassword.vue'
42 41 import userService from '../components/service/UserService'
43 42 import {Notification} from 'element-ui';
... ... @@ -55,18 +54,19 @@ export default {
55 54 };
56 55 },
57 56 created() {
58   - console.log(4444)
59 57 console.log(JSON.stringify(userService.getUser()))
60 58 if (this.$route.path.startsWith("/channelList")) {
61 59 this.activeIndex = "/deviceList"
62   -
63 60 }
64 61 },
65 62 mounted() {
66 63 window.addEventListener('beforeunload', e => this.beforeunloadHandler(e))
67   - // window.addEventListener('unload', e => this.unloadHandler(e))
68 64 this.alarmNotify = this.getAlarmSwitchStatus() === "true";
69   - this.sseControl();
  65 +
  66 + // TODO: 此处延迟连接 sse, 避免 sse 连接时 browserId 还未生成, 后续待优化
  67 + setTimeout(() => {
  68 + this.sseControl()
  69 + }, 3000);
70 70 },
71 71 methods: {
72 72 loginout() {
... ... @@ -107,10 +107,12 @@ export default {
107 107 this.sseSource = new EventSource('/api/emit?browserId=' + this.$browserId);
108 108 this.sseSource.addEventListener('message', function (evt) {
109 109 that.$notify({
110   - title: '收到报警信息',
  110 + title: '报警信息',
111 111 dangerouslyUseHTMLString: true,
112 112 message: evt.data,
113   - type: 'warning'
  113 + type: 'warning',
  114 + position: 'bottom-right',
  115 + duration: 3000
114 116 });
115 117 console.log("收到信息:" + evt.data);
116 118 });
... ...
web_src/src/main.js
1 1 import Vue from 'vue';
2 2 import App from './App.vue';
3   -
4   -Vue.config.productionTip = false;
5   -import ElementUI from 'element-ui';
  3 +import ElementUI, {Notification} from 'element-ui';
6 4 import 'element-ui/lib/theme-chalk/index.css';
7 5 import router from './router/index.js';
8 6 import axios from 'axios';
9 7 import VueCookies from 'vue-cookies';
10   -import echarts from 'echarts';
11 8 import VCharts from 'v-charts';
12 9  
13 10 import VueClipboard from 'vue-clipboard2';
14   -import {Notification} from 'element-ui';
15 11 import Fingerprint2 from 'fingerprintjs2';
16 12 import VueClipboards from 'vue-clipboards';
17 13 import Contextmenu from "vue-contextmenujs"
18 14 import userService from "./components/service/UserService"
19 15  
  16 +Vue.config.productionTip = false;
  17 +
20 18  
21 19 // 生成唯一ID
22 20 Fingerprint2.get(function (components) {
... ... @@ -29,10 +27,9 @@ Fingerprint2.get(function (components) {
29 27 //console.log(values) //使用的浏览器信息npm
30 28 // 生成最终id
31 29 let port = window.location.port;
32   - console.log(port);
33 30 const fingerPrint = Fingerprint2.x64hash128(values.join(port), 31)
34 31 Vue.prototype.$browserId = fingerPrint;
35   - console.log("唯一标识码:" + fingerPrint);
  32 + console.log("浏览器 ID: " + fingerPrint);
36 33 });
37 34  
38 35 Vue.use(VueClipboard);
... ... @@ -75,7 +72,7 @@ axios.interceptors.request.use(
75 72 );
76 73  
77 74 Vue.prototype.$axios = axios;
78   -Vue.prototype.$cookies.config(60*30);
  75 +Vue.prototype.$cookies.config(60 * 30);
79 76  
80 77 new Vue({
81 78 router: router,
... ...