How to change basePath for Springfox Swagger 2.0 - java

I'm running a service, where Swagger UI is accessible at:
http://serviceURL/swagger-ui.html
However, it is behind a proxy, such as:
http://proxyURL/serviceName
Generated URLs by Swagger UI are looking like:
http://proxyURL/
instead of the actual URL with the serviceName as suffix.
As far as I get it, this means manipulating the basePath property. As per documentation:
A swagger API documentation can no longer describe operations on
different base paths. In 1.2 and earlier, each resource could have had
a separate basePath. In 2.0, the basePath equivalents
(schemes+host+basePath) are defined for the whole specification.
#Api(basePath) is deprecated, and it doesn't say what to use and how to use it. How to make the paths generated by Swagger appear properly?
I'm using Spring Boot, Springfox Swagger and annotations.

#Bean
public Docket newsApi(ServletContext servletContext) {
return new Docket(DocumentationType.SWAGGER_2).pathProvider(new RelativePathProvider(servletContext) {
#Override
public String getApplicationBasePath() {
return "/serviceName" + super.getApplicationBasePath();
}
}).host("proxyURL");
}

You can edit your SwaggerConfiguration like that:
Take care to replace the package (which need to be the one
containing your REST controllers), the host, and the PATH you need
#Configuration
#EnableSwagger2
public class SwaggerConfiguration implements WebMvcConfigurer {
public static final String PATH = "/serviceName";
#Bean
public Docket api() {
final var package = "com.martin.rest";
final var host = "localhost:8080";
return new Docket(DocumentationType.SWAGGER_2)
.host(host)
.select()
.apis(RequestHandlerSelectors.basePackage(package))
.paths(PathSelectors.any())
.build();
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
final var apiDocs = "/v2/api-docs";
final var configUi = "/swagger-resources/configuration/ui";
final var configSecurity = "/swagger-resources/configuration/security";
final var resources = "/swagger-resources";
registry.addRedirectViewController(PATH + apiDocs, apiDocs).setKeepQueryParams(true);
registry.addRedirectViewController(PATH + resources, resources);
registry.addRedirectViewController(PATH + configUi, configUi);
registry.addRedirectViewController(PATH + configSecurity, configSecurity);
registry.addRedirectViewController(PATH, "/");
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(PATH + "/**").addResourceLocations("classpath:/META-INF/resources/");
}
}
Another solution is by changing the spring-boot URL context-path:
Edit pour application.properties file:
server.servlet.context-path=/serviceName
Or if you have an application.yml file:
server:
servlet:
context-path: /serviceName
Warning: It will change the base path of all your web services, not only Swagger

Using spring fox 2.9.2, using solution mentioned by other users is not works.
What is not working:
Overriding getApplicationBasePath on Docket pathProvider
Adding server.servlet.context-path=/serviceName
I don't know why they are not work, but in my project that using Springboot 2.1.6.RELEASE and Spring 5.1.8.RELEASE, the two solution above is being ignored.
So, I am trying another approach: https://github.com/springfox/springfox/issues/2817#issuecomment-517753110
According to the github issue comment, I need to override Springfox json serialize class and thank god this works.
Here is the code example:
import io.swagger.models.Swagger;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import springfox.documentation.spring.web.json.JacksonModuleRegistrar;
import springfox.documentation.spring.web.json.Json;
import springfox.documentation.spring.web.json.JsonSerializer;
import java.util.Arrays;
import java.util.List;
import static io.github.jhipster.config.JHipsterConstants.SPRING_PROFILE_PRODUCTION;
#Component
#Primary
public class CustomBasePathSerialize extends JsonSerializer {
// this injection is optional, if you don't need to
// add basePath based on active profile, remove this.
private final Environment env;
public CustomBasePathSerialize(List<JacksonModuleRegistrar> modules,
Environment env) {
super(modules);
this.env = env;
}
#Override
public Json toJson(Object toSerialize) {
if (toSerialize instanceof Swagger) {
Swagger swagger = (Swagger) toSerialize;
String basePath = "/serviceName";
List<String> profiles = Arrays.asList(env.getActiveProfiles());
// OPTIONAL: you can change basePath if you have difference path
// on any Spring profile, for example prod:
if (profiles.contains(SPRING_PROFILE_PRODUCTION)) {
basePath = "/";
}
swagger.basePath(basePath);
}
return super.toJson(toSerialize);
}
}

I added the following config into my application.yaml file:
springdoc:
swagger-ui:
configUrl: /pathProvider/v3/api-docs/swagger-config
disable-swagger-default-url: true
urls[0]:
url: /pathProvider/v3/api-docs/mobile-bff
name: pathName
where pathProvider should be replaced by the PathProvider config in the old swagger 2 yaml...
and also, had do add a server into the OpenAPI object with the same PathProvider name.
#Bean fun springShopOpenAPI(): OpenAPI? { return OpenAPI().addServersItem(Server().url("/pathProvider"))

Related

#PropertySource(factory=...) breaks #SpringBootTest when loading META-INF/build-info.properties

I am trying to setup a custom #ConfigurationProperties class loaded from a HOCON syntax .conf file.
I have a Class annotated with #PropertySource(factory=TypesafePropertySourceFactory.class, value = "classpath:app.conf")
#Configuration
#ConfigurationProperties(value = "app.server")
#PropertySource(factory = TypesafePropertySourceFactory.class, value = "classpath:app.conf")
public class ServerProperties {
public int port;
}
and a simple test class:
#SpringBootTest
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SomeTest {
#Test
public void someCoolTest() {/* ... */}
// ...
}
When i run my junit test runner, i get the following error:
Caused by: com.typesafe.config.ConfigException$BadPath: path parameter: Invalid path 'spring.info.build.location:classpath:META-INF/build-info.properties': Token not allowed in path expression: ':' (you can double-quote this token if you really want it here)
at com.typesafe.config.impl.PathParser.parsePathExpression(PathParser.java:155) ~[config-1.4.0.jar:1.4.0]
at com.typesafe.config.impl.PathParser.parsePathExpression(PathParser.java:74) ~[config-1.4.0.jar:1.4.0]
at com.typesafe.config.impl.PathParser.parsePath(PathParser.java:61) ~[config-1.4.0.jar:1.4.0]
...
If i uncomment the #PropertySource line on the ServerProperties class, the tests proceed normally. It seems strange to me that my custom PropertySourceFactory gets in the way of the default .properties file resolution process.
PropertySource and Factory classes
// TypesafeConfigPropertySource.java
import com.typesafe.config.Config;
import org.springframework.core.env.PropertySource;
public class TypesafeConfigPropertySource extends PropertySource<Config> {
public TypesafeConfigPropertySource(String name, Config source) {
super(name, source);
}
#Override
public Object getProperty(String path) {
if (source.hasPath(path)) {
return source.getAnyRef(path);
}
return null;
}
}
// TypesafePropertySourceFactory.java
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import java.io.IOException;
import java.util.Objects;
public class TypesafePropertySourceFactory implements PropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
Config config = ConfigFactory.load(Objects.requireNonNull(resource.getResource().getFilename())).resolve();
String safeName = name == null ? "typeSafe" : name;
return new TypesafeConfigPropertySource(safeName, config);
}
}
Am I missing something fundamental about configuring custom property resource factories, or is this a bug?
Versions
Spring boot 2.3.4
Junit Jupiter 5.6.2
Maybe you can also solve it with the use of a ContextInitializer as suggested in the answer here:
Spring Environment backed by Typesafe Config
TL;DR
Return null if you cannot process the path in your custom impl
public class TypesafeConfigPropertySource extends PropertySource<Config> {
// ...
#Override
public Object getProperty(String path) {
try {
if (source.hasPath(path)) {
return source.getAnyRef(path);
}
} catch(ConfigException.BadPath ignore) {
}
return null;
}
// ...
}
Explanation
I am making educated guesses, but functionally this appears supported by the way the code behaves
the most likely scenario here is the resolution order will consider our custom implementation before any default implementation. The method used in our implementation will error out with any path containing a ":" and "[" as the error occurs in the check for the path's existence.
I'm simply wrapping the BadPath exception in order to catch any problem and then returning null to signify no match.

Spring boot custom resolver for class variable

I'm trying to achieve something like this:
#Controller
public SomeController {
#CustomConfig("var.a")
private String varA;
#CustomConfig("var.b")
private String varB;
#RequestMapping(value = "/", method = RequestMethod.GET)
public String get() {
return varA;
}
}
CustomConfig would be an #Interface class that accepts one value parameter. The reason why we are not using #Value is because this will not come from config file but from API (such as https://getconfig.com/get?key=var.a). So we are going to make HTTP request to inject it.
So far I've only manage to make something work if the varA and varB is inside get() method as parameter, by using below in a class that extends WebMvcConfigurerAdapter:
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
CustomConfigResolver resolver = new CustomConfigResolver();
argumentResolvers.add(resolver);
}
And inside CustomComfigResolver.resolveArgument() we would do the HTTP query, but that's not really what we wanted, we need it to be injected as class variable.
Does anyone have experience in resolving it at class variable level?
Thank you
This could work if you use #Value instead of your own custom annotation. This uses the built in environment:
#Order(Ordered.HIGHEST_PRECEDENCE)
#Configuration
public class TcpIpPropertySourceConfig implements InitializingBean {
#Autowired
private ConfigurableEnvironment env;
#Autowired
private RestTemplate rest;
public void afterPropertiesSet() {
// Call your api using Resttemplate
RemoteProperties props = //Rest Call here;
// Add your source to the environment.
MutablePropertySources sources = env.getPropertySources();
sources.addFirst(new PropertiesPropertySource("customSourceName", props)
}
}
What you are trying to achieve is difficult when you start to consider "unhappy" scenarios. Server down / not reachable. You need to account for all of that in the method above.
I would highly recommend to instead use Spring Cloud Config. Great guide on that is here: https://www.baeldung.com/spring-cloud-configuration
This provides:
- Reloading of your #Value() properties, so no custom annotation needed.
- A more stable server and great Spring integration out of the box.
Best of all, it is easy to apply Retries and Backoffs if the configuration server goes down (see https://stackoverflow.com/a/44203216/2082699). This will make sure your app doesn't just crash when the server is not available.

Context Path not considered in HATEOAS links when upgrading from Spring Boot 1.5.9 to 2.2.6

I have recently upgraded an older application, based on Spring Boot, from version 1.5.9 to 2.2.6.
Unfortunately, after upgrading, the urls generated with HATEOAS are changed. Basically the context-path is missing from the Links now.
Example:
Before: https://domain.test.com/service/api/endpoint
Now: https://domain.test.com/service/endpoint
Right now I am using the following configs in application properties:
server.servlet.context-path: /api
server.forward-headers-strategy: FRAMEWORK
spring.data.rest.basePath: /api
(With none, the host is totally different(because of the x-forwarded-host. I have also tried with native, but same behavior)
I have also created a ForwardedHeaderFilter bean.
#Bean
public ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
Is there anything I can do to bypass this issue? Am I doing something wrong ?
One alternative would be to adjust the api gateway, but this would be really complicated from a business process perspective so I would prefer a more technical approach.
Thank you !
As a temporary solution, until I have time to really take a deeper look, I have created a new Utility class, that takes care of adjusting the path:
public class LinkUtil {
private LinkUtil() {
}
#SneakyThrows
public static <T> Link linkTo(T methodOn) {
String rawPath = WebMvcLinkBuilder.linkTo(methodOn).toUri().getRawPath();
rawPath = StringUtils.remove(rawPath, "/service");
BasicLinkBuilder basicUri = BasicLinkBuilder.linkToCurrentMapping().slash("/api").slash(rawPath);
return new Link(basicUri.toString());
}
}
Where /api is the context-path.
Then I use it like this:
Link whateverLink = LinkUtil.linkTo(methodOn(WhateverClass.class).whateverMethod(null)).withRel("whatever-rel));
#LoolKovski's temporary solution relies on an existing ServletRequest because of #linkToCurrentMapping. Use the following code if you, too, need to eliminate that restriction:
public class LinkUtil {
private LinkUtil() {
}
#SneakyThrows
public static <T> Link linkTo(T methodOn) {
var originalLink = WebMvcLinkBuilder.linkTo(methodOn);
var rawPathWO = StringUtils.remove(originalLink.toUri().getRawPath(), "/service");
return originalLink.withHref("/api" + rawPathWO);
}
}
Actually, in my case the links are generated during one of the RestController beans' initialization, so my real code looks like the following code.
I don't need to cut-off some other path part before but only need to prepend a configured context path.
#RestController
public class ExampleController implements ServletContextAware {
#Override
public void setServletContext(ServletContext servletContext) {
final var executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
someRepository.getExamples().forEach((name, thing) -> {
Link withRel = linkTo(methodOn(ExampleController.class).getElement(null, name, null))
.withSelfRel();
withRel = withRel.withHref(servletContext.getContextPath() + withRel.toUri().getRawPath());
thing.add(withRel);
});
executor.shutdown();
});
}
#RequestMapping(path = "/{name}/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public HttpEntity<Example> getElement(ServletWebRequest req, #PathVariable("name") String name, Principal principal) {
[...]
}

springdoc-openapi apply default global SecurityScheme possible?

I have the following SecurityScheme definition using springdoc-openapi for java SpringBoot RESTful app:
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("bearer-jwt",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")
.in(SecurityScheme.In.HEADER).name("Authorization")))
.info(new Info().title("App API").version("snapshot"));
}
Is it possible to apply it globally to all paths, without having to go and add #SecurityRequirement annotations to #Operation annotation everywhere in the code?
If it is, how to add exclusions to unsecured paths?
Yes, you can do it in the same place calling addSecurityItem:
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("bearer-jwt",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")
.in(SecurityScheme.In.HEADER).name("Authorization")))
.info(new Info().title("App API").version("snapshot"))
.addSecurityItem(
new SecurityRequirement().addList("bearer-jwt", Arrays.asList("read", "write")));
}
Global security schema can be overridden by a different one with the #SecurityRequirements annotation. Including removing security schemas for an operation. For example, we can remove security for registration path.
#SecurityRequirements
#PostMapping("/registration")
public ResponseEntity post(#RequestBody #Valid Registration: registration) {
return registrationService.register(registration);
}
While still keeping security schemas for other APIs.
Old answer (Dec 20 '19):
Global security schema can be overridden by a different one with the #SecurityRequirements annotation. but it cannot be removed for unsecured paths. It is acctualy missing fueature in the springdoc-openapi, OpenAPI standard allows it. See disable global security for particular operation
There is a workaround though. The springdoc-openapi has a concept of an OpenApiCustomiser which can be used to intercept generated schema. Inside the customizer, an operation can be modified programmatically. To remove any inherited security, the field security needs to be set to an empty array. The logic may be based on any arbitrary rules e.g operation name. I used tags.
The customizer:
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import org.springdoc.api.OpenApiCustomiser;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
#Component
public class SecurityOverrideCustomizer implements OpenApiCustomiser {
public static final String UNSECURED = "security.open";
private static final List<Function<PathItem, Operation>> OPERATION_GETTERS = Arrays.asList(
PathItem::getGet, PathItem::getPost, PathItem::getDelete, PathItem::getHead,
PathItem::getOptions, PathItem::getPatch, PathItem::getPut);
#Override
public void customise(OpenAPI openApi) {
openApi.getPaths().forEach((path, item) -> getOperations(item).forEach(operation -> {
List<String> tags = operation.getTags();
if (tags != null && tags.contains(UNSECURED)) {
operation.setSecurity(Collections.emptyList());
operation.setTags(filterTags(tags));
}
}));
}
private static Stream<Operation> getOperations(PathItem pathItem) {
return OPERATION_GETTERS.stream()
.map(getter -> getter.apply(pathItem))
.filter(Objects::nonNull);
}
private static List<String> filterTags(List<String> tags) {
return tags.stream()
.filter(t -> !t.equals(UNSECURED))
.collect(Collectors.toList());
}
}
Now we can add #Tag(name = SecurityOverrideCustomizer.UNSECURED) to unsecured methods:
#Tag(name = SecurityOverrideCustomizer.UNSECURED)
#GetMapping("/open")
#ResponseBody
public String open() {
return "It works!";
}
Please bear in mind that it is just a workaround. Hopefully, the issue will be resolved in the next springdoc-openapi versions (at the time of writing it the current version is 1.2.18).
For a working example see springdoc-security-override-fix
Tested with v1.2.29 of springdoc-openapi: Its possible to disable security for particular Endpoint using: #SecurityRequirements
#GetMapping("/open")
#ResponseBody
#SecurityRequirements
public String open() {
return "It works!";
}
For older versions, for example tested with v1.2.28 using OperationCustomizer:
public static final String UNSECURED = "security.open";
#Bean
public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
List<String> tags = operation.getTags();
if (tags != null && tags.contains(UNSECURED)) {
operation.setSecurity(Collections.emptyList());
operation.setTags(tags.stream()
.filter(t -> !t.equals(UNSECURED))
.collect(Collectors.toList()));
}
return operation;
};
}
According to Documentation of springdoc, for new versions you can do this
Add the #SecurityRequirement for the protected route like
#PostMapping(value = "/example")
#SecurityRequirement(name = "bearer-key")
public ResponseEntity<Object> exampleHandler() {
/// logic here
}
and then in your Security config class add
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-key",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")));
}
Here is the link to the docs

How do I assign a context path prefix to multiple Spring Boot classes?

I have a bunch of resources in my Spring Boot project. Instead of giving them fully static paths, I'd prefer to make the prefixes (i.e. /attributes/, /entitites/ etc.) configurable. Previously, I used the servlet.context-path settings, but I'd like to have a different path per package in my project and different path for static resources...
I.e. all beans from package Entities being mapped to a context with a prefix /{servlet.context-path}/entities/{RequestMapping}, beans from my package attributes to /{servlet.context-path}/attributes/{RequestMapping} without having to specify a static prefix, such as /attributes/static/list in every single bean.
I have the same problem and I have resolve with this Configuration :
#Configuration
public class FeaturesRestConfiguration implements WebMvcConfigurer {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix(
"apiPackage1",
HandlerTypePredicate.forAnnotation(RestController.class)
.and(
HandlerTypePredicate.forBasePackage(
"com.projects.api.package1")));
}
}
All java class with RestController annotation automatically have apiPackage1 prefixed in url.
tested on a Spring Boot 2.2 application
If you want to have prefixed RequestMapping per package and to don't repeat your slef you can create an abstract controller per package; for example :
#RequestMapping(value = "/entities", produces = {MediaType.APPLICATION_JSON_VALUE, MediaTypes.HAL_JSON_VALUE})
public abstract class EntitiesAbstractController {
}
#RequestMapping(value = "/attributes", produces = {MediaType.APPLICATION_JSON_VALUE, MediaTypes.HAL_JSON_VALUE})
public abstract class AttributesAbstractController {
}
And then in you controllers you'll have :
#RestController
public class ChildController extends EntitiesAbstractController{
#GetMapping(value="/PATH_SUFFIX_HERE")
//method here
}
So beside your servlet.context-path, you will have endpoints that looks like this :
/{servlet.context-path}/entities/PATH_SUFFIX_HERE
/{servlet.context-path}/attributes/PATH_SUFFIX_HERE

Categories

Resources