Override default dispatcherServlet when a custom REST controller has been created - spring

Following my question here, I have succeded in creating a custom REST controller to handle different kinds of requests to /api/urls and operate accordingly.
However, there is still a default controller handling requests at /urls which affects my application: When receiving a request that is not /api/something, it should fetch my database for the URL linked to said /whatever and redirect the user there. Moreover, under /api/urls I've developed certain validation rules to ensure integrity and optimization of the requests, which does not jhappen in /urls so anyone could insert any kind of data into my database.
What would be a possible way to disable this default handler? Seeing the logs I headed to register my own ServletRegistrationBean as instructed here but this is for having two isolated environments as far as I understand
My goal is to simply "disconnect" /urls URL from the default REST controller -which is no longer of any use to me now that I have my own one- and just use the custom one that I implemented in /api/urls (Or whatever other URL I may decide to use such as "/service/shortener* if possible)
Below are my Java classes:
Url.java (getters and setters omitted for brevity):
#Document
public class Url {
#Id private String id;
private String longURL;
private String hash;
private String originalUrl;
private String shortUri;
private Date creationDate;
}
UrlRepository.java
import org.springframework.data.mongodb.repository.MongoRepository;
public interface UrlRepository extends MongoRepository<Url, String> {
// Empty
}
UrlController.java:
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/api/urls")
public class UrlController {
#Autowired
private UrlRepository repo;
#RequestMapping(method=RequestMethod.GET)
public List<Url> getAll() {
System.out.println("Showing all stored links");
List<Url> results = repo.findAll();
return results;
}
#RequestMapping(method=RequestMethod.GET, value="{id}")
public Url getUrl(#PathVariable String id) {
System.out.println("Looking for URL " + id);
return null;
}
#RequestMapping(method=RequestMethod.POST)
public Url create(#RequestBody Url url) {
System.out.println("Received POST " + url);
return null;
}
#RequestMapping(method=RequestMethod.DELETE, value="{id}")
public void delete(#PathVariable String id) {
//TBD
}
#RequestMapping(method=RequestMethod.PUT, value="{id}")
public Url update(#PathVariable String id, #RequestBody Url url) {
//TBD
}
}
Application.java:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

Instead of trying to hack your way around Spring Boot and Spring Data REST I strongly suggest to work WITH the frameworks instead of around them.
To change the default context-path from / to /api simply add a property to your application.properties file.
server.context-path=/api
Now you would need to change your controller mapping to /urls instead of /api/urls.
If you only want /api for Spring Data REST endpoints use the following property
spring.data.rest.base-uri=/api
This will make all Spring Data REST endpoints available under /api. You want to override the /urls so instead of using #Controller use #RepositoryRestController this will make your controller override the one registered by default.
#RepositoryRestController
#RequestMapping("/urls")
public class UrlController { ... }

Related

Convert JWT Authentication Principal to something more usable in spring

I have a spring boot microservice that validates a JWT (issued by a different service) for authentication. It is working nicely, and I can access the JWT details in my controller like so:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// MyController.java
#RestController
#RequestMapping("/")
public class MyController {
#GetMapping()
public String someControllerMethod(#AuthenticationPrincipal Jwt jwt) {
int userId = Integer.parseInt(jwt.getClaim("userid"));
...
}
}
That works great. I can extract what I need from the JWT and go on to talk to my database with the correct userid etc.
However I find it a bit tedious to have to use the Jwt type to get these values in each controller. Is there a way I can inject a different type as the #AuthenticationPrincipal?
E.g. my own class which has already extracted what is needed from the JWT, and exposes something like .getUserId() that returns an int?
That would also let me centralise the logic of parsing the claims or throwing exceptions if they are not as expected etc.
UPDATE
After more google spelunking, it seems I have two options
Option1: #ControllerAdvice and #ModelAttribute
As explained in this answer. I can do something like:
import com.whatever.CustomPrincipal; // a basic "data" class with some properties, getters, setters and constructor
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
#ControllerAdvice
public class SecurityControllerAdvice {
#ModelAttribute
public CustomPrincipal customPrincipal(Authentication auth) throws Exception {
CustomPrincipal customPrincipal;
if (auth != null && auth.getPrincipal() instanceof Jwt) {
Jwt jwt = (Jwt) auth.getPrincipal();
String sessionId = jwt.getClaimAsString("sessionid");
int userId = Integer.parseInt(jwt.getClaimAsString("userid"));
customPrincipal = new CustomPrincipal(userId, sessionId);
} else {
// log an error and throw an exception?
}
return customPrincipal;
}
}
and then
import com.whatever.CustomPrincipal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;
#RestController
#ControllerAdvice
public class HelloWorldController {
#GetMapping("/controlleradvice")
public String index(#ModelAttribute CustomPrincipal cp) {
log.info(cp.getUserId());
return "whatever";
}
}
This seems pretty succinct, and neat and tidy. 1 new class with #ControllerAdvice, and bob's your uncle!
Option2: Using jwtAuthenticationConverter()
This answer shows another way to do it, using a "converter", which seems to convert the default Principal from a JWT to a custom object (that extends AbstractAuthenticationToken) that contains the JWT (.getCredentials()) as well as a custom object like CustomPrincipal (or a User class or something).
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer().jwt(customizer -> customizer.jwtAuthenticationConverter((new MyPrincipalJwtConvertor())));
return http.build();
}
}
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;
public class MyPrincipalJwtConvertor implements Converter<Jwt, MyAuthenticationToken> {
#Override
public MyAuthenticationToken convert(Jwt jwt) {
var principal = new MyPrincipal(Integer.parseInt(jwt.getClaimAsString("userid")), jwt.getClaimAsString("sessionid"));
return new MyAuthenticationToken(jwt, principal);
}
}
#RestController
public class HelloWorldController {
#GetMapping("/converter")
public String converter(#AuthenticationPrincipal MyPrincipal myPrincipal) {
log.info("/converter triggered");
log.info("" + myPrincipal.getUserId());
return "woo";
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
#Data
#AllArgsConstructor
public class MyPrincipal {
private int userId;
private String sessionId;
}
Option 1 is much simpler it seems.
But Option 2 is nice, as, I have Filter's that run to do additional validation (like validate the session id in the JWT). When that filter runs, when it calls SecurityContext.getContext().getAuthentication().getPrincipal(), it will get the MyPrincipal object, and not have to call Jwt.getClaimAsString() and cast it etc.
I guess I am asking, are there pros and cons to these two approaches I have not considered? Is one of them perhaps bastardising/abusing something in a way it is not meant to be?
Or is it much the same and I should select whichever I prefer?

Spring boot Angular routing [duplicate]

I believe this is a simple question, but I couldn't find an answer or at least use the correct terms in the search.
I am setting up Angular2 and Springboot together. By default, Angular will use paths like localhost:8080\dashboard and localhost:8080\dashboard\detail.
I'd like to avoid using path as hashs, if possible. As Angular documentation states:
The router's provideRouter function sets the LocationStrategy to the PathLocationStrategy, making it the default strategy. We can switch to the HashLocationStrategy with an override during the bootstrapping process if we prefer it.
And then...
Almost all Angular 2 projects should use the default HTML 5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.
The issue is that when I try to access localhost:8080\dashboard, Spring will look for some controller mapping to this path, which it won't have.
Whitelabel Error Page
There was an unexpected error (type=Not Found, status=404).
No message available
I thought initially to make all my services to be under localhost:8080\api and all my static under localhost:8080\app. But how do I tell Spring to ignore requests to this app path?
Is there a better solution with either Angular2 or Boot?
In my Spring Boot applications (version 1 and 2), my static resources are at a single place :
src/main/resources/static
static being a folder recognized by Spring Boot to load static resources.
Then the idea is to customize the Spring MVC configuration.
The simpler way is using Spring Java configuration.
I implement WebMvcConfigurer to override addResourceHandlers().
I add in a single ResourceHandler to the current ResourceHandlerRegistry.
The handler is mapped on every request and I specify classpath:/static/ as resource location value (you may of course adding others if required).
I add a custom PathResourceResolver anonymous class to override getResource(String resourcePath, Resource location).
And the rule to return the resource is the following : if the resource exists and is readable (so it is a file), I return it. Otherwise, by default I return the index.html page. Which is the expected behavior to handle HTML 5 urls.
Spring Boot 1.X Application :
Extending org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter is the way. The class is an adapter of the WebMvcConfigurer interface
with empty methods allowing sub-classes to override only the methods they're interested in.
Here is the full code :
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.resource.PathResourceResolver;
#Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
Spring Boot 2.X Application :
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter was deprecated.
Implementing directly WebMvcConfigurer is the way now as it is still an interface but it has now default methods (made possible by a Java 8 baseline) and can be implemented directly without the need for the adapter.
Here is the full code :
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
EDIT to address some comments :
For those that store their static resources at another location as src/main/resources/static, change the value of the var args parameter of addResourcesLocations() consequently.
For example if you have static resources both in static and in the public folder (no tried) :
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/", "/public")
I have a solution for you, you can add a ViewController to forward requests to Angular from Spring boot.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
public class ViewController {
#RequestMapping({ "/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers" })
public String index() {
return "forward:/index.html";
}
}
here I have redirected all my angular2 ("/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers") to my SPA
You can do the same for your preject and you can also redirect your 404 error page to the index page as a further step.
Enjoy!
You can forward all not found resources to your main page by providing custom ErrorViewResolver. All you need to do is to add this to your #Configuration class:
#Bean
ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() {
return new ErrorViewResolver() {
#Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
return status == HttpStatus.NOT_FOUND
? new ModelAndView("index.html", Collections.<String, Object>emptyMap(), HttpStatus.OK)
: null;
}
};
}
You can forward everything not mapped to Angular using something like this:
#Controller
public class ForwardController {
#RequestMapping(value = "/**/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
Source: https://stackoverflow.com/a/44850886/3854385
My Spring Boot server for angular is also a gateway server with the API calls to /api to not have a login page in front of the angular pages, you can use something like.
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
/**
* This sets up basic authentication for the microservice, it is here to prevent
* massive screwups, many applications will require more secuity, some will require less
*/
#EnableOAuth2Sso
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Override
public void configure(HttpSecurity http) throws Exception {
http
.logout().logoutSuccessUrl("/").and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll().and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
To make it more simple you can just implement ErrorPageRegistrar directly..
#Component
public class ErrorPageConfig implements ErrorPageRegistrar {
#Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
}
}
This would forward the requests to index.html.
#Controller
#RequestMapping("/")
public class MainPageController {
#ResponseStatus(HttpStatus.OK)
#RequestMapping({ "/" })
public String forward() {
return "forward:/";
}
}
I did it with a plain old filter:
public class PathLocationStrategyFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if(request instanceof HttpServletRequest) {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String uri = servletRequest.getRequestURI();
String contextPath = servletRequest.getContextPath();
if(!uri.startsWith(contextPath + "/api") &&
!uri.startsWith(contextPath + "/assets") &&
!uri.equals(contextPath) &&
// only forward if there's no file extension (exclude *.js, *.css etc)
uri.matches("^([^.]+)$")) {
RequestDispatcher dispatcher = request.getRequestDispatcher("/");
dispatcher.forward(request, response);
return;
}
}
chain.doFilter(request, response);
}
}
Then in web.xml:
<web-app>
<filter>
<filter-name>PathLocationStrategyFilter</filter-name>
<filter-class>mypackage.PathLocationStrategyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>PathLocationStrategyFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
</web-app>
These are the three steps you need to follow:
Implement your own TomcatEmbeddedServletContainerFactory bean and set up the RewriteValve
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
...
import org.apache.catalina.valves.rewrite.RewriteValve;
...
#Bean TomcatEmbeddedServletContainerFactory servletContainerFactory() {
TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
factory.setPort(8080);
factory.addContextValves(new RewriteValve());
return factory;
}
Add a rewrite.conf file to the WEB-INF directory of your application and specify the rewrite rules. Here is an example rewrite.conf content, which I'm using in the angular application to take advantage of the angular's PathLocationStrategy (basicly I just redirect everything to the index.html as we just use spring boot to serve the static web content, otherwise you need to filter your controllers out in the RewriteCond rule):
RewriteCond %{REQUEST_URI} !^.*\.(bmp|css|gif|htc|html?|ico|jpe?g|js|pdf|png|swf|txt|xml|svg|eot|woff|woff2|ttf|map)$
RewriteRule ^(.*)$ /index.html [L]
Get rid of the useHash (or set it to false) from your routing declarations:
RouterModule.forRoot(routes)
or
RouterModule.forRoot(routes, {useHash: false})
forward all Angular routing with index.html. Including base href.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
public class ViewController {
#RequestMapping({ "jsa/customer","jsa/customer/{id}",})
public String index() {
return "forward:/index.html";
}
}
In my case jsa is base href.
in my opinion the best way is to separate the User Interface paths and API paths by adding a prefix to them and serve the UI app entrypoint (index.html) for every path that matches UI prefix:
step 1 - add a prefix for all your UI paths (for example /app/page1, /app/page2, /app/page3, /app/page2/section01 and so on).
step 2 - copy UI files (HTML, JS, CSS, ...) into /resources/static/
step 3 - serve index.html for every path that begins with /app/ by a controller like this:
#Controller
public class SPAController {
#RequestMapping(value = "/app/**", method = RequestMethod.GET)
public ResponseEntity<String> defaultPath() {
try {
// Jar
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("/static/index.html");
// IDE
if (inputStream == null) {
inputStream = this.getClass().getResourceAsStream("/static/index.html");
}
String body = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(body);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error in redirecting to index");
}
}
#GetMapping(value = "/")
public String home(){
return "redirect:/app";
}
}

Spring MVC #PathVariable gives 404 error

I have been searching for the answer and many answers didn't solve my problem even though they solved very similar problems.
So My problem is this : I have a path variable which may contain character "/". The same value also contains other special characters such as "." "+" "=" etc .basically all valid Base64 characters.
But Spring MVC throws 404 with logs saying no handler found. I tried using regular expressions in path variable as well but to no avail. so below id my code snippets :
http://localhost:8080/sale/public/viewSaleDetails/b91a03730a746a2b27e1c7bbbd94ddf6a9df593301cd96c606348df5eed235da.FkJJbOqEM8Xvhffe6FwUdQ8/mMCD4+fxpY7w5L9kbJ8=
is my URL. If you see it has / in path variable value. along with "." and "+" and "=". Spring maps this correctly if I remove / between character "m" and "8". but with / in value it just doesnt work. I tried a lot of things including character encoding filter,regex in pathvariable etc. Please help.
Also I dont want to use request parameters as far as possible.
#RequestMapping(value = "/public/viewSaleDetails/{id}", method = RequestMethod.GET)
is my mapping. Also the url is hit from the browser as it is without any URL encoding. I tracked it on browser network bar and it doesnt encode it as expected. I am using Spring 4.2.8 RELEASE version with java 8 and tomcat 8
There is open issue in spring Jira according matching slashes in path. And due to discussion it is not reasonable to change mathing strategy on framework level. The issue was created due to this stackoverflow post and I suggest creating value resolver according to the answer.
Here is example code for such resolver:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
#SpringBootApplication
public class SampleSpringApp {
public static void main(String[] args) {
SpringApplication.run(SampleSpringApp.class, args);
}
}
#RestController
class SampleController {
#RequestMapping("/records/**")
public String getId(Id id) {
return id.id;
}
}
#Configuration
#EnableWebMvc
class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new IdResolver());
}
}
class IdResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return Id.class.isAssignableFrom(parameter.getParameterType());
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
String basePath = ((String) webRequest.getAttribute(
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST
)).replace("**", "");
String id = ((String) webRequest.getAttribute(
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST
)).replace(basePath, "");
return new Id(id);
}
}
class Id {
public final String id;
Id(String id) {
this.id = id;
}
}

SpringBoot Rest API custom authentication

I build a Rest Api using SpringBoot and the authentication I implemented using Firebase.
My problem right now is that I want to have control of the client applications that will access my application. The problem of using SpringSecurity is that as far as I know I have to do the authentication for it and I just want to "allow the client application."
Does anyone have any idea how to do?
Provide a unique key to your client. Which your microservice recognises and authenticates any request based on that key. This can be also given as a request parameter.
let say you add your key into a parameter called my-key, now before working on your logic inside you spring-boot app validate your key. like this -
your Rest Controller would look like this-
#RestController
class MyRest{
private static final String KEY = "someValue";
#RequestMapping("/some-mapping")
public #ResponseBody myMethod(#RequestParam(value="my-key", required=true) String key){
if(!validateRequest(key)){
//return error as response
}
System.out.println("Key Validation Successful!");
//here goes your logic
}
private boolean validateRequest(String key){
return key.equals(KEY);
}
}
in order to access this rest use - http://your-host:port/some-mapping?my-key=someValue
If you want to allow some of the clients to bypass the authentication, have a list of whitelisted IP addresses and check the IP of each incoming request. if the IP is in the list of whitelisted APIs, no need to authenticate.
Use HttpServletRequest.getRemoteAddr() to get the IP address.
Solution 1
Custom interceptor MyHandlerInterceptor.java:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MyHandlerInterceptor implements HandlerInterceptor {
private static final String YOUR_KEY = "KEY_VALUE";
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
String key = request.getHeader("X-Key");
boolean isValid = YOUR_KEY.equals(key);
if (!isValid) {
//invalid key
response.setStatus(401);
PrintWriter writer = response.getWriter();
writer.write("invalid key");
}
return isValid;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
}
Configure interceptor WebConfig.java:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyHandlerInterceptor());
}
}

Springboot/Angular2 - How to handle HTML5 urls?

I believe this is a simple question, but I couldn't find an answer or at least use the correct terms in the search.
I am setting up Angular2 and Springboot together. By default, Angular will use paths like localhost:8080\dashboard and localhost:8080\dashboard\detail.
I'd like to avoid using path as hashs, if possible. As Angular documentation states:
The router's provideRouter function sets the LocationStrategy to the PathLocationStrategy, making it the default strategy. We can switch to the HashLocationStrategy with an override during the bootstrapping process if we prefer it.
And then...
Almost all Angular 2 projects should use the default HTML 5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.
The issue is that when I try to access localhost:8080\dashboard, Spring will look for some controller mapping to this path, which it won't have.
Whitelabel Error Page
There was an unexpected error (type=Not Found, status=404).
No message available
I thought initially to make all my services to be under localhost:8080\api and all my static under localhost:8080\app. But how do I tell Spring to ignore requests to this app path?
Is there a better solution with either Angular2 or Boot?
In my Spring Boot applications (version 1 and 2), my static resources are at a single place :
src/main/resources/static
static being a folder recognized by Spring Boot to load static resources.
Then the idea is to customize the Spring MVC configuration.
The simpler way is using Spring Java configuration.
I implement WebMvcConfigurer to override addResourceHandlers().
I add in a single ResourceHandler to the current ResourceHandlerRegistry.
The handler is mapped on every request and I specify classpath:/static/ as resource location value (you may of course adding others if required).
I add a custom PathResourceResolver anonymous class to override getResource(String resourcePath, Resource location).
And the rule to return the resource is the following : if the resource exists and is readable (so it is a file), I return it. Otherwise, by default I return the index.html page. Which is the expected behavior to handle HTML 5 urls.
Spring Boot 1.X Application :
Extending org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter is the way. The class is an adapter of the WebMvcConfigurer interface
with empty methods allowing sub-classes to override only the methods they're interested in.
Here is the full code :
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.resource.PathResourceResolver;
#Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
Spring Boot 2.X Application :
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter was deprecated.
Implementing directly WebMvcConfigurer is the way now as it is still an interface but it has now default methods (made possible by a Java 8 baseline) and can be implemented directly without the need for the adapter.
Here is the full code :
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
EDIT to address some comments :
For those that store their static resources at another location as src/main/resources/static, change the value of the var args parameter of addResourcesLocations() consequently.
For example if you have static resources both in static and in the public folder (no tried) :
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/", "/public")
I have a solution for you, you can add a ViewController to forward requests to Angular from Spring boot.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
public class ViewController {
#RequestMapping({ "/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers" })
public String index() {
return "forward:/index.html";
}
}
here I have redirected all my angular2 ("/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers") to my SPA
You can do the same for your preject and you can also redirect your 404 error page to the index page as a further step.
Enjoy!
You can forward all not found resources to your main page by providing custom ErrorViewResolver. All you need to do is to add this to your #Configuration class:
#Bean
ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() {
return new ErrorViewResolver() {
#Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
return status == HttpStatus.NOT_FOUND
? new ModelAndView("index.html", Collections.<String, Object>emptyMap(), HttpStatus.OK)
: null;
}
};
}
You can forward everything not mapped to Angular using something like this:
#Controller
public class ForwardController {
#RequestMapping(value = "/**/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
Source: https://stackoverflow.com/a/44850886/3854385
My Spring Boot server for angular is also a gateway server with the API calls to /api to not have a login page in front of the angular pages, you can use something like.
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
/**
* This sets up basic authentication for the microservice, it is here to prevent
* massive screwups, many applications will require more secuity, some will require less
*/
#EnableOAuth2Sso
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Override
public void configure(HttpSecurity http) throws Exception {
http
.logout().logoutSuccessUrl("/").and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll().and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
To make it more simple you can just implement ErrorPageRegistrar directly..
#Component
public class ErrorPageConfig implements ErrorPageRegistrar {
#Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
}
}
This would forward the requests to index.html.
#Controller
#RequestMapping("/")
public class MainPageController {
#ResponseStatus(HttpStatus.OK)
#RequestMapping({ "/" })
public String forward() {
return "forward:/";
}
}
I did it with a plain old filter:
public class PathLocationStrategyFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if(request instanceof HttpServletRequest) {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String uri = servletRequest.getRequestURI();
String contextPath = servletRequest.getContextPath();
if(!uri.startsWith(contextPath + "/api") &&
!uri.startsWith(contextPath + "/assets") &&
!uri.equals(contextPath) &&
// only forward if there's no file extension (exclude *.js, *.css etc)
uri.matches("^([^.]+)$")) {
RequestDispatcher dispatcher = request.getRequestDispatcher("/");
dispatcher.forward(request, response);
return;
}
}
chain.doFilter(request, response);
}
}
Then in web.xml:
<web-app>
<filter>
<filter-name>PathLocationStrategyFilter</filter-name>
<filter-class>mypackage.PathLocationStrategyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>PathLocationStrategyFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
</web-app>
These are the three steps you need to follow:
Implement your own TomcatEmbeddedServletContainerFactory bean and set up the RewriteValve
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
...
import org.apache.catalina.valves.rewrite.RewriteValve;
...
#Bean TomcatEmbeddedServletContainerFactory servletContainerFactory() {
TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
factory.setPort(8080);
factory.addContextValves(new RewriteValve());
return factory;
}
Add a rewrite.conf file to the WEB-INF directory of your application and specify the rewrite rules. Here is an example rewrite.conf content, which I'm using in the angular application to take advantage of the angular's PathLocationStrategy (basicly I just redirect everything to the index.html as we just use spring boot to serve the static web content, otherwise you need to filter your controllers out in the RewriteCond rule):
RewriteCond %{REQUEST_URI} !^.*\.(bmp|css|gif|htc|html?|ico|jpe?g|js|pdf|png|swf|txt|xml|svg|eot|woff|woff2|ttf|map)$
RewriteRule ^(.*)$ /index.html [L]
Get rid of the useHash (or set it to false) from your routing declarations:
RouterModule.forRoot(routes)
or
RouterModule.forRoot(routes, {useHash: false})
forward all Angular routing with index.html. Including base href.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
public class ViewController {
#RequestMapping({ "jsa/customer","jsa/customer/{id}",})
public String index() {
return "forward:/index.html";
}
}
In my case jsa is base href.
in my opinion the best way is to separate the User Interface paths and API paths by adding a prefix to them and serve the UI app entrypoint (index.html) for every path that matches UI prefix:
step 1 - add a prefix for all your UI paths (for example /app/page1, /app/page2, /app/page3, /app/page2/section01 and so on).
step 2 - copy UI files (HTML, JS, CSS, ...) into /resources/static/
step 3 - serve index.html for every path that begins with /app/ by a controller like this:
#Controller
public class SPAController {
#RequestMapping(value = "/app/**", method = RequestMethod.GET)
public ResponseEntity<String> defaultPath() {
try {
// Jar
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("/static/index.html");
// IDE
if (inputStream == null) {
inputStream = this.getClass().getResourceAsStream("/static/index.html");
}
String body = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(body);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error in redirecting to index");
}
}
#GetMapping(value = "/")
public String home(){
return "redirect:/app";
}
}

Resources