From 5330d1e1844b0e79ef853ae1f0b3e25a09bac518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E4=B8=89=E7=82=AE?= <15856818120@163.com> Date: Wed, 10 Sep 2025 16:51:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=BC=8F=E6=B4=9E=E5=92=8C?= =?UTF-8?q?=E6=B8=97=E9=80=8F=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 | 2 +- .../manager/manager/config/CspFilter.java | 236 ++++++++++++++++++ .../manager/manager/filter/TokenFilter.java | 6 +- src/main/resources/application.properties | 48 ++-- 4 files changed, 269 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/bonus/boot/manager/manager/config/CspFilter.java diff --git a/src/main/java/com/bonus/boot/manager/manager/config/BnsSecurityConfig.java b/src/main/java/com/bonus/boot/manager/manager/config/BnsSecurityConfig.java index b7d97d2..667a019 100644 --- a/src/main/java/com/bonus/boot/manager/manager/config/BnsSecurityConfig.java +++ b/src/main/java/com/bonus/boot/manager/manager/config/BnsSecurityConfig.java @@ -65,7 +65,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(tokenFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/bonus/boot/manager/manager/config/CspFilter.java b/src/main/java/com/bonus/boot/manager/manager/config/CspFilter.java new file mode 100644 index 0000000..a63cc64 --- /dev/null +++ b/src/main/java/com/bonus/boot/manager/manager/config/CspFilter.java @@ -0,0 +1,236 @@ +package com.bonus.boot.manager.manager.config; + +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.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +@Component +@Order(1) // 确保过滤器优先级 +public class CspFilter 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("${spring.profiles.active:prod}") + private String activeProfile; + + @Value("${csp.report-only:false}") + private boolean cspReportOnly; + + @Value("${csp.allow-iframe:true}") + private boolean allowIframe; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + String requestUri = httpRequest.getRequestURI(); + + // 设置所有必要的安全头 + setSecurityHeaders(httpResponse, requestUri); + + 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); + } + + private boolean isProduction() { + return "prod".equals(activeProfile) || "production".equals(activeProfile); + } + + @Override + public void destroy() { + // 清理资源 + } +} diff --git a/src/main/java/com/bonus/boot/manager/manager/filter/TokenFilter.java b/src/main/java/com/bonus/boot/manager/manager/filter/TokenFilter.java index f79a925..d4f4206 100644 --- a/src/main/java/com/bonus/boot/manager/manager/filter/TokenFilter.java +++ b/src/main/java/com/bonus/boot/manager/manager/filter/TokenFilter.java @@ -45,10 +45,10 @@ public class TokenFilter extends OncePerRequestFilter implements Filter { } } // 在这里设置 CSP 头或其他过滤逻辑 - response.setHeader( + /*response.setHeader( "Content-Security-Policy", - "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline';" - ); + "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline';font-src 'self' data:;img-src 'self' data:;" + );*/ filterChain.doFilter(request, response); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 963af00..029649c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,19 +2,19 @@ #\u6B63\u5F0F\u7AEF\u53E3 #server.port=18088 #\u672C\u5730\u7AEF\u53E3 -server.port=18088 -#\u6D4B\u8BD5\u7AEF\u53E3 #server.port=18088 +#\u6D4B\u8BD5\u7AEF\u53E3 +server.port=18088 #\u8BBF\u95EE\u8DEF\u5F84 server.servlet.context-path=/YSpeaManager #\u6B63\u5F0F\u5E93 -spring.datasource.url=jdbc:mysql://192.168.1.8:23342/yn_tj_appoint?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true -spring.datasource.username=root -spring.datasource.password=Bonus@yntj123! -#\u6D4B\u8BD5\u5E93 -#spring.datasource.url=jdbc:mysql://192.168.0.14:1115/yn_tj_appoint?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +#spring.datasource.url=jdbc:mysql://192.168.1.8:23342/yn_tj_appoint?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true #spring.datasource.username=root -#spring.datasource.password=xbzadmin@szedu14! +#spring.datasource.password=Bonus@yntj123! +#\u6D4B\u8BD5\u5E93 +spring.datasource.url=jdbc:mysql://192.168.0.14:1115/yn_tj_appoint?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +spring.datasource.username=root +spring.datasource.password=xbzadmin@szedu14! #\u672C\u5730\u5E93 #spring.datasource.url=jdbc:mysql://127.0.0.1:3306/yn_tj_appoint?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai #spring.datasource.username=root @@ -32,13 +32,13 @@ mybatis.mapper-locations=classpath:mappers/*/*Mapper.xml mybatis.type-aliases-package=com.bonus.boot.manager.*.entity #\u7EBF\u4E0A -spring.redis.host=192.168.1.8 -spring.redis.port=23347 -spring.redis.password=Bonus@yntj123! +#spring.redis.host=192.168.1.8 +#spring.redis.port=23347 +#spring.redis.password=Bonus@yntj123! #\u6D4B\u8BD5 -#spring.redis.host=192.168.0.14 -#spring.redis.port=2001 -#spring.redis.password=Dszbns@Redis123! +spring.redis.host=192.168.0.14 +spring.redis.port=2001 +spring.redis.password=Dszbns@Redis123! #\u672C\u5730 #spring.redis.host=127.0.0.1 #spring.redis.port=6379 @@ -65,18 +65,18 @@ token.expire.seconds=7200 spring.servlet.multipart.enabled=true #\u6B63\u5F0F -files.url=http://112.29.103.165:1616/medicalDocumentation/statics -files.path=/data/yn -#\u6D4B\u8BD5 -#files.url=http://192.168.0.14:18077/medicalDocumentation/statics +#files.url=http://112.29.103.165:1616/medicalDocumentation/statics #files.path=/data/yn +#\u6D4B\u8BD5 +files.url=http://192.168.0.14:18088/medicalDocumentation/statics +files.path=/data/yn #\u672C\u5730 #files.url=http://192.168.0.110:18088/YSpeaManager/statics #files.path=d:\\data\\yn #files.upload=d:\\files # \u8DE8\u57DF\u914D\u7F6E # \u5141\u8BB8\u7684\u6E90\uFF08\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF09 -cors.allowed-origins=http://localhost:1616,http://127.0.0.1:1616,http://192.168.0.39:1616,http://192.168.0.14:1616,http://112.29.103.165:1616 +cors.allowed-origins=http://localhost:18088,http://127.0.0.1:18088,http://192.168.0.39:1616,http://192.168.0.14:18088,http://112.29.103.165:1616 # \u5141\u8BB8\u7684HTTP\u65B9\u6CD5 cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS @@ -101,3 +101,13 @@ security.hsts.enabled=true security.headers.clear-server-info=true management.endpoint.caches.enabled=false + +# CSP\u548C\u5B89\u5168\u5934\u914D\u7F6E +# \u662F\u5426\u542F\u7528CSP\u62A5\u544A\u6A21\u5F0F\uFF08true\u4E3A\u4EC5\u62A5\u544A\uFF0Cfalse\u4E3A\u5F3A\u5236\u6267\u884C\uFF09 +csp.report-only=false + +# \u662F\u5426\u5141\u8BB8\u9875\u9762\u5728iframe\u4E2D\u663E\u793A\uFF08true\u4E3A\u5141\u8BB8\u540C\u6E90iframe\uFF0Cfalse\u4E3A\u5B8C\u5168\u7981\u6B62\uFF09 +csp.allow-iframe=true + +# \u662F\u5426\u542F\u7528WebGL\u652F\u6301\uFF08true\u4E3A\u542F\u7528\uFF0Cfalse\u4E3A\u7981\u7528\uFF09 +csp.enable-webgl=true