src/main/resources/application-dev.yml
ymlspring:
# 设置接口超时时间
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
ymlspring:
# 设置接口超时时间
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
javascriptimport 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
javascriptimport 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
javapackage 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 许可协议。转载请注明出处!