Skip to content

Commit 49b90bc

Browse files
committed
Allow request-specific Swagger UI index transformation
Swagger UI index.html can be customized through SwaggerIndexTransformer, including CSP nonce injection, but cached resource handlers stored the first transformed index resource and reused it for later requests. Register index.html with the uncached Swagger UI handler patterns, matching swagger-initializer.js behavior, while retaining resource resolution caching. Constraint: CSP nonces must be regenerated per request and cannot be served from transformed resource cache Rejected: Add a new opt-in property | preserving the existing extension point is sufficient and avoids another public setting Confidence: high Scope-risk: narrow Tested: mvn -f springdoc-openapi-starter-common/pom.xml -DskipTests install Tested: mvn -f springdoc-openapi-starter-webmvc-ui/pom.xml -Dtest=SpringDocApp40Test,SpringDocApp42Test test Not-tested: Full reactor build; blocked by springdoc-openapi-tests parent version mismatch in current checkout Fixes: #3264
1 parent 7e649be commit 49b90bc

3 files changed

Lines changed: 115 additions & 2 deletions

File tree

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/Constants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ public final class Constants {
201201
*/
202202
public static final String INDEX_PAGE = "/index.html";
203203

204+
/**
205+
* The constant INDEX_PAGE_PATTERN.
206+
*/
207+
public static final String INDEX_PAGE_PATTERN = "/*index.html";
208+
204209
/**
205210
* The constant SWAGGER_UI_URL.
206211
*/

springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerConfigurer.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Arrays;
1010

1111
import static org.springdoc.core.utils.Constants.ALL_PATTERN;
12+
import static org.springdoc.core.utils.Constants.INDEX_PAGE_PATTERN;
1213
import static org.springdoc.core.utils.Constants.SWAGGER_INITIALIZER_PATTERN;
1314
import static org.springdoc.core.utils.Constants.SWAGGER_UI_PREFIX;
1415
import static org.springdoc.core.utils.Constants.SWAGGER_UI_WEBJAR_NAME;
@@ -54,6 +55,7 @@ protected AbstractSwaggerConfigurer(SwaggerUiConfigProperties swaggerUiConfigPro
5455
*/
5556
protected SwaggerResourceHandlerConfig[] getSwaggerHandlerConfigs() {
5657
String swaggerUiPattern = getUiRootPath() + SWAGGER_UI_PREFIX + ALL_PATTERN;
58+
String swaggerUiIndexPattern = combinePatterns(swaggerUiPattern, INDEX_PAGE_PATTERN);
5759
String swaggerUiInitializerPattern = combinePatterns(swaggerUiPattern, SWAGGER_INITIALIZER_PATTERN);
5860
String swaggerUiResourceLocation = WEBJARS_RESOURCE_LOCATION + SWAGGER_UI_WEBJAR_NAME + DEFAULT_PATH_SEPARATOR +
5961
swaggerUiConfigProperties.getVersion() + DEFAULT_PATH_SEPARATOR;
@@ -63,7 +65,7 @@ protected SwaggerResourceHandlerConfig[] getSwaggerHandlerConfigs() {
6365
.setPatterns(swaggerUiPattern)
6466
.setLocations(swaggerUiResourceLocation),
6567
SwaggerResourceHandlerConfig.createUncached()
66-
.setPatterns(swaggerUiInitializerPattern)
68+
.setPatterns(swaggerUiIndexPattern, swaggerUiInitializerPattern)
6769
.setLocations(swaggerUiResourceLocation)
6870
};
6971
}
@@ -77,6 +79,9 @@ protected SwaggerResourceHandlerConfig[] getSwaggerWebjarHandlerConfigs() {
7779
if (!springWebProperties.getResources().isAddMappings()) return new SwaggerResourceHandlerConfig[]{};
7880

7981
String swaggerUiWebjarPattern = combinePatterns(getWebjarsPathPattern(), SWAGGER_UI_WEBJAR_NAME_PATTERN) + ALL_PATTERN;
82+
String swaggerUiWebjarIndexPattern = combinePatterns(swaggerUiWebjarPattern, INDEX_PAGE_PATTERN);
83+
String swaggerUiWebjarVersionIndexPattern = combinePatterns(swaggerUiWebjarPattern,
84+
swaggerUiConfigProperties.getVersion() + INDEX_PAGE_PATTERN);
8085
String swaggerUiWebjarInitializerPattern = combinePatterns(swaggerUiWebjarPattern, SWAGGER_INITIALIZER_PATTERN);
8186
String swaggerUiWebjarVersionInitializerPattern = combinePatterns(swaggerUiWebjarPattern,
8287
swaggerUiConfigProperties.getVersion() + SWAGGER_INITIALIZER_PATTERN);
@@ -87,7 +92,8 @@ protected SwaggerResourceHandlerConfig[] getSwaggerWebjarHandlerConfigs() {
8792
.setPatterns(swaggerUiWebjarPattern)
8893
.setLocations(swaggerUiWebjarResourceLocation),
8994
SwaggerResourceHandlerConfig.createUncached()
90-
.setPatterns(swaggerUiWebjarInitializerPattern, swaggerUiWebjarVersionInitializerPattern)
95+
.setPatterns(swaggerUiWebjarIndexPattern, swaggerUiWebjarVersionIndexPattern,
96+
swaggerUiWebjarInitializerPattern, swaggerUiWebjarVersionInitializerPattern)
9197
.setLocations(swaggerUiWebjarResourceLocation)
9298
};
9399
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
*
3+
* * Copyright 2019-2026 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.ui.app42;
20+
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import org.junit.jupiter.api.Test;
26+
import org.springdoc.core.properties.SwaggerUiConfigProperties;
27+
import org.springdoc.core.properties.SwaggerUiOAuthProperties;
28+
import org.springdoc.core.providers.ObjectMapperProvider;
29+
import org.springdoc.webmvc.ui.SwaggerIndexPageTransformer;
30+
import org.springdoc.webmvc.ui.SwaggerIndexTransformer;
31+
import org.springdoc.webmvc.ui.SwaggerWelcomeCommon;
32+
import test.org.springdoc.ui.AbstractSpringDocTest;
33+
34+
import org.springframework.boot.autoconfigure.SpringBootApplication;
35+
import org.springframework.context.annotation.Bean;
36+
import org.springframework.core.io.Resource;
37+
import org.springframework.web.servlet.resource.ResourceTransformerChain;
38+
import org.springframework.web.servlet.resource.TransformedResource;
39+
40+
import static org.hamcrest.Matchers.containsString;
41+
import static org.hamcrest.Matchers.not;
42+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
43+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
44+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
45+
46+
/**
47+
* Tests per-request Swagger UI index transformations.
48+
*
49+
* @author limehee
50+
*/
51+
public class SpringDocApp42Test extends AbstractSpringDocTest {
52+
53+
@Test
54+
void indexPageTransformerRunsForEveryRequest() throws Exception {
55+
mockMvc.perform(get("/swagger-ui/index.html").requestAttr("cspNonce", "nonce-a"))
56+
.andExpect(status().isOk())
57+
.andExpect(content().string(containsString("nonce=\"nonce-a\"")))
58+
.andExpect(content().string(not(containsString("nonce=\"nonce-b\""))));
59+
60+
mockMvc.perform(get("/swagger-ui/index.html").requestAttr("cspNonce", "nonce-b"))
61+
.andExpect(status().isOk())
62+
.andExpect(content().string(containsString("nonce=\"nonce-b\"")))
63+
.andExpect(content().string(not(containsString("nonce=\"nonce-a\""))));
64+
}
65+
66+
@SpringBootApplication
67+
static class SpringDocTestApp {
68+
69+
@Bean
70+
SwaggerIndexTransformer swaggerIndexTransformer(SwaggerUiConfigProperties swaggerUiConfig,
71+
SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerWelcomeCommon swaggerWelcomeCommon,
72+
ObjectMapperProvider objectMapperProvider) {
73+
return new NonceSwaggerIndexTransformer(swaggerUiConfig, swaggerUiOAuthProperties, swaggerWelcomeCommon,
74+
objectMapperProvider);
75+
}
76+
77+
}
78+
79+
static class NonceSwaggerIndexTransformer extends SwaggerIndexPageTransformer {
80+
81+
NonceSwaggerIndexTransformer(SwaggerUiConfigProperties swaggerUiConfig,
82+
SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerWelcomeCommon swaggerWelcomeCommon,
83+
ObjectMapperProvider objectMapperProvider) {
84+
super(swaggerUiConfig, swaggerUiOAuthProperties, swaggerWelcomeCommon, objectMapperProvider);
85+
}
86+
87+
@Override
88+
public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
89+
throws IOException {
90+
Resource transformedResource = super.transform(request, resource, transformerChain);
91+
if (request.getRequestURI().endsWith("/index.html")) {
92+
String html = new String(transformedResource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
93+
String nonce = request.getAttribute("cspNonce").toString();
94+
html = html.replace("<script ", "<script nonce=\"" + nonce + "\" ");
95+
return new TransformedResource(resource, html.getBytes(StandardCharsets.UTF_8));
96+
}
97+
return transformedResource;
98+
}
99+
100+
}
101+
102+
}

0 commit comments

Comments
 (0)