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
Related
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) {
[...]
}
Given a Spring Data REST (SDR) server built with Spring Boot Gradle Plugin 2.2.5.RELEASE, is it possible to load an #Entity by self link within the server application?
I'm aware how to access it with an HTTP client, e.g. using curl:
$ curl localhost/users/1 # Responds with 200 OK and JSON representation
What I'm searching for is a mechanism to do this in the server using Java only, ideally using a standard SDR mechanism:
#Service
public class SelfLinkResolver {
public Object findBySelfLink(Link self) {
if (self == null || !self.getRel().equals(SELF)) {
throw new IllegalArgumentException("Non-null self link expected");
}
return null; // How to return the entity using a standard SDR mechanism?
}
public void exampleCall() {
Link self = new Link("localhost/users/1");
Object entity = findBySelfLink(self);
requireNonNull(entity, "Failed to load entity by self link");
}
}
An internal solution is parse your link and extract the ID (1 in your example), the call repository.findById(id).
Another solution would be new a RestTemplate, call your own API.
I finally came up with this solution, which uses SDR's UriToEntityConverter. In contrast to my question, it requires not only the self link, but also the entity class. It therefore doesn't fully answer my initial question.
I guess that there is no SDR solution that does not require the entity class, since there is no need for this within the framework, at least for usual API calls. SDR is always provided with the type information through the Repository, to which the self link refers. However, I didn't dive into other classes such as PersistentEntities, RepositoryInvokerFactory or Repositories, which might provide a solution for this.
WARNING: My tested implementation differs from this. This code is untested, but should illustrate the idea.
import lombok.NonNull;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static org.springframework.hateoas.IanaLinkRelations.SELF;
#Component
public class SelfLinkToEntityConverter extends UriToEntityConverter {
private static final TypeDescriptor URI_DESCRIPTOR = TypeDescriptor.valueOf(URI.class);
SelfLinkToEntityConverter(#NonNull PersistentEntities entities,
#NonNull RepositoryInvokerFactory invokerFactory,
#NonNull Repositories repositories) {
super(entities, invokerFactory, repositories);
}
#NonNull
public <T> Optional<T> findBySelfLink(#NonNull Link self, #NonNull Class<T> entityClass) {
checkArgument(self.getRel().equals(SELF), "Non-null self link expected");
URI uri = self.expand().toUri();
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(entityClass);
try {
#SuppressWarnings("unchecked")
T entity = (T) super.convert(uri, URI_DESCRIPTOR, typeDescriptor);
return Optional.ofNullable(entity);
} catch (IllegalArgumentException o_O) {
throw new IllegalArgumentException(format("Failed to load %s: %s",
entityClass.getSimpleName(), self.getHref()));
}
}
}
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"))
I am using JAX-RS and it has default implementations for #OPTIONS and #HEAD methods, but I would like to provide a different functionality.
What I have done currently is this:
#GET
#Path("path/to/resource")
#Produces(MediaType.APPLICATION_JSON)
public Response resourceCall(){
// Normal method implementation
}
#OPTIONS
#Path("path/to/resource")
#Produces(MediaType.APPLICATION_JSON)
public Response resourceCall(){
Response.status(Response.Status.METHOD_NOT_ALLOWED).build();
}
So basically I create a new call to each one of my resources. But I would like to have a catch-all method here that would treat all my calls to #OPTION or to #HEAD.
How do I implement such functionality?
EDIT
Just for clarity. I know how to do this using a Servlet Filter but I am wondering if JAX-RS has a similar feature built-in for th is specific case.
Since you are using Jersey, Jersey has a feature that allows you to programmatically add and modify resources. So you could add OPTIONS and HEAD methods to all your resources, without having to touch your resource classes. Below is an example that does nothing but send a 405 with a No <Method> message. Probably not what you want, but you should be able to figure out what you need to modify to get what you want.
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.model.ModelProcessor;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceModel;
#Provider
public class HeadAndOptionsModelProcessor implements ModelProcessor {
#Override
public ResourceModel processResourceModel(ResourceModel resourceModel,
Configuration configuration) {
ResourceModel.Builder resourceModelBuilder = new ResourceModel.Builder(false);
for (Resource rootResource: resourceModel.getResources()) {
final Resource.Builder rootResourceBuilder = Resource.builder(rootResource);
addOptions(rootResourceBuilder);
addHead(rootResourceBuilder);
for (Resource childResource: rootResource.getChildResources()) {
final Resource.Builder childResourceBuilder = Resource.builder(childResource);
addOptions(childResourceBuilder);
addHead(childResourceBuilder);
rootResourceBuilder.addChildResource(childResourceBuilder.build());
}
resourceModelBuilder.addResource(rootResourceBuilder.build());
}
return resourceModelBuilder.build();
}
#Override
public ResourceModel processSubResource(ResourceModel subResourceModel,
Configuration configuration) {
return subResourceModel;
}
private void addOptions(Resource.Builder resourceBuilder) {
resourceBuilder.addMethod("OPTIONS")
.handledBy(new Inflector<ContainerRequestContext, Response>() {
#Override
public Response apply(ContainerRequestContext context) {
return getOptionsResponse(context);
}
}).produces(MediaType.TEXT_PLAIN).extended(true);
}
private void addHead(Resource.Builder resourceBuilder) {
resourceBuilder.addMethod("HEAD")
.handledBy(new Inflector<ContainerRequestContext, Response>() {
#Override
public Response apply(ContainerRequestContext context) {
return getHeadResponse(context);
}
}).produces(MediaType.TEXT_PLAIN).extended(true);
}
private static Response getOptionsResponse(ContainerRequestContext context) {
return Response.status(Response.Status.METHOD_NOT_ALLOWED).entity("No Options").build();
}
private static Response getHeadResponse(ContainerRequestContext context) {
return Response.status(Response.Status.METHOD_NOT_ALLOWED).entity("No Head").build();
}
}
Default options handler is implemented via
#OPTIONS
#Path("{path: .*}")
public Response allOptions() {
}
As an example, take subdomain mapping.
This article: Managing multiple Domain and Sub Domain on Google App Engine for Same Application
recommends to resolve subdomain on Filter and assign variable to ServletRequest headers.
Then the mapping will look like this:
#RequestMapping(value = "/path", headers="subdomain=www")
public String subsiteIndexPage(Model model,HttpServletRequest request) { ... }
If we'd like to create custom #RequestMapping property, such as subdomain, eg. to create mapping like this:
#RequestMapping(value = "/some/action", subdomain = "www")
public String handlerFunction(){ ... }
we should override #RequestMapping #interface definition and override RequestMappingHandlerMapping protected methods, with our own implementation
(as stated on JIRA: "Allow custom request mapping conditions SPR-7812").
Is it right? Can anybody provide a hint, how to achieve this functionality?
Idea 1:
As suggested on original jira thread, is to create own implementation of RequestCondition
There is an project which uses this solution available on github: https://github.com/rstoyanchev/spring-mvc-31-demo/
And related SO question: Adding custom RequestCondition's in Spring mvc 3.1
Maybe mapping like #Subdomain("www") for both Type and Method, is possible solution?
Link to same question on forum.springsource.com
I've created solution based on referenced spring-mvc-31-demo
This solution can be used to map only single RequestCondition as of now. I've created two Issues to notify, this should be changed:
https://github.com/rstoyanchev/spring-mvc-31-demo/issues/5
https://jira.springsource.org/browse/SPR-9350
This solution uses custom #RequestCondition feature of Spring 3.1.1.RELEASE platform
USAGE
Example 1:
#Controller
#SubdomainMapping(value = "subdomain", tld = ".mydomain.com")
class MyController1 {
// Code here will be executed only on address match:
// subdomain.mydomain.com
}
Example 2:
#Controller
class MyController2 {
#RequestMapping("/index.html")
#SubdomainMapping("www")
public function index_www(Map<Object, String> map){
// on www.domain.com
// where ".domain.com" is defined in SubdomainMapping.java
}
#RequestMapping("/index.html")
#SubdomainMapping("custom")
public function index_custom(Map<Object, String> map){
// on custom.domain.com
// where ".domain.com" is defined in SubdomainMapping.java
}
}
We need three files
SubdomainMapping.java
SubdomainRequestCondition.java
SubdomainRequestMappingHandlerMapping.java
SubdomainMapping.java
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface SubdomainMapping {
/**
* This param defines single or multiple subdomain
* Where the Method/Type is valid to be called
*/
String[] value() default {};
/**
* This param defines site domain and tld
* It's important to put the leading dot
* Not an array, so cannot be used for mapping multiple domains/tld
*/
String tld() default ".custom.tld";
}
SubdomainRequestCondition.java
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
public class SubdomainRequestCondition implements
RequestCondition<SubdomainRequestCondition> {
private final Set<String> subdomains;
private final String tld;
public SubdomainRequestCondition(String tld, String... subdomains) {
this(tld, Arrays.asList(subdomains));
}
public SubdomainRequestCondition(String tld, Collection<String> subdomains) {
this.subdomains = Collections.unmodifiableSet(new HashSet<String>(
subdomains));
this.tld = tld;
}
#Override
public SubdomainRequestCondition combine(SubdomainRequestCondition other) {
Set<String> allRoles = new LinkedHashSet<String>(this.subdomains);
allRoles.addAll(other.subdomains);
return new SubdomainRequestCondition(tld, allRoles);
}
#Override
public SubdomainRequestCondition getMatchingCondition(
HttpServletRequest request) {
try {
URL uri = new URL(request.getRequestURL().toString());
String[] parts = uri.getHost().split(this.tld);
if (parts.length == 1) {
for (String s : this.subdomains) {
if (s.equalsIgnoreCase(parts[0])) {
return this;
}
}
}
} catch (Exception e) {
e.printStackTrace(System.err);
}
return null;
}
#Override
public int compareTo(SubdomainRequestCondition other,
HttpServletRequest request) {
return org.apache.commons.collections.CollectionUtils.removeAll(other.subdomains, this.subdomains).size();
}
}
SubdomainRequestMappingHandlerMapping.java
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
public class CustomRequestMappingHandlerMapping extends
RequestMappingHandlerMapping {
#Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
SubdomainMapping typeAnnotation = AnnotationUtils.findAnnotation(
handlerType, SubdomainMapping.class);
return createCondition(typeAnnotation);
}
#Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
SubdomainMapping methodAnnotation = AnnotationUtils.findAnnotation(
method, SubdomainMapping.class);
return createCondition(methodAnnotation);
}
private RequestCondition<?> createCondition(SubdomainMapping accessMapping) {
return (accessMapping != null) ? new SubdomainRequestCondition(
accessMapping.tld(), accessMapping.value()) : null;
}
}
Instalation
IMPORTANT: So far, it is not possible to use this solution with XML element
<mvc:annotation-driven />, see JIRA https://jira.springsource.org/browse/SPR-9344 for explanation
You have to register custom MappingHandler bean, pointing at this custom implementation SubdomainRequestMappingHandlerMapping class
You have to set it's order to be lower than default RequestMappingHandlerMapping
OR
Replace the registered RequestMappingHandlerMapping (possibly on order=0)
For more wide explanation on implementing this solution, see the related github project
That's correct, but that would be too complicated. You'd better check the Host header, whether it contains a given subdomain.
But you should not really need this more than once or twice, so you can also do it manually in the method body. If you really need it in many places, it would be an odd requirement.