diff --git a/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/SecurityHeaderFilter.java b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/SecurityHeaderFilter.java new file mode 100644 index 0000000..82bb570 --- /dev/null +++ b/sgzb-gateway/src/main/java/com/bonus/sgzb/gateway/filter/SecurityHeaderFilter.java @@ -0,0 +1,116 @@ +package com.bonus.sgzb.gateway.filter; + +/** + * @author 30791 + * @version 1.0 + * Create by 2025/10/22 10:41 + */ + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; + + +@Slf4j +@Component +public class SecurityHeaderFilter implements GlobalFilter, Ordered { + + // 定义需要添加安全头的页面路径 + private static final List PROTECTED_PATH_PREFIXES = Arrays.asList( + "/login", "/admin", "/PMA", "/html", "/sitemap.xml", + "/db", "/dbadmin", "/myadmin", "/mysql", "/mysqladmin", + "/phpMyAdmin", "/phpMyAdmin2" + ); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + String path = request.getPath().value(); + String method = request.getMethod().name(); + + log.debug("SecurityHeaderFilter 处理请求: {} {}", method, path); + + // 获取响应头 + HttpHeaders headers = response.getHeaders(); + + // 检查是否为受保护的路径 + boolean isProtectedPage = isProtectedPath(path); + + + // 设置 Referrer-Policy(所有请求都应用) + headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); + + if (isProtectedPage) { + // 添加点击劫持防护头 + headers.add("X-Frame-Options", "DENY"); + // 改成完整的 CSP,仍保留“禁止被嵌套” + headers.add("Content-Security-Policy", + "default-src 'self'; " + // 默认仅本域 + "script-src 'self' https://code.jquery.com; " + // 允许 jQuery CDN + "style-src 'self' 'unsafe-inline'; " + // 本域 CSS + 内联 + "img-src 'self' data:; " + // 本域图片、data URI + "frame-ancestors 'none'"); // 继续禁止被 iframe + // 添加其他推荐的安全头 +// headers.add("X-Content-Type-Options", "nosniff"); +// headers.add("X-XSS-Protection", "1; mode=block"); +// headers.add("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + + log.info("为受保护路径 {} 添加严格安全头部", path); + + // 调试日志 + if (log.isDebugEnabled()) { + log.debug("设置的严格安全头部:"); + log.debug(" X-Frame-Options: DENY"); + log.debug(" Content-Security-Policy: frame-ancestors 'none'"); + } + } else { + // 即使不是受保护路径,也添加基本安全头 + headers.add("X-Frame-Options", "SAMEORIGIN"); + // 改成完整的 CSP,仍保留“禁止被嵌套” + headers.add("Content-Security-Policy", + "default-src 'self'; " + // 默认仅本域 + "script-src 'self' https://code.jquery.com; " + // 允许 jQuery CDN + "style-src 'self' 'unsafe-inline'; " + // 本域 CSS + 内联 + "img-src 'self' data:; " + // 本域图片、data URI + "frame-ancestors 'none'"); // 继续禁止被 iframe + //headers.add("X-Content-Type-Options", "nosniff"); + //headers.add("X-XSS-Protection", "1; mode=block"); + + log.debug("为普通路径 {} 添加基本安全头部", path); + } + + // 继续执行过滤器链 + return chain.filter(exchange); + } + + @Override + public int getOrder() { + // 设置过滤器执行顺序,数字越小优先级越高 + // 设置为高位值,确保在路由之后执行,能够修改响应头 + return Ordered.LOWEST_PRECEDENCE; + } + + /** + * 检查请求路径是否为受保护路径 + */ + private boolean isProtectedPath(String requestPath) { + for (String prefix : PROTECTED_PATH_PREFIXES) { + if (requestPath.startsWith(prefix)) { + log.debug("路径 {} 匹配受保护前缀: {}", requestPath, prefix); + return true; + } + } + return false; + } +} \ No newline at end of file