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 package com.genersoft.iot.vmp.conf.security; 1 package com.genersoft.iot.vmp.conf.security;
2 2
3 import com.genersoft.iot.vmp.conf.UserSetting; 3 import com.genersoft.iot.vmp.conf.UserSetting;
4 -import org.springframework.core.annotation.Order;  
5 import org.slf4j.Logger; 4 import org.slf4j.Logger;
6 import org.slf4j.LoggerFactory; 5 import org.slf4j.LoggerFactory;
7 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.beans.factory.annotation.Autowired;
8 import org.springframework.context.annotation.Bean; 7 import org.springframework.context.annotation.Bean;
9 import org.springframework.context.annotation.Configuration; 8 import org.springframework.context.annotation.Configuration;
  9 +import org.springframework.core.annotation.Order;
10 import org.springframework.security.authentication.AuthenticationManager; 10 import org.springframework.security.authentication.AuthenticationManager;
11 import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 11 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
12 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 12 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@@ -28,6 +28,7 @@ import java.util.Arrays; @@ -28,6 +28,7 @@ import java.util.Arrays;
28 28
29 /** 29 /**
30 * 配置Spring Security 30 * 配置Spring Security
  31 + *
31 * @author lin 32 * @author lin
32 */ 33 */
33 @Configuration 34 @Configuration
@@ -75,6 +76,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -75,6 +76,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
75 matchers.add("/js/**"); 76 matchers.add("/js/**");
76 matchers.add("/api/device/query/snap/**"); 77 matchers.add("/api/device/query/snap/**");
77 matchers.add("/record_proxy/*/**"); 78 matchers.add("/record_proxy/*/**");
  79 + matchers.add("/api/emit");
78 matchers.addAll(userSetting.getInterfaceAuthenticationExcludes()); 80 matchers.addAll(userSetting.getInterfaceAuthenticationExcludes());
79 // 可以直接访问的静态数据 81 // 可以直接访问的静态数据
80 web.ignoring().antMatchers(matchers.toArray(new String[0])); 82 web.ignoring().antMatchers(matchers.toArray(new String[0]));
@@ -83,6 +85,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -83,6 +85,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
83 85
84 /** 86 /**
85 * 配置认证方式 87 * 配置认证方式
  88 + *
86 * @param auth 89 * @param auth
87 * @throws Exception 90 * @throws Exception
88 */ 91 */
@@ -111,7 +114,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -111,7 +114,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
111 .authorizeRequests() 114 .authorizeRequests()
112 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() 115 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
113 .antMatchers(userSetting.getInterfaceAuthenticationExcludes().toArray(new String[0])).permitAll() 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 .anyRequest().authenticated() 118 .anyRequest().authenticated()
116 // 异常处理器 119 // 异常处理器
117 .and() 120 .and()
@@ -124,7 +127,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -124,7 +127,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
124 127
125 } 128 }
126 129
127 - CorsConfigurationSource configurationSource(){ 130 + CorsConfigurationSource configurationSource() {
128 // 配置跨域 131 // 配置跨域
129 CorsConfiguration corsConfiguration = new CorsConfiguration(); 132 CorsConfiguration corsConfiguration = new CorsConfiguration();
130 corsConfiguration.setAllowedHeaders(Arrays.asList("*")); 133 corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
@@ -135,7 +138,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -135,7 +138,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
135 corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader())); 138 corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader()));
136 139
137 UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource(); 140 UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource();
138 - url.registerCorsConfiguration("/**",corsConfiguration); 141 + url.registerCorsConfiguration("/**", corsConfiguration);
139 return url; 142 return url;
140 } 143 }
141 144
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java
1 package com.genersoft.iot.vmp.gb28181.event.alarm; 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 import org.springframework.context.ApplicationListener; 6 import org.springframework.context.ApplicationListener;
4 import org.springframework.stereotype.Component; 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 import java.util.Iterator; 10 import java.util.Iterator;
9 import java.util.Map; 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 @Component 21 @Component
21 public class AlarmEventListener implements ApplicationListener<AlarmEvent> { 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 @Override 38 @Override
32 - public void onApplicationEvent(AlarmEvent event) { 39 + public void onApplicationEvent(@NotNull AlarmEvent event) {
33 if (logger.isDebugEnabled()) { 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 try { 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 it.remove(); 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,7 +37,6 @@
37 </template> 37 </template>
38 38
39 <script> 39 <script>
40 -  
41 import changePasswordDialog from '../components/dialog/changePassword.vue' 40 import changePasswordDialog from '../components/dialog/changePassword.vue'
42 import userService from '../components/service/UserService' 41 import userService from '../components/service/UserService'
43 import {Notification} from 'element-ui'; 42 import {Notification} from 'element-ui';
@@ -55,18 +54,19 @@ export default { @@ -55,18 +54,19 @@ export default {
55 }; 54 };
56 }, 55 },
57 created() { 56 created() {
58 - console.log(4444)  
59 console.log(JSON.stringify(userService.getUser())) 57 console.log(JSON.stringify(userService.getUser()))
60 if (this.$route.path.startsWith("/channelList")) { 58 if (this.$route.path.startsWith("/channelList")) {
61 this.activeIndex = "/deviceList" 59 this.activeIndex = "/deviceList"
62 -  
63 } 60 }
64 }, 61 },
65 mounted() { 62 mounted() {
66 window.addEventListener('beforeunload', e => this.beforeunloadHandler(e)) 63 window.addEventListener('beforeunload', e => this.beforeunloadHandler(e))
67 - // window.addEventListener('unload', e => this.unloadHandler(e))  
68 this.alarmNotify = this.getAlarmSwitchStatus() === "true"; 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 methods: { 71 methods: {
72 loginout() { 72 loginout() {
@@ -107,10 +107,12 @@ export default { @@ -107,10 +107,12 @@ export default {
107 this.sseSource = new EventSource('/api/emit?browserId=' + this.$browserId); 107 this.sseSource = new EventSource('/api/emit?browserId=' + this.$browserId);
108 this.sseSource.addEventListener('message', function (evt) { 108 this.sseSource.addEventListener('message', function (evt) {
109 that.$notify({ 109 that.$notify({
110 - title: '收到报警信息', 110 + title: '报警信息',
111 dangerouslyUseHTMLString: true, 111 dangerouslyUseHTMLString: true,
112 message: evt.data, 112 message: evt.data,
113 - type: 'warning' 113 + type: 'warning',
  114 + position: 'bottom-right',
  115 + duration: 3000
114 }); 116 });
115 console.log("收到信息:" + evt.data); 117 console.log("收到信息:" + evt.data);
116 }); 118 });
web_src/src/main.js
1 import Vue from 'vue'; 1 import Vue from 'vue';
2 import App from './App.vue'; 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 import 'element-ui/lib/theme-chalk/index.css'; 4 import 'element-ui/lib/theme-chalk/index.css';
7 import router from './router/index.js'; 5 import router from './router/index.js';
8 import axios from 'axios'; 6 import axios from 'axios';
9 import VueCookies from 'vue-cookies'; 7 import VueCookies from 'vue-cookies';
10 -import echarts from 'echarts';  
11 import VCharts from 'v-charts'; 8 import VCharts from 'v-charts';
12 9
13 import VueClipboard from 'vue-clipboard2'; 10 import VueClipboard from 'vue-clipboard2';
14 -import {Notification} from 'element-ui';  
15 import Fingerprint2 from 'fingerprintjs2'; 11 import Fingerprint2 from 'fingerprintjs2';
16 import VueClipboards from 'vue-clipboards'; 12 import VueClipboards from 'vue-clipboards';
17 import Contextmenu from "vue-contextmenujs" 13 import Contextmenu from "vue-contextmenujs"
18 import userService from "./components/service/UserService" 14 import userService from "./components/service/UserService"
19 15
  16 +Vue.config.productionTip = false;
  17 +
20 18
21 // 生成唯一ID 19 // 生成唯一ID
22 Fingerprint2.get(function (components) { 20 Fingerprint2.get(function (components) {
@@ -29,10 +27,9 @@ Fingerprint2.get(function (components) { @@ -29,10 +27,9 @@ Fingerprint2.get(function (components) {
29 //console.log(values) //使用的浏览器信息npm 27 //console.log(values) //使用的浏览器信息npm
30 // 生成最终id 28 // 生成最终id
31 let port = window.location.port; 29 let port = window.location.port;
32 - console.log(port);  
33 const fingerPrint = Fingerprint2.x64hash128(values.join(port), 31) 30 const fingerPrint = Fingerprint2.x64hash128(values.join(port), 31)
34 Vue.prototype.$browserId = fingerPrint; 31 Vue.prototype.$browserId = fingerPrint;
35 - console.log("唯一标识码:" + fingerPrint); 32 + console.log("浏览器 ID: " + fingerPrint);
36 }); 33 });
37 34
38 Vue.use(VueClipboard); 35 Vue.use(VueClipboard);
@@ -75,7 +72,7 @@ axios.interceptors.request.use( @@ -75,7 +72,7 @@ axios.interceptors.request.use(
75 ); 72 );
76 73
77 Vue.prototype.$axios = axios; 74 Vue.prototype.$axios = axios;
78 -Vue.prototype.$cookies.config(60*30); 75 +Vue.prototype.$cookies.config(60 * 30);
79 76
80 new Vue({ 77 new Vue({
81 router: router, 78 router: router,