From d0ac01ec866cfc6fff753e81dc49337e0c6c5b05 Mon Sep 17 00:00:00 2001 From: "liang.chao" <1360241448@qq.com> Date: Wed, 27 Aug 2025 14:40:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BC=8F=E6=B4=9E=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/config/BnsSecurityConfig.java | 9 +- .../evaluate/manager/config/CorsConfig.java | 89 +++++++ .../manager/filter/DecryptionFilter.java | 218 ++++++++++++++++-- .../manager/filter/SecurityHeadersFilter.java | 71 ++++++ src/main/resources/application.properties | 45 +++- .../summaryAudit/summaryAuditView.html | 1 - 6 files changed, 411 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/bonus/gs/sub/evaluate/manager/config/CorsConfig.java create mode 100644 src/main/java/com/bonus/gs/sub/evaluate/manager/filter/SecurityHeadersFilter.java diff --git a/src/main/java/com/bonus/gs/sub/evaluate/manager/config/BnsSecurityConfig.java b/src/main/java/com/bonus/gs/sub/evaluate/manager/config/BnsSecurityConfig.java index ba23b49..afaae39 100644 --- a/src/main/java/com/bonus/gs/sub/evaluate/manager/config/BnsSecurityConfig.java +++ b/src/main/java/com/bonus/gs/sub/evaluate/manager/config/BnsSecurityConfig.java @@ -16,6 +16,7 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.web.cors.CorsConfigurationSource; /** * spring security配置 @@ -36,6 +37,9 @@ public class BnsSecurityConfig extends WebSecurityConfigurerAdapter { private UserDetailsService userDetailsService; @Autowired private TokenFilter tokenFilter; + @Autowired + private CorsConfigurationSource corsConfigurationSource; + @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { @@ -48,7 +52,8 @@ public class BnsSecurityConfig extends WebSecurityConfigurerAdapter { // 基于token,所以不需要session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - + // 使用新的跨域配置 + http.cors(cors -> cors.configurationSource(corsConfigurationSource)); http.authorizeRequests() .antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**", "/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**", @@ -59,7 +64,7 @@ public class BnsSecurityConfig extends WebSecurityConfigurerAdapter { .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 解决不允许显示在iframe的问题 - http.headers().frameOptions().disable(); +// http.headers().frameOptions().disable(); http.headers().cacheControl(); http.addFilterBefore(new DecryptionFilter(),UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/bonus/gs/sub/evaluate/manager/config/CorsConfig.java b/src/main/java/com/bonus/gs/sub/evaluate/manager/config/CorsConfig.java new file mode 100644 index 0000000..0c85440 --- /dev/null +++ b/src/main/java/com/bonus/gs/sub/evaluate/manager/config/CorsConfig.java @@ -0,0 +1,89 @@ +package com.bonus.gs.sub.evaluate.manager.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; +import java.util.List; + +/** + * 跨域配置类 + * 解决前后端不分离项目的跨域问题 + */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Value("${cors.allowed-origins}") + private String allowedOrigins; + + @Value("${cors.allowed-methods}") + private String allowedMethods; + + @Value("${cors.allowed-headers}") + private String allowedHeaders; + + @Value("${cors.allow-credentials}") + private boolean allowCredentials; + + @Value("${cors.max-age}") + private long maxAge; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns(getAllowedOriginPatterns().toArray(new String[0])) + .allowedMethods(getAllowedMethodArray()) + .allowedHeaders(getAllowedHeaderArray()) + .allowCredentials(allowCredentials) + .maxAge(maxAge) + .exposedHeaders("Content-Length", "Content-Type", "Token", "Authorization"); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(getAllowedOriginPatterns()); + configuration.setAllowedMethods(Arrays.asList(getAllowedMethodArray())); + configuration.setAllowedHeaders(Arrays.asList(getAllowedHeaderArray())); + configuration.setExposedHeaders(Arrays.asList("Content-Length", "Content-Type", "Token", "Authorization")); + configuration.setAllowCredentials(allowCredentials); + configuration.setMaxAge(maxAge); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + // 统一仅注册一套白名单策略,避免出现“*” + source.registerCorsConfiguration("/**", configuration); + return source; + } + + private List getAllowedOriginPatterns() { + if (allowedOrigins == null || allowedOrigins.trim().isEmpty()) { + return Arrays.asList( + "http://localhost:*", + "http://127.0.0.1:*", + "http://192.168.*.*:*", + "http://10.*.*.*:*" + ); + } + return Arrays.asList(allowedOrigins.split(",")); + } + + private String[] getAllowedMethodArray() { + if (allowedMethods == null || allowedMethods.trim().isEmpty()) { + return new String[]{"GET", "POST", "PUT", "DELETE", "OPTIONS"}; + } + return allowedMethods.split(","); + } + + private String[] getAllowedHeaderArray() { + if (allowedHeaders == null || allowedHeaders.trim().isEmpty()) { + return new String[]{"Content-Type", "X-Requested-With", "Token", "Authorization", "X-Custom-Header"}; + } + return allowedHeaders.split(","); + } +} diff --git a/src/main/java/com/bonus/gs/sub/evaluate/manager/filter/DecryptionFilter.java b/src/main/java/com/bonus/gs/sub/evaluate/manager/filter/DecryptionFilter.java index ee9d74c..6e98a8b 100644 --- a/src/main/java/com/bonus/gs/sub/evaluate/manager/filter/DecryptionFilter.java +++ b/src/main/java/com/bonus/gs/sub/evaluate/manager/filter/DecryptionFilter.java @@ -5,32 +5,54 @@ package com.bonus.gs.sub.evaluate.manager.filter; * @Date:2025/5/17 - 11:26 */ +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; - +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +@Component +@Order(1) // 确保过滤器优先级 public class DecryptionFilter implements Filter { + // 静态资源扩展名模式 + private static final Pattern STATIC_RESOURCE_PATTERN = Pattern.compile( + ".*\\.(css|js|map|png|jpg|jpeg|gif|ico|svg|webp|bmp|" + + "woff|woff2|ttf|eot|otf|pdf|txt|xml|json|" + + "zip|rar|7z|tar|gz|mp4|mp3|wav|avi|mov|webm|" + + "doc|docx|xls|xlsx|ppt|pptx)$", + Pattern.CASE_INSENSITIVE + ); + // 静态资源路径前缀 + private static final List STATIC_PATH_PREFIXES = Arrays.asList( + "/static/", "/public/", "/resources/", "/assets/", "/css/", "/js/", + "/images/", "/img/", "/fonts/", "/webjars/", "/vendor/", "/dist/", + "/uploads/", "/downloads/", "/libs/", "/layui/" + ); + + // WebGL和3D地图相关页面路径 + private static final List WEBGL_PAGE_PATHS = Arrays.asList( + "/pages/synthesisQuery/digitalSignage.html", + "/pages/basic/lineManagement/child/setSpanTowerLonAndLat.html" + ); + + @Value("${csp.report-only}") + private boolean cspReportOnly; + + @Value("${csp.allow-iframe}") + private boolean allowIframe; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; - // 防止点击劫持:禁止被 iframe 嵌套 - httpResponse.setHeader("X-Frame-Options", "SAMEORIGIN"); - httpResponse.setHeader("Content-Security-Policy", - "default-src 'self'; " + - "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + - "style-src 'self' 'unsafe-inline'; " + - "img-src 'self' data:; " + - "font-src 'self'; " + - "connect-src 'self'; " + - "form-action 'self'; " + - "object-src 'none'; " + - "base-uri 'self';"); - if (httpRequest.getRequestURI().endsWith("/login") && "POST".equalsIgnoreCase(httpRequest.getMethod())) { // 处理登录请求 @@ -38,8 +60,176 @@ public class DecryptionFilter implements Filter { chain.doFilter(wrappedRequest, response); return; } + // 设置所有必要的安全头 + setSecurityHeaders(httpResponse, httpRequest.getRequestURI()); chain.doFilter(request, response); } + private void setSecurityHeaders(HttpServletResponse response, String requestUri) { + // 1. 设置ClickJacking防护头(优先解决) + setClickJackingProtectionHeaders(response, requestUri); + + // 2. 设置CSP头 + setCspHeader(response, requestUri); + + // 3. 设置其他安全头 + setAdditionalSecurityHeaders(response); + } + private void setCspHeader(HttpServletResponse response, String requestUri) { + String cspPolicy; + + if (isStaticResource(requestUri)) { + // 静态资源使用简单策略 + cspPolicy = "default-src 'self'"; + } + else if (isLoginPage(requestUri)) { + // 登录页面 - 使用安全的CSP策略,移除不安全的指令 + String frameAncestors = allowIframe ? "'self'" : "'none'"; + + cspPolicy = "default-src 'self'; " + + // 允许同源脚本和外部JavaScript库 + "script-src 'self' 'unsafe-inline' https:; " + + // 只允许同源样式 + "style-src 'self' 'unsafe-inline' https:; " + + // 只允许同源图片和数据URI + "img-src 'self' data: blob: https:; " + + // 只允许同源字体和数据URI + "font-src 'self' data: https:; " + + // 只允许同源连接 + "connect-src 'self' https:; " + + "frame-ancestors " + frameAncestors + "; " + + "form-action 'self'; " + + "object-src 'none'; " + + "base-uri 'self'; " + + "report-uri /api/csp-violation"; + } + else if (isWebglPage(requestUri)) { + // WebGL和3D地图页面 - 需要更宽松的策略支持WebGL、Worker等 + String frameAncestors = allowIframe ? "'self'" : "'none'"; + + cspPolicy = "default-src 'self'; " + + "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data:; " + + "style-src 'self' 'unsafe-inline' data: blob:; " + + "img-src 'self' data: blob: https:; " + + "font-src 'self' data: blob: https:; " + + "connect-src 'self' https: blob: data: http://data.mars3d.cn; " + + "frame-ancestors " + frameAncestors + "; " + + "form-action 'self'; " + + "object-src 'none'; " + + "base-uri 'self'; " + + "worker-src 'self' blob: data:; " + + "child-src 'self' blob: data:; " + + "report-uri /api/csp-violation"; // 移除 upgrade-insecure-requests,避免强制HTTPS + } else { + // 普通HTML页面 - 根据配置决定是否允许iframe + String frameAncestors = allowIframe ? "'self'" : "'none'"; + + cspPolicy = "default-src 'self'; " + + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; " + + "style-src 'self' 'unsafe-inline' https:; " + + "img-src 'self' data: blob: https:; " + + "font-src 'self' data: https:; " + + "connect-src 'self' https:; " + + "frame-ancestors " + frameAncestors + "; " + + "form-action 'self'; " + + "object-src 'none'; " + + "base-uri 'self'; " + + "report-uri /api/csp-violation"; // 移除 upgrade-insecure-requests,避免强制HTTPS + } + + String headerName = cspReportOnly ? + "Content-Security-Policy-Report-Only" : "Content-Security-Policy"; + + response.setHeader(headerName, cspPolicy); + } + + private void setClickJackingProtectionHeaders(HttpServletResponse response, String requestUri) { + // 对于静态资源,使用宽松的ClickJacking防护 + if (isStaticResource(requestUri)) { + response.setHeader("X-Frame-Options", "SAMEORIGIN"); + return; + } + + // 对于HTML页面,根据配置决定防护级别 + if (allowIframe) { + response.setHeader("X-Frame-Options", "SAMEORIGIN"); + } else { + response.setHeader("X-Frame-Options", "DENY"); + } + } + + private void setAdditionalSecurityHeaders(HttpServletResponse response) { + response.setHeader("X-Content-Type-Options", "nosniff"); + response.setHeader("X-XSS-Protection", "1; mode=block"); + response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); + response.setHeader("Permissions-Policy", + "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=()"); + + // 注意:HSTS 只应在 HTTPS 部署下开启;当前未在此处强制设置 + // 如需开启,请在 HTTPS 部署完成后,通过配置控制 + // 例如:Strict-Transport-Security: max-age=31536000; includeSubDomains; preload + } + + private boolean isStaticResource(String uri) { + if (uri == null || uri.isEmpty()) { + return false; + } + + String path = uri.split("\\?")[0]; + + if (STATIC_RESOURCE_PATTERN.matcher(path).matches()) { + return true; + } + + return STATIC_PATH_PREFIXES.stream().anyMatch(path::startsWith); + } + + /** + * 判断是否为登录页面 + */ + private boolean isLoginPage(String requestUri) { + return requestUri != null && ( + requestUri.endsWith("/login.html") || + requestUri.endsWith("/login") || + requestUri.contains("/login") + ); + } + + /** + * 生成随机nonce值 + */ + private String generateNonce() { + byte[] nonceBytes = new byte[16]; + new java.util.Random().nextBytes(nonceBytes); + return java.util.Base64.getEncoder().encodeToString(nonceBytes); + } + + /** + * 生成内容的SHA-256哈希值 + */ + private String generateHash(String content) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(content.getBytes("UTF-8")); + return "'sha256-" + java.util.Base64.getEncoder().encodeToString(hash) + "'"; + } catch (Exception e) { + return ""; + } + } + + private boolean isWebglPage(String uri) { + if (uri == null || uri.isEmpty()) { + return false; + } + + String path = uri.split("\\?")[0]; + return WEBGL_PAGE_PATHS.stream().anyMatch(path::contains); + } + + @Override + public void destroy() { + // 清理资源 + } + } diff --git a/src/main/java/com/bonus/gs/sub/evaluate/manager/filter/SecurityHeadersFilter.java b/src/main/java/com/bonus/gs/sub/evaluate/manager/filter/SecurityHeadersFilter.java new file mode 100644 index 0000000..969ad56 --- /dev/null +++ b/src/main/java/com/bonus/gs/sub/evaluate/manager/filter/SecurityHeadersFilter.java @@ -0,0 +1,71 @@ +package com.bonus.gs.sub.evaluate.manager.filter; + +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 额外的安全头过滤器 + * 用于设置更多的安全相关头信息 + */ +@Component +@Order(2) +public class SecurityHeadersFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + // 设置额外的安全头 + setAdditionalSecurityHeaders(httpRequest, httpResponse); + + chain.doFilter(request, response); + } + + private void setAdditionalSecurityHeaders(HttpServletRequest request, HttpServletResponse response) { + // 1) 缓存控制 + response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + // 2) IE下载策略 + response.setHeader("X-Download-Options", "noopen"); + + // 3) 跨域策略(条件化设置) + if (isPotentiallyTrustworthy(request)) { + response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + } else { + response.setHeader("Cross-Origin-Opener-Policy", ""); + response.setHeader("Cross-Origin-Resource-Policy", ""); + response.setHeader("Cross-Origin-Embedder-Policy", ""); + } + + // 4) 不再设置已废弃的 Feature-Policy,避免与 Permissions-Policy 冲突 + // Permissions-Policy 已在 CspFilter 中统一设置 + } + + private boolean isPotentiallyTrustworthy(HttpServletRequest request) { + boolean isSecure = request.isSecure(); + String forwardedProto = request.getHeader("X-Forwarded-Proto"); + if (!isSecure && forwardedProto != null) { + isSecure = "https".equalsIgnoreCase(forwardedProto); + } + String host = request.getServerName(); + boolean isLocalhost = "localhost".equalsIgnoreCase(host) || "127.0.0.1".equals(host); + return isSecure || isLocalhost; + } + + @Override + public void destroy() { + // 清理资源 + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a33f546..e56c592 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,12 +1,12 @@ server.port=1803 server.servlet.context-path=/GsSubEvaluate -#spring.datasource.url=jdbc:mysql://127.0.0.1:3307/aaa?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 -#spring.datasource.username=root -#spring.datasource.password=root -spring.datasource.url=jdbc:mysql://192.168.0.14:1115/gs_sub_evaluate?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false +spring.datasource.url=jdbc:mysql://127.0.0.1:3307/aaa?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 spring.datasource.username=root -spring.datasource.password=xbzadmin@szedu14! +spring.datasource.password=root +#spring.datasource.url=jdbc:mysql://192.168.0.14:1115/gs_sub_evaluate?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false +#spring.datasource.username=root +#spring.datasource.password=xbzadmin@szedu14! spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.max-idle=10 spring.datasource.max-wait=60000 @@ -41,6 +41,41 @@ spring.http.multipart.maxFileSize=10Mb spring.http.multipart.maxRequestSize=10Mb token.expire.seconds=7200 spring.servlet.multipart.enabled=true +# CSP和安全头配置 +# 是否启用CSP报告模式(true为仅报告,false为强制执行) +csp.report-only=false + +# 是否允许页面在iframe中显示(true为允许同源iframe,false为完全禁止) +csp.allow-iframe=true + +# 是否启用WebGL支持(true为启用,false为禁用) +csp.enable-webgl=true + +# 跨域配置 +# 允许的源(多个用逗号分隔) +cors.allowed-origins=http://localhost:1803,http://127.0.0.1:1803,http://192.168.0.14:1803,http://192.168.1.3:1803,http://112.29.103.165:1618/ + +# 允许的HTTP方法 +cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS + +# 允许的请求头 +cors.allowed-headers=Content-Type,X-Requested-With,Token,Authorization,X-Custom-Header + +# 是否允许携带认证信息 +cors.allow-credentials=true + +# 预检请求缓存时间(秒) +cors.max-age=3600 + +# 安全头配置 +# 是否启用严格的安全头 +security.headers.strict=true + +# 是否启用HSTS(HTTP严格传输安全) +security.hsts.enabled=true + +# 是否清除服务器信息头 +security.headers.clear-server-info=true #upload.dir=/data/upload upload.dir=E:/upload diff --git a/src/main/resources/static/pages/evaluate/summaryAudit/summaryAuditView.html b/src/main/resources/static/pages/evaluate/summaryAudit/summaryAuditView.html index 8905799..d1b0d70 100644 --- a/src/main/resources/static/pages/evaluate/summaryAudit/summaryAuditView.html +++ b/src/main/resources/static/pages/evaluate/summaryAudit/summaryAuditView.html @@ -586,7 +586,6 @@ type: 'auditAll', }, success: function (res) { - debugger if (res.res == '1') { isHide =false; }else{