Making Spring/Tomcat compatible with HTML5 pushState - java

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.

Related

How to handle 401 with spring boot with angular 8

I want to be able to handle 401 and show a specific page in angular 8 but currently only showing index.html file
Things to mind
Angular is the view for the spring boot so its not a separate application
I am not using spring security. Im just using filters in spring to determine if to be authorize
This is my filter.
#Component
public class CustomSessionFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) httpServletRequest;
if (!req.getRequestURI().startsWith("/sample/path")){
HttpServletResponse httpResponse = (HttpServletResponse) httpServletResponse;
httpResponse.setContentType("application/json");
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
}
Maybe its relevant that i have a Controller that extend ErrorController
#CrossOrigin
#RestController
public class IndexController implements ErrorController {
private static final String PATH = "/error";
#RequestMapping(value = PATH)
public ModelAndView saveLeadQuery() {
return new ModelAndView("forward:/");
}
#Override
public String getErrorPath() {
return PATH;
}
}
EDIT: I didnt use spring security because i dont need to login i just have to go through specific path and do some authentication.. and there is no user for the application
I just wanted to place my 0.02$ here.
In order to secure a route in your application, you can create a Security Config that extends WebSecurityConfigurerAdapter, where you can protect certain routes through either antMatchers or mvcMathchers (second is recommended). Furthermore, there you can declare a set of roles or conditions (via Spring Expression Language) that will automatically throw a 401 Error in case a user that does not have an access is trying to access the route.
More about that you can find here https://www.baeldung.com/security-spring
As of ErrorController, I believe that controller advice would be more suitable for this use case. It pretty much intercepts all errors that you declare and can return more informative and generic response :)
More about that https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc
As

How to declare Servlet on root path without overriding Spring MVC Controllers

I have Spring Boot application with REST API mapped on /api. I need to define additional servlet on /. I want all request that match /api was handled by REST API and all others requests by the servlet. How to do this?
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
#RestController
#RequestMapping("/api")
public class ApiController {
#GetMapping
public String get() {
return "api";
}
}
#Bean
public ServletRegistrationBean customServletBean() {
return new ServletRegistrationBean<>(new HttpServlet() {
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("custom");
}
}, "/*");
}
}
In code above I want something like this:
curl http://localhost:8080/api/
> api⏎
curl http://localhost:8080/custom/
> custom
I have tried with filter to redirect requests, but all requests go to custom servlet:
#Bean
public FilterRegistrationBean apiResolverFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter((req, response, chain) -> {
HttpServletRequest request = (HttpServletRequest) req;
String path = request.getRequestURI().substring(request.getContextPath().length());
if (path.startsWith("/api/")) {
request.getRequestDispatcher(path).forward(request, response);
} else {
chain.doFilter(request, response);
}
});
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
This project is available on github: https://github.com/mariuszs/nestedweb
When mapping a servlet to the root path you will override the mapping for the DispatcherServlet which, by default, is mapped to /.
There are basically 3 solutions you could try
Map the DispatcherServlet to /api and modify the mappings in your controllers
Use a ServletForwardingController to forward the request to the configured but unmapped Servlet
Use a ServletWrappingController to wrap a Servlet instance
Number 2 and 3 are almost the same, with this difference that with option 3 Spring also manages the Servlet instance whereas with option 2, the Servlet container manages the Servlet.
Mapping DispatcherServlet to /api
Option 1 can be an option if all of your controllers are mapped under /api, if they aren't this isn't an option. In your application.properties you would set the spring.mvc.servlet.path to /api. Then you would configure your other Servlet like you did in your question.
Use a ServletForwardingController
Spring provides a ServletForwardingController which will lookup a Servlet in the ServletContext given the name of the servlet and forward the request to it. You will still have to register the Servlet but prevent it from being mapped.
Next you would need a SimpleUrlHandlerMapping to map the URLs to this controller or set it as the default handler (basically a catch all).
#Bean
public ServletForwardingController forwarder() {
ServletForwardingController controller = new ServletForwardingController();
controller.setServletName("my-servlet");
return controller;
}
#Bean
public CustomServlet customServlet() {
return new CustomServlet();
}
#Bean
public ServletRegistrationBean customServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(customServlet(), false);
registration.setServletName("customServlet");
return registration;
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setDefaultHandler(forwarder());
mapping.setOrder(LOWEST_PRECEDENCE - 2);
return mapping;
}
Use a ServletWrappingController
Spring provides a ServletWrappingController which will internally create and configure a Servlet instance. It acts as an adapter from/to the Servlet to a Spring Controller. You don't have to register the CustomServlet in this case and is thus slightly easier to configure the then ServletForwardingController.
Next you would need a SimpleUrlHandlerMapping to map the URLs to this controller or set it as the default handler (basically a catch all).
#Bean
public ServletWrappingController wrapper() {
ServletWrappingController controller = new ServletWrappingController ();
controller.setServletName("my-servlet");
controller.setServletClass(CustomerServlet.class);
return controller;
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setDefaultHandler(wrapper());
mapping.setOrder(LOWEST_PRECEDENCE - 2);
return mapping;
}
Depending on your architecture and url structure you might want to go for option 1 or option 3.

Neo4j unmanaged extension - add custom request Filter to web server

We have unmanaged extension. We implemented custom communication API between server and client.
Now we need to ensure that client and server have same API version.
One solution - verify version in each resource. But this approach is messy and leads to code duplication.
What we want is to implement our own Filter and add it to Neo server.
Is this possible? If yes - then how?
This is possible!
Approach is a bit tricky and fragile, but it's working (blog post).
Dependency
You need neo4j-server dependency, because it contains SPIPluginLifecycle that is needed to get access to Neo4j web server.
So, add to your pom.xml:
<dependency>
<groupId>org.neo4j.app</groupId>
<artifactId>neo4j-server</artifactId>
<version>${version.neo4j}</version>
</dependency>
Filter
Create your filter. Let's take this one for example:
public class CustomFilter implements Filter {
public CustomFilter() {
}
#Override
public void init(final FilterConfig filterConfig) throws ServletException {}
#Override
public void doFilter(final ServletRequest request,
final ServletResponse response,
final FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
#Override
public void destroy() {}
}
This filter doesn't do anything usefull - just continue chain further.
Lifecycle plugin
Now tricky part. We need to:
Implement SPIPluginLifecycle
Get web server
Add filter to web server
Code:
public final class ExtensionPluginLifecycle implements SPIPluginLifecycle {
private WebServer webServer;
private CustomFilter customFilter;
#Override
public Collection<Injectable<?>> start(final NeoServer neoServer) {
webServer = getWebServer(neoServer);
addFilters();
}
#Override
public void stop() {
removeFilters();
}
#Override
public Collection<Injectable<?>> start(final GraphDatabaseService graphDatabaseService,
final Configuration config) {
throw new IllegalAccessError();
}
private WebServer getWebServer(final NeoServer neoServer) {
if (neoServer instanceof AbstractNeoServer) {
return ((AbstractNeoServer) neoServer).getWebServer();
}
throw new IllegalArgumentException(String.format("Expected: [AbstractNeoServer], Received: [%s].", neoServer));
}
private void addFilters() {
customFilter = new CustomFilter();
webServer.addFilter(customFilter, "/extension-path/*");
}
private void removeFilters() {
webServer.removeFilter(customFilter, "/extension-path/*");
}
}
Tricky part is not so "legal" access to web server. This can break in future, so be carefull.
Note addFilters() and removeFilters() methods - this is why we have been done all this way.
Important: lifecycle plugin should be registered as service:
// file: META-INF/services/org.neo4j.server.plugins.PluginLifecycle
my.company.extension.ExtensionPluginLifecycle

Grizzly/Jersey: Request injected into ContainerRequestFilter is null

I'm trying to implement a ContainerRequestFilter that checks some stuff. Ultimately, it shall extract the common name from a SSL client certificate, but I'm not there yet. The filter runs on a Grizzly HTTP Server (Grizzly 2.3.8, no servlet container) and sits in front of a JAX-RS resource (Jersey 2.6).
When I'm trying to inject org.glassfish.grizzly.http.server.Request into the filter, it is null.
import org.glassfish.grizzly.http.server.Request;
#Provider
#Priority(Priorities.AUTHENTICATION) // somewhat early
#SkpSecureClient // name the filter with my own annotation
public class SecureClientFilter implements ContainerRequestFilter {
private final Logger log = LoggerFactory.getLogger(this.getClass());
#Context
Request request;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
if (request == null) {
log.debug("Unable to inject 'request'");
// I always end up here
} else if (request.isSecure()) {
log.warn("Request is not secure!");
// do something, e.g. abort request
} else {
log.debug("Request is secure!");
}
}
}
Injecting the request into a JAX-RS resource, the injection is successful and I can work with it.
What am I doing wrong?
I think it's a bug in Jersey.
Just filed an issue
https://java.net/jira/browse/JERSEY-2462

Provide a response with view name or HTTP error code

Im am writing a Spring MVC #Controller. On a #RequestMapping method I would like to return a View name OR report back a 404 HTTP status code.
How can I continue using Spring View Resolver (for when I need a view) and control response error code without throwing an exception?
just raw draft
//initialised by init
private Map<String, View> viewsMap;
#RequestMapping(value = "/{path}")
#ResponseStatus(HttpStatus.OK)
public View getResponse(HttpServletResponse resp, #PathVariable String path) {
if (viewsMap.get(path ) != null){
return viewsMap.get(path );
}
resp.setStatus(404);
return null;
}
You'd need to implement your own ViewResolver and a View that returns a particular HTTP status code. You can extend InternalResourceViewResolver (or whatever you're using now).
public class MyViewResolver extends InternalResourceViewResolver {
public static final String HTTP_404_VIEW = "http404view";
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (HTTP_404_VIEW.equals(viewName)) {
return new StatusCodeView(404);
}
else
{
return super.resolveViewName(viewName, locale);
}
}
}
public class StatusCodeView implements View
{
private final int code;
public StatusCodeView(int code)
{
this.code = code;
}
public void render(Map model, HttpServletRequest request, HttpServletResponse response)
{
response.sendError(this.code);
}
}
In your controller, just return MyViewResolver.HTTP_404_VIEW.
The best way I found to solve this is using directly the provided View Resolver for both cases. I simply created a view (in my case a .jsp file) for "not found" responses and:
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return "error/" + HttpServletResponse.SC_NOT_FOUND;
Extra: as there can be different ways to trigger a HTTP error response, I also mapped the error pages in web.xml to have a similar view for same HTTP error code responses.
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/views/error/404.jsp</location>
</error-page>
A little work for what I needed, but it made me set HTTP error handling more accurately in my application.

Categories

Resources