漏洞修复

This commit is contained in:
liang.chao 2025-08-27 14:40:40 +08:00
parent 942ff20ef7
commit d0ac01ec86
6 changed files with 411 additions and 22 deletions

View File

@ -16,6 +16,7 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfigurationSource;
/** /**
* spring security配置 * spring security配置
@ -36,6 +37,9 @@ public class BnsSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService; private UserDetailsService userDetailsService;
@Autowired @Autowired
private TokenFilter tokenFilter; private TokenFilter tokenFilter;
@Autowired
private CorsConfigurationSource corsConfigurationSource;
@Bean @Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() { public BCryptPasswordEncoder bCryptPasswordEncoder() {
@ -48,7 +52,8 @@ public class BnsSecurityConfig extends WebSecurityConfigurerAdapter {
// 基于token所以不需要session // 基于token所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 使用新的跨域配置
http.cors(cors -> cors.configurationSource(corsConfigurationSource));
http.authorizeRequests() http.authorizeRequests()
.antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**", .antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**",
"/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**", "/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**",
@ -59,7 +64,7 @@ public class BnsSecurityConfig extends WebSecurityConfigurerAdapter {
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 解决不允许显示在iframe的问题 // 解决不允许显示在iframe的问题
http.headers().frameOptions().disable(); // http.headers().frameOptions().disable();
http.headers().cacheControl(); http.headers().cacheControl();
http.addFilterBefore(new DecryptionFilter(),UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(new DecryptionFilter(),UsernamePasswordAuthenticationFilter.class);

View File

@ -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<String> 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(",");
}
}

View File

@ -5,32 +5,54 @@ package com.bonus.gs.sub.evaluate.manager.filter;
* @Date2025/5/17 - 11:26 * @Date2025/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.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; 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 { 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<String> 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<String> 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 @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response; 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") if (httpRequest.getRequestURI().endsWith("/login")
&& "POST".equalsIgnoreCase(httpRequest.getMethod())) { && "POST".equalsIgnoreCase(httpRequest.getMethod())) {
// 处理登录请求 // 处理登录请求
@ -38,8 +60,176 @@ public class DecryptionFilter implements Filter {
chain.doFilter(wrappedRequest, response); chain.doFilter(wrappedRequest, response);
return; return;
} }
// 设置所有必要的安全头
setSecurityHeaders(httpResponse, httpRequest.getRequestURI());
chain.doFilter(request, response); 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地图页面 - 需要更宽松的策略支持WebGLWorker等
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() {
// 清理资源
}
} }

View File

@ -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() {
// 清理资源
}
}

View File

@ -1,12 +1,12 @@
server.port=1803 server.port=1803
server.servlet.context-path=/GsSubEvaluate 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.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.username=root 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.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.max-idle=10 spring.datasource.max-idle=10
spring.datasource.max-wait=60000 spring.datasource.max-wait=60000
@ -41,6 +41,41 @@ spring.http.multipart.maxFileSize=10Mb
spring.http.multipart.maxRequestSize=10Mb spring.http.multipart.maxRequestSize=10Mb
token.expire.seconds=7200 token.expire.seconds=7200
spring.servlet.multipart.enabled=true spring.servlet.multipart.enabled=true
# CSP和安全头配置
# 是否启用CSP报告模式true为仅报告false为强制执行
csp.report-only=false
# 是否允许页面在iframe中显示true为允许同源iframefalse为完全禁止
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
# 是否启用HSTSHTTP严格传输安全
security.hsts.enabled=true
# 是否清除服务器信息头
security.headers.clear-server-info=true
#upload.dir=/data/upload #upload.dir=/data/upload
upload.dir=E:/upload upload.dir=E:/upload

View File

@ -586,7 +586,6 @@
type: 'auditAll', type: 'auditAll',
}, },
success: function (res) { success: function (res) {
debugger
if (res.res == '1') { if (res.res == '1') {
isHide =false; isHide =false;
}else{ }else{