The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
8.7 KiB
Plaintext
159 lines
8.7 KiB
Plaintext
# 필터가 무엇을 붙이고, 그것이 되돌려질 수 있다고 무엇이 말하는가
|
|
/**
|
|
* Applies the skeleton's default HTTP cache policy: every response gets {@code Cache-Control:
|
|
* no-store} and a {@code Vary: Accept, Accept-Encoding, Authorization} header. Defaults are set
|
|
* before the chain so a cacheable endpoint can opt in by returning a {@code ResponseEntity} whose
|
|
* {@code Cache-Control} header overwrites the default. See README for the design rationale.
|
|
*/
|
|
@Component
|
|
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
|
|
public class CacheControlFilter extends OncePerRequestFilter {
|
|
|
|
static final String DEFAULT_CACHE_CONTROL = "no-store";
|
|
static final String DEFAULT_VARY = "Accept, Accept-Encoding, Authorization";
|
|
|
|
@Override
|
|
protected void doFilterInternal(
|
|
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
|
throws ServletException, IOException {
|
|
response.setHeader(ApiHeaders.CACHE_CONTROL, DEFAULT_CACHE_CONTROL);
|
|
response.setHeader(ApiHeaders.VARY, DEFAULT_VARY);
|
|
chain.doFilter(request, response);
|
|
}
|
|
|
|
# 그 정책을 이 필터가 혼자 갖도록 보안 설정이 프레임워크 기본값을 끈다
|
|
http.cors(c -> c.configurationSource(corsConfigurationSource()))
|
|
// Disable Spring Security's default Cache-Control writer; CacheControlFilter
|
|
// owns the cache header policy. See README for the design rationale.
|
|
.headers(headers -> headers.cacheControl(cache -> cache.disable()))
|
|
다만 픽스처 모듈은 그 설정을 스캔에서 뺀다 : 38: SecurityConfig.class,
|
|
39: MethodSecurityConfig.class
|
|
그 모듈이 적어 둔 제외 이유 :
|
|
// The standalone sample is a public demo app: URL access should not require a
|
|
// configured IdP or method-security principal. Production modules keep these guards.
|
|
그 모듈이 대신 두는 체인. 캐시 기록기를 끄지 않는다 :
|
|
http.securityMatcher("/**")
|
|
.csrf(csrf -> csrf.disable())
|
|
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
|
|
return http.build();
|
|
}
|
|
|
|
# 조건부 읽기를 구현한 자리 전부와 그것을 켜는 조건
|
|
main 에서 응답에 개체 태그를 쓰는 파일 (아웃바운드 S3 클라이언트는 뺐다) :
|
|
WorkLogController.java
|
|
WebSuccessResponse.java
|
|
WebResponseContract.java
|
|
MvcDownloadResponseWriter.java
|
|
ReactiveDownloadResponseWriter.java
|
|
이 파일들과 304 판정 클래스를, 시험을 빼고 부르는 자리가 있는지 :
|
|
WebSuccessResponse 를 부르는 자리 : WebResponseContract.java (끝)
|
|
WebResponseContract 를 부르는 자리 : WebSuccessResponse.java (끝)
|
|
ConditionalReadEvaluator 를 부르는 자리 : (끝)
|
|
같은 패키지의 ETags 를 실제로 호출하는 자리 : WorkLogController.java (끝)
|
|
레지스트리가 선언한 런타임 컴포지션과 이 리프의 소속 :
|
|
"runtime_compositions": [
|
|
"app-bootstrap",
|
|
"sample-portfolio"
|
|
],
|
|
"modules": [
|
|
"id": "adapter-inbound-web",
|
|
"runtime_memberships": [
|
|
"app-bootstrap",
|
|
"sample-portfolio"
|
|
]
|
|
304 판정을 구현해 둔 클래스와 그 자바독 첫 줄 :
|
|
/**
|
|
* Decides whether a {@code GET} or {@code HEAD} may be answered with a 304.
|
|
내려받기 컨트롤러를 켜는 조건과 그 스위치의 출하값 :
|
|
24:import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
41:@ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
|
|
854: enabled: ${APP_FILESERVER_PLATFORM_ENABLED:false}
|
|
.env:161:APP_FILESERVER_PLATFORM_ENABLED=false
|
|
.env.example:240:APP_FILESERVER_PLATFORM_ENABLED=false
|
|
|
|
# 내려받기 경로가 스스로 쓰는 태그와 지시자
|
|
37: response.setHeader(HttpHeaders.ETAG, descriptor.representation().strongEtag());
|
|
43: response.setHeader(HttpHeaders.CACHE_CONTROL, descriptor.cacheControl());
|
|
같은 문자열을 자바 밖에서 다시 쓰는 자리 :
|
|
.env:181:APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL=private, no-store
|
|
app-bootstrap/src/main/resources/application.yml:880: cache-control: ${APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL:private, no-store}
|
|
.env.example:255:APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL=private, no-store
|
|
DownloadPolicy.java:23: /** Design default: single Range, {@code private, no-store}, attachment only. */
|
|
DownloadPolicy.java:25: return new DownloadPolicy(RangeBudget.unbounded(), "private, no-store", false);
|
|
FileserverPlatformSettings.java:127: @DefaultValue("private, no-store") String cacheControl,
|
|
|
|
# 픽스처 모듈 쪽 조건부 읽기 경로
|
|
/** Emits an {@code ETag}; honours {@code If-None-Match} with a 304 (no body). */
|
|
@GetMapping("/worklogs/{id}")
|
|
public ResponseEntity<WorkLogResponse> getOne(
|
|
@PathVariable String id,
|
|
@RequestHeader(value = ApiHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch) {
|
|
WorkLog workLog = get.handle(new GetWorkLogQuery(toId(id)));
|
|
String etag = etagOf(workLog);
|
|
if (ETags.matches(ifNoneMatch, etag)) {
|
|
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
|
|
}
|
|
return ResponseEntity.ok().eTag(etag).body(WorkLogWebMapper.toResponse(workLog));
|
|
}
|
|
이 모듈 main 에서 Cache-Control 을 스스로 정하는 자리 : (0)
|
|
|
|
# 둘을 조정하라고 만들어 둔 패키지
|
|
28줄 WebCacheHeaders.java
|
|
72줄 WebCachePolicy.java
|
|
120줄 WebCachePolicyCatalog.java
|
|
90줄 WebVaryPolicy.java
|
|
그 타입들을 이름으로 부르는 파일과 횟수 :
|
|
2 main · WebCacheHeaders.java
|
|
5 main · WebCachePolicy.java
|
|
23 main · WebCachePolicyCatalog.java
|
|
6 main · WebVaryPolicy.java
|
|
22 test · WebCachePolicyTest.java
|
|
cache 패키지 밖에서 부르는 파일 : (0)
|
|
|
|
# 그 패키지를 그렇게 둔 근거
|
|
/**
|
|
* The published cache profiles, their directives and the {@code Vary} rule.
|
|
*
|
|
* <p>CORE: a caching decision is a set of directives, not a framework call. Keeping it free of
|
|
* Spring is what lets the same profile be applied by the servlet writer, the reactive writer and
|
|
* the OpenAPI document without three renderings of it.
|
|
*/
|
|
CACHE("cache", "cache", WebModulePurity.CORE),
|
|
|
|
# 그 근거가 말한 적용자들이 실제로 무엇을 쓰는가
|
|
main 에서 Cache-Control 값을 정하는 자리 (헤더 이름 상수 두 파일은 뺐다) :
|
|
MvcDownloadResponseWriter.java:43 response.setHeader(HttpHeaders.CACHE_CONTROL, descriptor.cacheControl());
|
|
ReactiveDownloadResponseWriter.java:36 headers.set(HttpHeaders.CACHE_CONTROL, descriptor.cacheControl());
|
|
TusController.java:138 .header(HttpHeaders.CACHE_CONTROL, "no-store");
|
|
CacheControlFilter.java:24 static final String DEFAULT_CACHE_CONTROL = "no-store";
|
|
CacheControlFilter.java:31 response.setHeader(ApiHeaders.CACHE_CONTROL, DEFAULT_CACHE_CONTROL);
|
|
뺀 두 파일 :
|
|
ApiHeaders.java
|
|
WebHeaderName.java
|
|
연산 프로파일이 이름으로 들고 있는 캐시 정책과 그것을 읽는 자리 :
|
|
36: CachePolicyName cachePolicy,
|
|
95: CachePolicyName.standard(),
|
|
110: CachePolicyName.standard(),
|
|
cachePolicy() 를 읽는 자리 : (0)
|
|
지시자 문자열을 직접 들고 있는 main 자리 :
|
|
DownloadPolicy.java:25 return new DownloadPolicy(RangeBudget.unbounded(), "private, no-store", false);
|
|
FileserverPlatformSettings.java:127 @DefaultValue("private, no-store") String cacheControl,
|
|
TusController.java:138 .header(HttpHeaders.CACHE_CONTROL, "no-store");
|
|
CacheControlFilter.java:24 static final String DEFAULT_CACHE_CONTROL = "no-store";
|
|
WebCachePolicy.java:41 if (cacheControl.contains("no-store") && freshness.isPresent()) {
|
|
WebCachePolicy.java:43 "no-store and a freshness lifetime contradict each other: one says never keep this, the"
|
|
WebCachePolicyCatalog.java:36 "sensitive", "private, no-store", WebVaryPolicy.none(), false, Optional.empty());
|
|
WebCachePolicyCatalog.java:49 "public, max-age=31536000, immutable",
|
|
WebCachePolicyCatalog.java:65 "public, max-age=0, must-revalidate",
|
|
WebCachePolicyCatalog.java:80 "private, max-age=30",
|
|
CachePolicyName.java:28 return new CachePolicyName("no-store");
|
|
GraphQlHttpGetCachePolicy.java:13 NO_STORE("no-store"),
|
|
GraphQlHttpGetCachePolicy.java:16 PRIVATE("private, max-age=0"),
|
|
GraphQlHttpGetCachePolicy.java:21 SHARED("public, max-age=60");
|
|
목록이 발행하는 프로파일 이름과 그 지시자 :
|
|
36: "sensitive", "private, no-store", WebVaryPolicy.none(), false, Optional.empty());
|
|
48: "immutable-asset",
|
|
64: "revalidated",
|
|
79: "browser-private",
|