From ff0c25a1c9ad34dbf2a8440e329adacae2706449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E4=BA=AE?= Date: Thu, 28 Aug 2025 11:12:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../basic/controller/SysUserController.java | 1 + .../com/bonus/system/config/CorsConfig.java | 89 +++++++ .../com/bonus/system/config/CspFilter.java | 236 ++++++++++++++++++ .../mapper/att/AttDetailByMonthMapper.xml | 3 +- .../mapper/att/AttSourceDataMapper.xml | 2 +- .../main/resources/mapper/basic/SysOrgDao.xml | 8 +- .../mapper/evection/EvectionMapper.xml | 3 +- .../mapper/holiday/RequestReportMapper.xml | 4 +- .../monitor/config/WebSecurityConfigurer.java | 2 +- 9 files changed, 338 insertions(+), 10 deletions(-) create mode 100644 bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CorsConfig.java create mode 100644 bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CspFilter.java diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/basic/controller/SysUserController.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/basic/controller/SysUserController.java index ba6c0c8..58cd2da 100644 --- a/bonus-modules/bonus-system/src/main/java/com/bonus/system/basic/controller/SysUserController.java +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/basic/controller/SysUserController.java @@ -311,6 +311,7 @@ public class SysUserController extends BaseController { // 权限集合 Set permissions = sysMenuService.getMenuPermission(user); AjaxResult ajax = AjaxResult.success(); + user.setPassword(null); ajax.put("user", user); ajax.put("roles", roles); ajax.put("permissions", permissions); diff --git a/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CorsConfig.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CorsConfig.java new file mode 100644 index 0000000..6819663 --- /dev/null +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CorsConfig.java @@ -0,0 +1,89 @@ +package com.bonus.system.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.1.*.*:*" + ); + } + 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/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CspFilter.java b/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CspFilter.java new file mode 100644 index 0000000..82fe8b5 --- /dev/null +++ b/bonus-modules/bonus-system/src/main/java/com/bonus/system/config/CspFilter.java @@ -0,0 +1,236 @@ +package com.bonus.system.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() { + // 清理资源 + } +} \ No newline at end of file diff --git a/bonus-modules/bonus-system/src/main/resources/mapper/att/AttDetailByMonthMapper.xml b/bonus-modules/bonus-system/src/main/resources/mapper/att/AttDetailByMonthMapper.xml index c0b7e1c..44c34c8 100644 --- a/bonus-modules/bonus-system/src/main/resources/mapper/att/AttDetailByMonthMapper.xml +++ b/bonus-modules/bonus-system/src/main/resources/mapper/att/AttDetailByMonthMapper.xml @@ -174,7 +174,7 @@ from v_att_update_data vat left join sys_user su on vat.user_id = su.user_id left join sys_organization so on vat.org_id = so.id - where 1=1 + and vat.user_id = #{userId} @@ -225,6 +225,7 @@ and date_format(vat.att_current_day,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d') +