Serving multiple static assets using Dropwizard - java

I have a react app that I am building and trying to serve using an AssetBundle, as such:
#Override
public void initialize(final Bootstrap<PersonalWebsiteConfiguration> bootstrap) {
bootstrap.addBundle(new SwaggerBundle<PersonalWebsiteConfiguration>() {
protected SwaggerBundleConfiguration getSwaggerBundleConfiguration(final PersonalWebsiteConfiguration configuration) {
return configuration.swaggerBundleConfiguration;
}
});
bootstrap.addBundle(new AssetsBundle("/build", "/", "index.html"));
}
I also added the configuration
server:
rootPath: /api
so there wouldn't be conflicts with my API.
This works great for just the landing page of my React app. Whenever I try the route /login /dashboard, this page from the UI not found. So I tried adding more bundles to fix that problem with the routing:
bootstrap.addBundle(new AssetsBundle("/build", "/", "index.html"));
bootstrap.addBundle(new AssetsBundle("/build", "/login", "index.html"));
bootstrap.addBundle(new AssetsBundle("/build", "/dashboard", "index.html"));
Now, only the dashboard is working. Does anyone know how to serve a React build with multiple routing/pages.

For a Single Page App you'd need every client route to return the index.html (to support browser reload or landing on paths other than /)
As far as I know, Dropwizard AssetBundle can't do that, i.e. serve all routes with index.html.
See similar (old) question.
You can implement a servlet filter yourself or use some community plugin like this one.
I must say that another approach worked better for me, don't use dropwizard for serving the static assets at all, only use it as a backend API.
Use CDN routing or different subdomains for API and static assets. That way you can have you static assets at www.mydomain.com and your API at api.mydomain.com (or use same domain and based on the path prefix e.g. /api route to backend or static resources)

You can add a filter to implement the functionality.
For a dropwizard plugin which implements this kind of filter see https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-spa.
The following stand-alone example code is in Kotlin.
class SinglePageAppFilter : Filter {
override fun doFilter(servletRequest: ServletRequest,
servletResponse: ServletResponse,
chain: FilterChain) {
val request = servletRequest as HttpServletRequest
val response = servletResponse as HttpServletResponse
if (request.requestURI == "/" || request.requestURI.startsWith("/api")) {
chain.doFilter(servletRequest, servletResponse)
} else {
val wrapper = ResponseWrapper(response)
chain.doFilter(servletRequest, wrapper)
val sc = wrapper.sc
if (sc == HttpServletResponse.SC_NOT_FOUND) {
request.getRequestDispatcher("/").forward(request, response)
} else if (sc != null) {
response.sendError(sc)
}
}
}
override fun init(filterConfig: FilterConfig) {}
override fun destroy() {}
}
class ResponseWrapper(response: HttpServletResponse) : HttpServletResponseWrapper(response) {
var sc: Int? = null
override fun sendError(sc: Int) {
this.sc = sc
}
}
class MyApplication : Application<MyConfiguration>() {
override fun initialize(bootstrap: Bootstrap<MyConfiguration>) {
bootstrap.addBundle(AssetsBundle("/assets", "/", "index.html"))
}
override fun run(configuration: MyConfiguration, environment: Environment) {
environment.servlets().addFilter("SinglePageAppFilter", SinglePageAppFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*")
}
}

Related

Can I determine in a servlet filter whether a HttpServletRequest maps to a particular Spring controller class

I'm working on an application that's using a OncePerRequestFilter to do some custom log-like behavior using the incoming web request. This behavior uses both the HttpServletRequest & HttpServletResponse. Additionally, the filter uses both ContentCachingRequestWrapper & ContentCachingResponseWrapper to access the request/response bodies.
It's been decided that we only want to do this behavior when methods in particular Spring controllers have been called, since it's not something we want to do for other controllers/actuator endpoints/etc. Is there a way to tell whether the incoming request will be (or was) mapped to a controller?
public class ExampleFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Can I tell here whether this will be mapping to an endpoint in
// ExampleController or NestedExampleController?
ContentCachingRequestWrapper requestToUse = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseToUse = new ContentCachingResponseWrapper(response);
try {
filterChain.doFilter(requestToUse, responseToUse);
// Can I tell here whether this was mapped to an endpoint in
// ExampleController or OtherExampleController?
} finally {
responseToUse.copyBodyToResponse(); // Write the cached body back to the real response
}
}
}
#RestController
#RequestMapping("/example")
public class ExampleController {
#GetMapping("/{id}")
public Example retrieveExample() {
return getValue(); // Retrieve the value
}
// ...
}
#RestController
#RequestMapping("/example/{id}/nested")
public class NestedExampleController {
#GetMapping("/{nestedId}")
public NestedExample retrieveNestedExample() {
return getValue(); // Retrieve the value
}
// ...
}
I've dug around the Spring MVC/Boot internals a bit, and I'm not sure if there's a way to easily do this. As an alternative, I can do some manual URL pattern matching, which probably won't necessarily exactly match up to the methods in the controllers, but may get me close enough to be an acceptable solution.
To summarize: is there a way in a web filter to tell whether the incoming request will be mapped to a controller (prior to executing the filter chain) or whether it was mapped to a controller (after executing the filter chain)?
What you want is basically a cross-cutting concern that targets a specific part of your application - in this case, logging.
This is one of the most common use-cases for aspect-oriented programming, for which Spring has built-in support using AspectJ-style pointcuts.
You will need:
To enable AOP within your Spring configuration on a configuration class, as follows:
#Configuration
#EnableAspectJAutoProxy
public class AopConfiguration {
}
Define an aspect, e.g. as follows:
#Aspect
public class LoggingAspect {
Logger log = ...; // define logger
// Matches all executions in com.example.ExampleController,
// with any return value, using any parameters
#Pointcut("execution(* com.example.ExampleController.*(..))")
public void controllerExecutionPointcut() {}
#Around("controllerExecutionPointcut()")
public Object aroundTargetControllerInvocation(ProceedingJoinPoint pjp) {
log.debug("About to invoke method: {}", pjp.getSignature().getName());
try {
return pjp.proceed();
} catch (Throwable t) {
// note that getArgs() returns an Object[],
// so you may want to map it to a more readable format
log.debug("Encountered exception while invoking method with args {}", pjp.getArgs());
throw t;
}
log.debug("Sucessfully finished invocation");
}
}
See e.g. this guide to learn more about pointcut expressions.
Another common use-case for this is timing your method calls, although for that something like Micrometer (and the Micrometer adapter for Spring) using #Timed would probably be better.
You may also wish to read through the reference documentation, which devotes quite a lot of information on how AOP in Spring works.
Note: as will almost all other Spring proxying mechanisms, invocations from within the target object will not be proxied, i.e. this.otherControllerMethod() will not be subject to interception by the above advice. Similarly, private methods also cannot be intercepted. See section 5.4.3 of the reference documentation for more information.
As a last note, if performance is of great importance, you should check out AspectJ compile-time or load-time weaving, which gets rid of some of the overhead introduced by Spring's proxying mechanism (which is what Spring AOP uses under the hood). This will most likely not be necessary in your case, but is good to keep in mind.
Edit for comment:
Thanks! One caveat with this approach is that it does not give me access to the HttpServletRequest or HttpServletResponse, which is something I'm making use of. I can see where this would be helpful if that wasn't something I needed. I see that I wasn't explicit about that requirement in my question, so I'll update accordingly.
Indeed, that is unfortunately not directly possible with this approach. If you really need the request, then the HandlerInterceptor approach mentioned by #DarrenForsythe is another possible to go. If all you're going for is logging though, I see no reason why you absolutely need the request - unless you wish to extract specific headers and log those.
In that case, IMO, a OncePerRequestFilter as you originally tried would be far better, as you can control for which requests the filter gets applied (using shouldNotFilter(HttpServletRequest request) and matching on the URL).
After some additional poking around and some trial and error, I discovered that the controller is accessible through the RequestMappingHandlerMapping bean. When the request can be handled by a controller, this will map the request to a HandlerMethod for the controller's request handling method.
public class ExampleFilter extends OncePerRequestFilter {
private RequestMappingHandlerMapping requestMappingHandlerMapping;
#Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Object handler = getHandlerBean(request);
boolean isHandledController = handler instanceof ExampleController
|| handler instanceof NestedEampleController;
if (!isHandledController) {
filterChain.doFilter(request, response);
return;
}
// ...
}
private Object getHandlerBean(HttpServletRequest request) {
try {
HandlerExecutionChain handlerChain = requestMappingHandlerMapping.getHandler(request);
if (handlerChain != null) {
Object handler = handlerChain.getHandler();
if (handler instanceof HandlerMethod) {
return ((HandlerMethod) handler).getBean();
}
}
return null;
} catch (Exception e) {
return null;
}
}
#Override
protected void initFilterBean() {
WebApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
requestMappingHandlerMapping = appContext.getBean(RequestMappingHandlerMapping.class);
}
}
To be extra thorough and truly mimic Spring's handler logic, the DispatcherServlet logic could be used/mimicked instead of directly referencing RequestMappingHandlerMapping. This will consult all handlers, not just the RequestMappingHandlerMapping.
public class ExampleFilter extends OncePerRequestFilter {
private DispatcherServlet dispatcherServlet;
#Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Object handler = getHandlerBean(request);
boolean isHandledController = handler instanceof ExampleController
|| handler instanceof NestedEampleController;
if (!isHandledController) {
filterChain.doFilter(request, response);
return;
}
// ...
}
private Object getHandlerBean(HttpServletRequest request) {
try {
HandlerExecutionChain handlerChain = getHandler(request);
if (handlerChain != null) {
Object handler = handlerChain.getHandler();
if (handler instanceof HandlerMethod) {
return ((HandlerMethod) handler).getBean();
}
}
return null;
} catch (Exception e) {
return null;
}
}
/**
* Duplicates the protected "getHandler" method logic from DispatcherServlet.
*/
private HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
List<HandlerMapping> handlerMappings = dispatcherServlet.getHandlerMappings();
if (handlerMappings != null) {
for (HandlerMapping mapping : handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
#Override
protected void initFilterBean() {
WebApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
dispatcherServlet = appContext.getBean(DispatcherServlet.class);
}
}
I'm not sure if there is a more idiomatic approach, and it definitely feels like it's jumping through some hoops and digging into the Spring internals a bit too much. But it does appear to work, at least on spring-web 5.2.7.RELEASE.

Java servlets: frontController's urlPattern of `/` overrides default static file serving

I'm using gretty and running app with tomcat.
My only servlet is:
#WebServlet(name = "frontServlet", urlPatterns = arrayOf("/"))
class FrontServlet : HttpServlet() {
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
Router.doGet(req, resp)
}
...
}
My static assets are under WebContent/public
BUT any request including with paths to assets is handled by FrontServlet.
Default static serving works if I set FronServlet's url pattern to specific one (but I need it to catch all requests except for requests to static files).
What should I do and is there any way to invoke server's default static file handlers from my custom servlets?
Well, after struggling with such ancient technology and using some other answers, I've came to this two solutions:
1. In FrontServlet doGet method:
if (req.requestURI.startsWith("/static/") || req.requestURI.startsWith("/favicon.ico")) {
req.session.servletContext.getNamedDispatcher("default").forward(req, resp)
} else {
Router.doGet(req, resp)
}
Write a filter:
#WebFilter(filterName = "frontFiletr", urlPatterns = arrayOf("/*"))
class FrontFilter: Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val path = (request as HttpServletRequest).requestURI
if (path.startsWith("/static/")) {
chain.doFilter(request, response)
} else {
request.session.servletContext.getNamedDispatcher("frontServlet").forward(request, response)
}
}
}

How to make Basic Authentication work as an alternative for keycloak in a Angular JS/Spring boot app

We have migrated from Basic Authentication to Keycloak method in our project in the production environment. However we would like continue using Basic Authentication, for local development, standalone and demo instalations, which could be triggered by a profile or something like this.
In this project we have REST APIs developed with Java/Spring boot and an AngularJS application which consumes these APIs. We are using Keycloak to protect both AngularJS app and the APIs.
The problem is how to make Spring Security and Keycloak to work "together" in the same application with different profiles. The solution I found so far, was to configure both Spring Security and Keycloak, and made a workaround with properties files, as described below:
application-keycloak.properties
#Unactivate Basic Authentication
security.ignored=/**
application-local-auth.properties
#Unactivate Keycloak
spring.autoconfigure.exclude=org.keycloak.adapters.springboot.KeycloakSpringBootConfiguration
When I wanto to use keycloak, I have to ignore security in order to not have problems and when I want to use basic authentication I have to exclude Keycloak configuration in order to also prevent conflicts.
This is my Security Configuration class:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and()
.authorizeRequests()
.antMatchers("/","/scripts/**","/keycloak/isActive","/keycloak/config","/bower_components/**","/views/**","/fonts/**",
"/views/inventory/dialogs/**", "/services/**","/resources/**","/styles/**", "/info")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
}
And this is my Keycloak Spring Boot configuration:
# Keycloak
keycloak.realm=local
keycloak.realmKey=MIIBIjANBgkqhkiG9wsIIBCgKCAQEAuJYmaWvF3YhifflJhspXOs8RJn74w+eVD8PtpVbu2cYG9OIa49P8SwqVn/kyJQr7kT3OlCq3XMZWBHe+JSzSz7KttKkhfFSfzISdKDKlkPena2H/i3FKlRZIldbeeuQNYdD6nMpzU6QWLwGF1cUAo1M11f2p99QI1FOhVPJSErWsjDsKpWqG+rMMjT1eos0QCNP7krx/yfMdlUyaJCYiDvpOAoec3OWXvDJovEajBNAZMWVXgJF90wAVPRF6szraA2m7K2gG9ozaCNWB0v4Sy6czekbKjqEBPJo45uEmGHd92V//uf/WQG4HSiuv8CTV+b6TQxKtZCpQpqp2DyCLewIDAQAB
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.ssl-required=none
keycloak.resource=App-backend
keycloak.bearer-only=true
keycloak.credentials.secret=a714aede-5af9-4560-8c9d-d655c831772f
keycloak.securityConstraints[0].securityCollections[0].name=Secured API
keycloak.securityConstraints[0].securityCollections[0].authRoles[0]=ROLE_USER
keycloak.securityConstraints[0].securityCollections[0].patterns[0]=/api/*
It is working, however I think it is not an elegant solution. I have tried to implement this using the Keycloak property enable-basic-auth, but I could not understand how it works but it seems that it is just to protect Rest APIs, it does not allow the browser to create a session and use it for all the other requests.
Have someone ever had to implement something like this and can give me some better idea?
I managed to solve this. However, how beautiful my solution is is up for debate.
My use case is that I need to secure most of my endpoints using Keycloak but some (for batch processing) should just use Basic Auth. Configuring both has the downside that Keycloak tries to validate the Authorization Header even if it is Basic Auth so I needed to do three things.
Deactivate all automatic security for my batch route.
Write a custom request filter which secures the batch route.
Manipulate the servlet request object such that the zealous keycloak filter doesn't trip on it.
My security configuration.
#EnableWebSecurity
#EnableResourceServer
public class SecurityConfiguration extends KeycloakWebSecurityConfigureAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
// usual configuration ...
.antMatchers("/api/v1/batch/**").permitAll() // decouple security for this route
.anyRequest().denyAll();
}
}
My custom request filter (needs to run before the spring security filter, thus the ordering annotation):
#Component
#Slf4j
#Order(Ordered.HIGHEST_PRECEDENCE + 2)
public class BasicAuthRequestFilter extends OncePerRequestFilter {
#Value("${batch.user}")
private String user;
#Value("${batch.password}")
private String password;
#Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if (isBatchRequest(request)) {
SimpleHttpFacade facade = new SimpleHttpFacade(request, response);
if (AuthOutcome.AUTHENTICATED.equals(auth(facade))) {
filterChain.doFilter(new AuthentifiedHttpServletRequest(request), response);
}
log.debug("Basic auth failed");
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate with basic authentication");
return;
}
filterChain.doFilter(request, response);
}
private boolean isBatchRequest(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/v1/batch/");
}
private AuthOutcome auth(HttpFacade exchange) {
return extractToken(exchange.getRequest().getHeaders(HttpHeaders.AUTHORIZATION))
.map(token -> extractUserPw(token)
.filter(userpw -> verify(userpw.getFirst(), userpw.getSecond()))
.map(userpw -> AuthOutcome.AUTHENTICATED)
.orElse(AuthOutcome.FAILED))
.orElse(AuthOutcome.NOT_ATTEMPTED);
}
private Optional<String> extractToken(List<String> authHeaders) {
return authHeaders == null ? Optional.empty() : authHeaders.stream().map(authHeader -> authHeader.trim().split("\\s+"))
.filter(split -> split.length == 2)
.filter(split -> split[0].equalsIgnoreCase("Basic"))
.map(split -> split[1])
.findFirst();
}
private Optional<Pair<String, String>> extractUserPw(String token) {
try {
String userpw = new String(Base64.decode(token));
String[] parts = userpw.split(":");
if (parts.length == 2) {
return Optional.of(Pair.of(parts[0], parts[1]));
}
} catch (Exception e) {
log.debug("Basic Auth Token formatting error", e);
}
return Optional.empty();
}
private boolean verify(String user, String password) {
return (this.user.equals(user) && this.password.equals(password));
}
}
And finally the wrapped ServletRequest (as you cannot remove Headers from the request):
public class AuthentifiedHttpServletRequest extends HttpServletRequestWrapper {
public AuthentifiedHttpServletRequest(HttpServletRequest request) {
super(request);
}
#Override
public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
return true;
}
#Override
public String getAuthType() {
return "Basic";
}
#Override
public String getHeader(String name) {
if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
return super.getHeader(name);
}
return null;
}
#Override
public Enumeration<String> getHeaders(String name) {
if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
return super.getHeaders(name);
}
return Collections.enumeration(Collections.emptyList());
}
#Override
public Enumeration<String> getHeaderNames() {
return Collections.enumeration(EnumerationUtils.toList(super.getHeaderNames())
.stream()
.filter(s -> !HttpHeaders.AUTHORIZATION.equalsIgnoreCase(s))
.collect(Collectors.toList()));
}
#Override
public int getIntHeader(String name) {
if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
return super.getIntHeader(name);
}
return -1;
}
}
Not quite sure whether this is still relevant or not, but maybe someone will find it helpful.
By default, Keycloak is overwriting plenty of configurations. It's intercepting all Auth request (OAuth2, BasicAuth etc.)
Fortunately, with Keycloak, it's possible to enable authentication both with OAuth2 and BasicAuth in parallel, which I assume is what you want to enable in your dev/localhost environments.
In order to do that, you first need to add the following property to your
application-local-auth.properties:
keycloak.enable-basic-auth=true
This property will enable Basic Auth in your dev environment. However, you also need to enable Basic Auth at your client in Keycloak.
You can accomplish that by connecting to the Keycloak Admin Console on your local Keycloak server and enabling the Direct Access Grant for your client:
Enabling Basic Auth in Keycloak
After that you can authenticate both with Bearer Token and Basic Auth.

How to pass NTLM credentials to zuul request header

I have a Windows service "A" being used for authentication purposes (NOT managed by us) and I have Spring-boot based REST Api service "B" (managed by us) which uses Zuul to route traffic. There is an external service "C" (NOT managed by us) that needs to talk to the Windows service through our REST Apis. Since "A" uses NTLM authentication we need to pass the request body from "C" and add the ntlm credentials in the headers at "B" and route the traffic using zuul.
My question is, how do I add NTLM credentials in Java to the routed traffic in zuul headers?
~ Jatin
You need to write your own ZuulFilter.
Something along the lines of
import javax.servlet.http.HttpServletRequest;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.ZuulFilter;
public class MyFilter extends ZuulFilter {
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 1;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// now add your headers to the request
return null;
}
}
In your app just make sure the filter bean is created and it will be automatically registered:
#EnableZuulProxy
#SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
#Bean
public MyFilter myFilter() {
return new MyFilter();
}
}
Have a look at this guide for more info.
Zuul will work fine with Spring Session. There are many blogs about this.
http://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot.html

Making Spring/Tomcat compatible with HTML5 pushState

I have a single-page web app that's using Backbone.js client routing with pushState. In order to get this to work, I have to tell my server (Java, Spring 3, Tomcat) which URLs should be resolved on the server (actual JSP views, API requets), and which should simply be sent to the index page to be handled by the client. Currently I'm using an InternalResourceViewResolver to simply serve JSP views that match the name of the URL request. Since client-side URLs don't have a view on the server, the server returns a 404.
What is the best way to specify to Spring (or Tomcat) that a few specific URLs (my client-side routes) should all resolve to index.jsp, and anything else should fall through to the InternalResourceViewResolver?
I found that Spring MVC 3 added a tag that does exactly what I need, the mvc:view-controller tag. This got it done for me:
<mvc:view-controller path="/" view-name="index" />
<mvc:view-controller path="/admin" view-name="index" />
<mvc:view-controller path="/volume" view-name="index" />
http://static.springsource.org/spring/docs/3.0.x/reference/mvc.html
In theory, to handle navigation via history.pushState you want to return index.html for unhandled resources. If you look at official documentation for modern web frameworks it's often realised based on 404 status.
In spring you should handle resources in order:
path mapped REST controllers
app static resources
index.html for others
To do this you have at least 4 possible solutions.
Using EmbeddedServletContainerCustomizer and custom 404 handler
#Controller
static class SpaController {
#RequestMapping("resourceNotFound")
public String handle() {
return "forward:/index.html";
}
}
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/resourceNotFound"));
}
Using custom default request mapping handler
#Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
static class SpaWithHistoryPushStateHandler {
}
static class SpaWithHistoryPushStateHandlerAdapter implements HandlerAdapter {
#Override
public boolean supports(final Object handler) {
return handler instanceof SpaWithHistoryPushStateHandler;
}
#Override
public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
response.getOutputStream().println("default index.html");
return null;
}
#Override
public long getLastModified(final HttpServletRequest request, final Object handler) {
return -1;
}
}
#Bean
public SpaWithHistoryPushStateHandlerAdapter spaWithHistoryPushStateHandlerAdapter() {
return new SpaWithHistoryPushStateHandlerAdapter();
}
#PostConstruct
public void setupDefaultHandler() {
requestMappingHandlerMapping.setDefaultHandler(new SpaWithHistoryPushStateHandler());
}
Using custom ResourceResolver
#Autowired
private ResourceProperties resourceProperties;
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations(resourceProperties.getStaticLocations())
.setCachePeriod(resourceProperties.getCachePeriod())
.resourceChain(resourceProperties.getChain().isCache())
.addResolver(new PathResourceResolver() {
#Override
public Resource resolveResource(final HttpServletRequest request, final String requestPath, final List<? extends Resource> locations, final ResourceResolverChain chain) {
final Resource resource = super.resolveResource(request, requestPath, locations, chain);
if (resource != null) {
return resource;
} else {
return super.resolveResource(request, "/index.html", locations, chain);
}
}
});
}
Using custom ErrorViewResolver
#Bean
public ErrorViewResolver customErrorViewResolver() {
final ModelAndView redirectToIndexHtml = new ModelAndView("forward:/index.html", Collections.emptyMap(), HttpStatus.OK);
return (request, status, model) -> status == HttpStatus.NOT_FOUND ? redirectToIndexHtml : null;
}
Summary
Fourth option looks simplest but as always it depends what you need. You may also want to restric returning index.html only when request expects text/html (which BasicErrorController already do based on "produces" header).
I hope one of this options will help in your case.
I would give a clear scheme to my urls and separate frontend from backend.
Some suggestions:
Route all requests starting by /server to the backend and all others to the frontend.
Setup two different domains, one for the backend, one for the frontend.

Categories

Resources