2025-07-22
技术分享
00
请注意,本文编写于 73 天前,最后修改于 36 天前,其中某些信息可能已经过时。

目录

基于WVP-GB28181-pro[https://github.com/648540858/wvp-GB28181-pro]

基于WVP-GB28181-pro[https://github.com/648540858/wvp-GB28181-pro]

src/main/resources/application-dev.yml

yml
spring: # 设置接口超时时间 mvc: async: request-timeout: 20000 thymeleaf: cache: false # [可选]上传文件大小限制 servlet: multipart: max-file-size: 10MB max-request-size: 100MB cache: type: redis # REDIS数据库配置 redis: # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1 host: 127.0.0.1 # [必须修改] 端口号 port: 6379 # [可选] 数据库 DB database: 7 # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 password: dev_pwd_123 # [可选] 超时时间 timeout: 10000 # mysql数据源 datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 server: port: 18080 # [可选] HTTPS配置, 默认不开启 ssl: # [可选] 是否开启HTTPS访问 enabled: false # [可选] 证书文件路径,放置在resource/目录下即可,修改xxx为文件名 key-store: classpath:test.monitor.89iot.cn.jks # [可选] 证书密码 key-store-password: gpf64qmw # [可选] 证书类型, 默认为jks,根据实际修改 key-store-type: JKS # 作为28181服务器的配置 sip: # [可选] 28181服务监听的端口 port: 8116 # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) # 后两位为行业编码,定义参照附录D.3 # 3701020049标识山东济南历下区 信息行业接入 # [可选] domain: 6101120042 # [可选] id: 61011200422000000001 # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验 password: 12345678 # 是否存储alarm信息 alarm: false #zlm 默认服务器配置 media: id: zlmediakit # [必须修改] zlm服务器的内网IP ip: 192.168.1.253 # [必须修改] zlm服务器的http.port http-port: 8611 # [必选选] zlm服务器的hook.admin_params=secret secret: ViLEk9N0Gl9kQuWNm8whHI2YBJaBuqe7 # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 rtp: # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 enable: true # [可选] 在此范围内选择端口用于媒体流传输, 必须提前在zlm上配置该属性,不然自动配置此属性可能不成功 port-range: 40000,45000 # 端口范围 # [可选] 国标级联在此范围内选择端口发送媒体流, send-port-range: 50000,55000 # 端口范围 # [根据业务需求配置] user-settings: # 点播/录像回放 等待超时时间,单位:毫秒 play-timeout: 180000 # [可选] 自动点播, 使用固定流地址进行播放时,如果未点播则自动进行点播, 需要rtp.enable=true auto-apply-play: true # 推流直播是否录制 record-push-live: true # 国标是否录制 record-sip: true # 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放 stream-on-demand: true # 是否返回Date属性,true:不返回,避免摄像头通过该参数自动校时,false:返回,摄像头可能会根据该时间校时 disable-date-header: false # 接口鉴权 interface-authentication: false

src/main/resources/application-prod.yml

yml
spring: # 设置接口超时时间 mvc: async: request-timeout: 20000 thymeleaf: cache: false # [可选]上传文件大小限制 servlet: multipart: max-file-size: 10MB max-request-size: 100MB cache: type: redis # REDIS数据库配置 redis: # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1 host: 10.35.11.38 # [必须修改] 端口号 port: 6379 # [可选] 数据库 DB database: 7 # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 password: # [可选] 超时时间 timeout: 10000 # mysql数据源 datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://10.35.11.38:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: root #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 server: port: 18080 # [可选] HTTPS配置, 默认不开启 ssl: # [可选] 是否开启HTTPS访问 enabled: false # [可选] 证书文件路径,放置在resource/目录下即可,修改xxx为文件名 key-store: classpath:test.monitor.89iot.cn.jks # [可选] 证书密码 key-store-password: gpf64qmw # [可选] 证书类型, 默认为jks,根据实际修改 key-store-type: JKS # 作为28181服务器的配置 sip: # [可选] 28181服务监听的端口 port: 8116 # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) # 后两位为行业编码,定义参照附录D.3 # 3701020049标识山东济南历下区 信息行业接入 # [可选] domain: 6101120042 # [可选] id: 61011200422000000001 # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验 password: 12345678 # 是否存储alarm信息 alarm: false #zlm 默认服务器配置 media: id: zlmediakit # [必须修改] zlm服务器的内网IP ip: 10.35.11.34 # [必须修改] zlm服务器的http.port http-port: 8611 # [必选选] zlm服务器的hook.admin_params=secret secret: ViLEk9N0Gl9kQuWNm8whHI2YBJaBuqe7 # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 rtp: # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 enable: true # [可选] 在此范围内选择端口用于媒体流传输, 必须提前在zlm上配置该属性,不然自动配置此属性可能不成功 port-range: 40000,45000 # 端口范围 # [可选] 国标级联在此范围内选择端口发送媒体流, send-port-range: 50000,55000 # 端口范围 # [根据业务需求配置] user-settings: # 点播/录像回放 等待超时时间,单位:毫秒 play-timeout: 180000 # [可选] 自动点播, 使用固定流地址进行播放时,如果未点播则自动进行点播, 需要rtp.enable=true auto-apply-play: true # 推流直播是否录制 record-push-live: true # 国标是否录制 record-sip: true # 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放 stream-on-demand: true # 接口鉴权 interface-authentication: false

web/src/router/index.js

javascript
import Vue from "vue"; import Router from "vue-router"; Vue.use(Router); /* Layout */ import Layout from "@/layout"; /** * Note: sub-menu only appear when route children.length >= 1 * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html * * hidden: true if set true, item will not show in the sidebar(default is false) * alwaysShow: true if set true, will always show the root menu * if not set alwaysShow, when item has more than one children route, * it will becomes nested mode, otherwise not show the root menu * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb * name:'router-name' the name is used by <keep-alive> (must set!!!) * meta : { roles: ['admin','editor'] control the page roles (you can set multiple roles) title: 'title' the name show in sidebar and breadcrumb (recommend set) icon: 'svg-name'/'el-icon-x' the icon show in the sidebar breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) activeMenu: '/example/list' if set path, the sidebar will highlight the path you set } */ /** * constantRoutes * a base page that does not have permission requirements * all roles can be accessed */ export const constantRoutes = [ { path: "/login", component: () => import("@/views/login/index"), hidden: true, }, { path: "/404", component: () => import("@/views/404"), hidden: true, }, { path: "/", component: Layout, redirect: "/dashboard", children: [ { path: "dashboard", // 路由路径 name: "控制台", // 路由名称 component: () => import("@/views/dashboard/index"), // 组件路径 meta: { title: "控制台", icon: "dashboard", affix: true }, props: true, // 启用参数传递 }, ], }, { path: "/live", component: Layout, redirect: "/live", children: [ { path: "", name: "Live", component: () => import("@/views/live/index"), meta: { title: "分屏监控", icon: "live" }, props: true, // 启用参数传递 }, ], }, { path: "/device", component: Layout, redirect: "/device", onlyIndex: 0, children: [ { path: "", name: "Device", component: () => import("@/views/device/index"), meta: { title: "国标设备", icon: "device" }, props: true, // 启用参数传递 }, { path: "/device/record/:deviceId/:channelDeviceId", name: "DeviceRecord", component: () => import("@/views/device/channel/record"), meta: { title: "国标录像" }, props: true, // 启用参数传递 }, ], }, { path: "/push", component: Layout, redirect: "/push", children: [ { path: "", name: "PushList", component: () => import("@/views/streamPush/index"), meta: { title: "推流列表", icon: "streamPush" }, }, ], }, { path: "/proxy", component: Layout, redirect: "/proxy", children: [ { path: "", name: "Proxy", component: () => import("@/views/streamProxy/index"), meta: { title: "拉流代理", icon: "streamProxy" }, }, ], }, { path: "/commonChannel", component: Layout, redirect: "/commonChannel/region", name: "通道管理", meta: { title: "通道管理", icon: "channelManger" }, children: [ { path: "region", name: "Region", component: () => import("@/views/channel/region/index"), meta: { title: "行政区划", icon: "region" }, props: true, // 启用参数传递 }, { path: "group", name: "Group", component: () => import("@/views/channel/group/index"), meta: { title: "业务分组", icon: "tree" }, props: true, // 启用参数传递 }, ], }, { path: "/recordPlan", component: Layout, redirect: "/recordPlan", children: [ { path: "", name: "RecordPlan", component: () => import("@/views/recordPlan/index"), meta: { title: "录制计划", icon: "recordPlan" }, }, ], }, { path: "/cloudRecord", component: Layout, redirect: "/cloudRecord", onlyIndex: 0, children: [ { path: "/cloudRecord", name: "CloudRecord", component: () => import("@/views/cloudRecord/index"), meta: { title: "云端录像", icon: "cloudRecord" }, }, { path: "/cloudRecord/detail/:app/:stream", name: "CloudRecordDetail", component: () => import("@/views/cloudRecord/detail"), meta: { title: "云端录像详情" }, }, ], }, { path: "/mediaServer", component: Layout, redirect: "/mediaServer", children: [ { path: "", name: "MediaServer", component: () => import("@/views/mediaServer/index"), meta: { title: "媒体节点", icon: "mediaServerList" }, }, ], }, { path: "/platform", component: Layout, redirect: "/platform", children: [ { path: "", name: "Platform", component: () => import("@/views/platform/index"), meta: { title: "国标级联", icon: "platform" }, }, ], }, { path: "/user", component: Layout, redirect: "/user", children: [ { path: "", name: "User", component: () => import("@/views/user/index"), meta: { title: "用户管理", icon: "user" }, }, ], }, // { // path: '/setting', // component: Layout, // redirect: '/setting', // children: [ // { // path: '', // name: '系统设置', // component: () => import('@/views/platform/index'), // meta: { title: '系统设置', icon: 'setting' } // } // ] // }, { path: "/operations", component: Layout, meta: { title: "运维中心", icon: "operations" }, redirect: "/operations/systemInfo", children: [ { path: "/operations/systemInfo", name: "OperationsSystemInfo", component: () => import("@/views/operations/systemInfo"), meta: { title: "平台信息", icon: "systemInfo" }, }, { path: "/operations/historyLog", name: "OperationsHistoryLog", component: () => import("@/views/operations/historyLog"), meta: { title: "历史日志", icon: "historyLog" }, }, { path: "/operations/realLog", name: "OperationsRealLog", component: () => import("@/views/operations/realLog"), meta: { title: "实时日志", icon: "realLog" }, }, ], }, { path: "/play/wasm/:url", name: "wasmPlayer", hidden: true, component: () => import("@/views/common/jessibuca.vue"), }, { path: "/play/rtc/:url", name: "rtcPlayer", component: () => import("@/views/common/rtcPlayer.vue"), }, // 404 page must be placed at the end !!! { path: "*", redirect: "/404", hidden: true }, ]; const createRouter = () => new Router({ // mode: 'history', // require service support scrollBehavior: () => ({ y: 0 }), routes: constantRoutes, }); const router = createRouter(); // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 export function resetRouter() { const newRouter = createRouter(); router.matcher = newRouter.matcher; // reset router } export default router;

web/src/layout/index.vue

html
<template> <div :class="[classObj, {'sidebar-hidden': isSidebarHidden}]" class="app-wrapper"> <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> <sidebar class="sidebar-container" v-if="!isSidebarHidden"/> <div class="main-container"> <div :class="{'fixed-header':fixedHeader}" v-if="!isSidebarHidden"> <navbar /> </div> <tags-View v-if="!isSidebarHidden"/> <app-main /> </div> </div> </template> <script> import { Navbar, Sidebar, AppMain, TagsView } from './components' import ResizeMixin from './mixin/ResizeHandler' export default { name: 'Layout', components: { Navbar, Sidebar, AppMain, TagsView }, mixins: [ResizeMixin], computed: { sidebar() { return this.$store.state.app.sidebar }, device() { return this.$store.state.app.device }, fixedHeader() { return this.$store.state.settings.fixedHeader }, classObj() { return { hideSidebar: !this.sidebar.opened, openSidebar: this.sidebar.opened, withoutAnimation: this.sidebar.withoutAnimation, mobile: this.device === 'mobile' } }, // 控制侧边栏隐藏的计算属性 isSidebarHidden() { //根据路由参数决定组件隐藏 return this.$route?.query?.freelogin === '1' } }, methods: { handleClickOutside() { this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) } } } </script> <style lang="scss" scoped> @import "~@/styles/mixin.scss"; @import "~@/styles/variables.scss"; .app-wrapper { @include clearfix; position: relative; height: 100%; width: 100%; &.mobile.openSidebar{ position: fixed; top: 0; } } .drawer-bg { background: #000; opacity: 0.3; width: 100%; top: 0; height: 100%; position: absolute; z-index: 999; } .fixed-header { position: fixed; top: 0; right: 0; z-index: 9; width: calc(100% - #{$sideBarWidth}); transition: width 0.28s; } .hideSidebar .fixed-header { width: calc(100% - 54px) } .mobile .fixed-header { width: 100%; } // 侧边栏隐藏时的样式调整 .app-wrapper.sidebar-hidden .main-container { margin-left: 0 !important; // 移除左侧边距 width: 100% !important; // 主内容占满全屏 } .app-wrapper.sidebar-hidden .fixed-header { width: 100% !important; // 顶部导航占满全屏 } </style>

web/src/permission.js

javascript
import router from './router' import store from './store' import NProgress from 'nprogress' // progress bar import 'nprogress/nprogress.css' // progress bar style import { getToken, getName, getServerId } from '@/utils/auth' // get token from cookie import getPageTitle from '@/utils/get-page-title' NProgress.configure({ showSpinner: false }) // NProgress Configuration //放行免登录页面路由 const whiteList = ['/login','/dashboard','/live','/device','/device/record','/commonChannel/region','/commonChannel/group'] // no redirect whitelist router.beforeEach(async(to, from, next) => { // start progress bar NProgress.start() // set page title document.title = getPageTitle(to.meta.title) // determine whether the user has logged in const hasToken = getToken() if (hasToken) { if (to.path === '/login') { // if is logged in, redirect to the home page next({ path: '/' }) NProgress.done() } else { const hasGetUserInfo = store.getters.name if (!hasGetUserInfo) { store.commit('user/SET_NAME', getName()) store.commit('user/SET_SERVER_ID', getServerId()) } next() } } else { /* has no token*/ if (whiteList.indexOf(to.path) !== -1) { // in the free login whitelist, go directly next() } else { // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { // finish progress bar NProgress.done() })

src/main/java/com/genersoft/iot/vmp/conf/security/SecurityUtils.java

java
package com.genersoft.iot.vmp.conf.security; import com.genersoft.iot.vmp.conf.UserSetting; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsUtils; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 配置Spring Security * * @author lin */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @Order(1) @Slf4j public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserSetting userSetting; @Autowired private DefaultUserDetailsServiceImpl userDetailsService; /** * 登出成功的处理 */ @Autowired private LogoutHandler logoutHandler; /** * 未登录的处理 */ @Autowired private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; /** * 配置认证方式 * * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); // 设置不隐藏 未找到用户异常 provider.setHideUserNotFoundExceptions(true); // 用户认证service - 查询数据库的逻辑 provider.setUserDetailsService(userDetailsService); // 设置密码加密算法 provider.setPasswordEncoder(passwordEncoder()); auth.authenticationProvider(provider); } @Override protected void configure(HttpSecurity http) throws Exception { List<String> defaultExcludes = new ArrayList<>(); defaultExcludes.add("/"); defaultExcludes.add("/#/**"); defaultExcludes.add("/static/**"); defaultExcludes.add("/swagger-ui.html"); defaultExcludes.add("/swagger-ui/**"); defaultExcludes.add("/swagger-resources/**"); defaultExcludes.add("/doc.html"); defaultExcludes.add("/doc.html#/**"); defaultExcludes.add("/v3/api-docs/**"); defaultExcludes.add("/index.html"); defaultExcludes.add("/webjars/**"); defaultExcludes.add("/js/**"); defaultExcludes.add("/api/device/query/snap/**"); defaultExcludes.add("/record_proxy/*/**"); defaultExcludes.add("/api/emit"); defaultExcludes.add("/favicon.ico"); defaultExcludes.add("/api/user/login"); defaultExcludes.add("/index/hook/**"); defaultExcludes.add("/api/device/query/snap/**"); defaultExcludes.add("/index/hook/abl/**"); // 添加免登录API defaultExcludes.add("/api/**"); if (userSetting.getInterfaceAuthentication() && !userSetting.getInterfaceAuthenticationExcludes().isEmpty()) { defaultExcludes.addAll(userSetting.getInterfaceAuthenticationExcludes()); } http.headers().contentTypeOptions().disable() // 添加允许iframe嵌入的设置 .frameOptions().disable() // 禁用X-Frame-Options头,允许所有iframe嵌入 .and().cors().configurationSource(configurationSource()) .and().csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 配置拦截规则 .and() .authorizeRequests() .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() .antMatchers(defaultExcludes.toArray(new String[0])).permitAll() .anyRequest().authenticated() // 异常处理器 .and() .exceptionHandling() .authenticationEntryPoint(anonymousAuthenticationEntryPoint) .and().logout().logoutUrl("/api/user/logout").permitAll() .logoutSuccessHandler(logoutHandler) ; http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } CorsConfigurationSource configurationSource() { // 配置跨域 CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedHeaders(Arrays.asList("*")); corsConfiguration.setAllowedMethods(Arrays.asList("*")); corsConfiguration.setMaxAge(3600L); if (userSetting.getAllowedOrigins() != null && !userSetting.getAllowedOrigins().isEmpty()) { corsConfiguration.setAllowCredentials(true); corsConfiguration.setAllowedOrigins(userSetting.getAllowedOrigins()); } else { // 在SpringBoot 2.4及以上版本处理跨域时,遇到错误提示:当allowCredentials为true时,allowedOrigins不能包含特殊值"*"。 // 解决方法是明确指定allowedOrigins或使用allowedOriginPatterns。 corsConfiguration.setAllowCredentials(true); corsConfiguration.addAllowedOriginPattern(CorsConfiguration.ALL); // 默认全部允许所有跨域 } corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader())); UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource(); url.registerCorsConfiguration("/**", corsConfiguration); return url; } /** * 描述: 密码加密算法 BCrypt 推荐使用 **/ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 描述: 注入AuthenticationManager管理器 **/ @Override @Bean public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } }

本文作者:周得水

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!