(調査中)「レスポンス用のサニタイザと統合した統一ユーティリティ」や「Spring Filter / HandlerInterceptor で自動適用」

以下に、あなたの要件に対応する以下の2つを紹介します：

---

### ✅ ① リクエスト／レスポンス両方のサニタイザを統合した共通ユーティリティクラス

**→ `HttpHeaderSanitizerUtil` に統合**

```java
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import java.util.*;

public class HttpHeaderSanitizerUtil {

    // --- リクエスト用：削除対象ヘッダー ---
    private static final Set<String> REQUEST_SENSITIVE_HEADERS = Set.of(
            HttpHeaders.HOST,
            HttpHeaders.CONTENT_LENGTH,
            HttpHeaders.TRANSFER_ENCODING,
            HttpHeaders.CONNECTION,
            HttpHeaders.EXPECT,
            HttpHeaders.TE,
            HttpHeaders.UPGRADE,
            HttpHeaders.COOKIE,
            HttpHeaders.AUTHORIZATION // 任意許可制
    );

    // --- レスポンス用：削除対象ヘッダー ---
    private static final Set<String> RESPONSE_SENSITIVE_HEADERS = Set.of(
            HttpHeaders.TRANSFER_ENCODING,
            HttpHeaders.SERVER,
            HttpHeaders.DATE,
            HttpHeaders.CONNECTION,
            HttpHeaders.SET_COOKIE,
            HttpHeaders.SET_COOKIE2,
            HttpHeaders.WWW_AUTHENTICATE,
            HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
            HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
            HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
            HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,
            HttpHeaders.LOCATION,
            HttpHeaders.CACHE_CONTROL,
            HttpHeaders.ETAG
    );

    /** リクエスト用サニタイズ */
    public static HttpHeaders sanitizeRequestHeaders(HttpHeaders original, Set<String> allowedAdditional) {
        HttpHeaders sanitized = new HttpHeaders();

        for (Map.Entry<String, List<String>> entry : original.entrySet()) {
            String headerName = entry.getKey();
            if (!REQUESTSENSITIVE_HEADERS.contains(headerName)
                    || (allowedAdditional != null && allowedAdditional.contains(headerName))) {
                sanitized.put(headerName, entry.getValue());
            }
        }

        return sanitized;
    }

    /** レスポンス用サニタイズ */
    public static HttpHeaders sanitizeResponseHeaders(HttpHeaders original, long contentLength, MediaType contentType) {
        HttpHeaders sanitized = new HttpHeaders();

        for (String key : original.keySet()) {
            if (!RESPONSE_SENSITIVE_HEADERS.contains(key)) {
                sanitized.put(key, original.get(key));
            }
        }

        sanitized.setContentLength(contentLength);
        if (contentType != null) {
            sanitized.setContentType(contentType);
        }

        return sanitized;
    }
}
```

---

### ✅ ② Spring Filter または HandlerInterceptor による自動適用

#### ✅ `OncePerRequestFilter` による実装（レスポンスヘッダーの最終制御）

```java
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class ResponseHeaderSanitizingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        // ラップして書き込みをフック
        HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(response) {
            @Override
            public void setHeader(String name, String value) {
                if (!shouldSuppressHeader(name)) {
                    super.setHeader(name, value);
                }
            }

            @Override
            public void addHeader(String name, String value) {
                if (!shouldSuppressHeader(name)) {
                    super.addHeader(name, value);
                }
            }

            private boolean shouldSuppressHeader(String name) {
                return HttpHeaderSanitizerUtil.RESPONSE_SENSITIVE_HEADERS
                        .stream()
                        .anyMatch(h -> h.equalsIgnoreCase(name));
            }
        };

        filterChain.doFilter(request, responseWrapper);
    }
}
```

> ※このフィルターは**すべてのレスポンスヘッダーに適用**されます。特定のControllerに限定したい場合は、条件分岐で判定を追加してください。

---

### ✅ コントローラ内での利用例（両方のユーティリティを適用）

```java
@GetMapping("/proxy")
public Mono<ResponseEntity<byte[]>> proxy(@RequestHeader HttpHeaders incomingHeaders) {
    // リクエストヘッダーのクリーンアップ
    HttpHeaders sanitizedRequestHeaders = HttpHeaderSanitizerUtil.sanitizeRequestHeaders(
        incomingHeaders, Set.of(HttpHeaders.AUTHORIZATION)
    );

    return webClient.get()
            .uri("https://example.com/api")
            .headers(h -> h.putAll(sanitizedRequestHeaders))
            .exchangeToMono(res -> res.bodyToMono(byte[].class).map(body -> {
                HttpHeaders sanitizedResponseHeaders = HttpHeaderSanitizerUtil.sanitizeResponseHeaders(
                        res.headers().asHttpHeaders(),
                        body.length,
                        res.headers().contentType().orElse(null)
                );

                return new ResponseEntity<>(body, sanitizedResponseHeaders, res.statusCode());
            }));
}
```

---

### ✅ 補足

| 方法                 | 主な用途               | 特徴                       |
| ------------------ | ------------------ | ------------------------ |
| ユーティリティクラス         | 中継系処理（WebClientなど） | 明示的に使う設計がしやすい            |
| Filter             | 全てのレスポンスに対して       | グローバル適用。誤爆の危険もあるので慎重に    |
| HandlerInterceptor | 認証・トレース向き          | リクエストだけに処理を入れたい場合はこちらが安全 |

---