Whitelist endpoint in spring-webflux security without context root path - spring

I am trying to whitelist all the custom endpoints which are ending with /health but in WebFlux Security I'm unable to use a regular expression to match all end-points. e.g. /app/health, /app/meta/health, /app/custom/meta/health, /api/app/custom/meta/health, etc. I'm not finding any pattern (or regex) to whitelist these end-points with one entry while constructing SecurityWebFilterChain.
Also I get a warning if I try to use regex
spring-security '**' patterns are not supported in the middle of patterns and will be rejected in the future. Consider using '*' instead for matching a single path segment.
Below is the code snippet
String[] ALLOWED_PATTERNS = {".*/health"};
// String[] ALLOWED_PATTERNS = {"/*/health"}; // allows whitelist /app/health endpoint
// String[] ALLOWED_PATTERNS = {"/*/health", "/*/*/health"}; // need generic way to whitelist all
#Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers(ALLOWED_PATTERNS).permitAll()
.anyExchange().authenticated()
...
.build();
}
I could able to achieve the same in Spring Security (not reactive/web flux security) by providing antMatchers
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/**/health");
}
Note: this is not a spring actuator health endpoint but a custom health endpoint

You can write your own matcher.
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
#Slf4j
public class RegexRequestMatcher {
private static final Cache<String, Pattern> CACHE = CacheBuilder.newBuilder().build();
private static final ConcurrentMap<String, Pattern> PATTERNS = CACHE.asMap();
public static Mono<ServerWebExchangeMatcher.MatchResult> matches(String regex, ServerWebExchange exchange) {
return matches(regex, exchange.getRequest().getURI().getPath());
}
public static Mono<ServerWebExchangeMatcher.MatchResult> matches(String regex, String uri) {
Pattern pattern = getPattern(regex);
Matcher matcher = pattern.matcher(uri);
boolean matches = matcher.matches();
return Mono.just(matches)
.flatMap(m -> m ? ServerWebExchangeMatcher.MatchResult.match() : ServerWebExchangeMatcher.MatchResult.notMatch());
}
private static Pattern getPattern(String regex) {
return PATTERNS.computeIfAbsent(regex, r -> Pattern.compile(r));
}
}
Then instead of pathMatchers you can use your own matcher.
.matchers(exchange -> RegexRequestMatcher.matches(".*/health", exchange)).permitAll()

Related

I'm using Cognito + Spring security. There are any way to use authorization?

I'm using spring Security and cognito for authentication and authorization. I entered some custom roles via aws IAM and I would like to know if there was a method to grant controlled access to resources. On the web I found some that set the cognito:groups as a role and used that, but they use deprecated classes and methods on it. Is there any way to do this with the latest versions?
I tried to create a class:
package com.projectname.name.Configurations;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CognitoAccessTokenConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
public CognitoAccessTokenConverter() {
}
#Override
public AbstractAuthenticationToken convert(#NonNull final Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream
.concat(defaultGrantedAuthoritiesConverter.convert(jwt).stream(), extractResourceRoles(jwt).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities);
}
private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt) {
Collection<String> userRoles = jwt.getClaimAsStringList("cognito:groups");
//System.out.println("\n!!!!!!!!" +userRoles +"!!!!!!!!!!\n"); DEBUG
if (userRoles != null)
return userRoles
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
return Collections.emptySet();
}
}
/*
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.
#Component
public class CognitoAccessTokenConverter extends OAuth2AuthorizationCodeRequestAuthenticationToken{
private static final String COGNITO_GROUPS = "cognito:groups";
private static final String SPRING_AUTHORITIES = "authorities";
private static final String COGNITO_USERNAME = "username";
private static final String SPRING_USER_NAME = "user_name";
}
#Component
public class CognitoAccessTokenConverter extends {
// Note: This the core part.
private static final String COGNITO_GROUPS = "cognito:groups";
private static final String SPRING_AUTHORITIES = "authorities";
private static final String COGNITO_USERNAME = "username";
private static final String SPRING_USER_NAME = "user_name";
#SuppressWarnings("unchecked")
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
if (claims.containsKey(COGNITO_GROUPS))
((Map<String, Object>) claims).put(SPRING_AUTHORITIES, claims.get(COGNITO_GROUPS));
if (claims.containsKey(COGNITO_USERNAME))
((Map<String, Object>) claims).put(SPRING_USER_NAME, claims.get(COGNITO_USERNAME));
return super.extractAuthentication(claims);
}
} */
how can I use this conversion in my spring security configuration?
package com.SSDProject.Booked.Configurations;
import java.io.*;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin").hasAuthority("max")
.requestMatchers("/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login();
return http.build();
}
Help me, I tried to implements it and search everywhere. Some helps? Have you an idea?
I've recently created the same PoC using SpringBoot 2.x and Java 17.
In my case I don't have any deprecation warning from my IDE, here my example:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf()
.and()
.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.userDetailsService(null)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())));
return http.build();
}
private JwtAuthenticationConverter grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
String[] scopes;
if (jwt.getClaims().containsKey("cognito:groups")) {
scopes = ((JSONArray) jwt.getClaims().get("cognito:groups")).toArray(new String[0]);
} else {
scopes = ((String) jwt.getClaims().getOrDefault("scope", "")).split(" ");
}
return Arrays.stream(scopes)
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase(Locale.ROOT)))
.collect(Collectors.toSet());
}
);
return jwtAuthenticationConverter;
}
Exactly which line is deprecated in your code? And what version of resource-server are you using? For me spring-boot-starter-oauth2-resource-server is 2.7.5.
This is actually not an answer but I don't have the reputation for add comment to the question :)
Is your Spring application serving server-side rendered UI (Thymeleaf, JSF or alike) or is it a REST API (#RestController or #Controller with #ResponseBody)?
In second case, your app is a resource-server. OAuth2 login should be handled by clients, not resource-server: clients acquire access token and send it as Authorization header to resource-server.
In my answer to Use Keycloak Spring Adapter with Spring Boot 3, I explain how to configure both Spring resource-servers and clients. All you'll have to adapt for Cognito are issuer URI and the private-claims name to extract authorities from.
Configuring a resource-server with authorities mapped from cognito:groups using my starters (thin wrappers around spring-boot-starter-oauth2-resource-server) can be as simple as:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.10</version>
</dependency>
#Configuration
#EnableMethodSecurity
public class SecurityConfig {
}
com.c4-soft.springaddons.security.issuers[0].location=https://cognito-idp.Region.amazonaws.com/your user pool ID/.well-known/openid-configuration
com.c4-soft.springaddons.security.issuers[0].authorities.claims=cognito:groups
# This is probably too permissive but can be fine tuned (origins, headers and methods can be defined per path)
com.c4-soft.springaddons.security.cors[0].path=/**
If your application is only a client, my starters won't be of any help.
If your app is both a resource-server and a client (serves JSON payloads and server-side rendered UI with, for instance, Thymeleaf), then you'll have to define a second SecurityFilterChain bean. Details in the answer linked earlier.
If you don't want to use my starters, then you'll have to write quite some java conf. Details in the previously linked answer, again.

Spring Security: mapping OAuth2 claims with roles to secure Resource Server endpoints

I'm setting up a Resource Server with Spring Boot and to secure the endpoints I'm using OAuth2 provided by Spring Security. So I'm using the Spring Boot 2.1.8.RELEASE which for instance uses Spring Security 5.1.6.RELEASE.
As Authorization Server I'm using Keycloak. All processes between authentication, issuing access tokens and validation of the tokens in the Resource Server are working correctly. Here is an example of an issued and decoded token (with some parts are cut):
{
"jti": "5df54cac-8b06-4d36-b642-186bbd647fbf",
"exp": 1570048999,
"aud": [
"myservice",
"account"
],
"azp": "myservice",
"realm_access": {
"roles": [
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"myservice": {
"roles": [
"ROLE_user",
"ROLE_admin"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email offline_access microprofile-jwt profile address phone",
}
How can I configure Spring Security to use the information in the access token to provide conditional authorization for different endpoints?
Ultimately I want to write a controller like this:
#RestController
public class Controller {
#Secured("ROLE_user")
#GetMapping("userinfo")
public String userinfo() {
return "not too sensitive action";
}
#Secured("ROLE_admin")
#GetMapping("administration")
public String administration() {
return "TOOOO sensitive action";
}
}
After messing around a bit more, I was able to find a solution implementing a custom jwtAuthenticationConverter, which is able to append resource-specific roles to the authorities collection.
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new JwtAuthenticationConverter()
{
#Override
protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt)
{
Collection<GrantedAuthority> authorities = super.extractAuthorities(jwt);
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource = null;
Collection<String> resourceRoles = null;
if (resourceAccess != null &&
(resource = (Map<String, Object>) resourceAccess.get("my-resource-id")) !=
null && (resourceRoles = (Collection<String>) resource.get("roles")) != null)
authorities.addAll(resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet()));
return authorities;
}
});
Where my-resource-id is both the resource identifier as it appears in the resource_access claim and the value associated to the API in the ResourceServerSecurityConfigurer.
Notice that extractAuthorities is actually deprecated, so a more future-proof solution should be implementing a full-fledged converter
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
{
private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt, final String resourceId)
{
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource;
Collection<String> resourceRoles;
if (resourceAccess != null && (resource = (Map<String, Object>) resourceAccess.get(resourceId)) != null &&
(resourceRoles = (Collection<String>) resource.get("roles")) != null)
return resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet());
return Collections.emptySet();
}
private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final String resourceId;
public CustomJwtAuthenticationConverter(String resourceId)
{
this.resourceId = resourceId;
}
#Override
public AbstractAuthenticationToken convert(final Jwt source)
{
Collection<GrantedAuthority> authorities = Stream.concat(defaultGrantedAuthoritiesConverter.convert(source)
.stream(),
extractResourceRoles(source, resourceId).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(source, authorities);
}
}
I have tested both solutions using Spring Boot 2.1.9.RELEASE, Spring Security 5.2.0.RELEASE and an official Keycloak 7.0.0 Docker image.
Generally speaking, I suppose that whatever the actual Authorization Server (i.e. IdentityServer4, Keycloak...) this seems to be the proper place to convert claims into Spring Security grants.
Here is another solution
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
The difficulty you are experiencing is partly due to your roles being positioned in the JWT under resource_server->client_id. This then requires a custom token converter to extract them.
You can configure keycloak to use a client mapper that will present the roles under a top-level claim name such as "roles". This makes the Spring Security configuration simpler as you only need JwtGrantedAuthoritiesConverter with the authoritiesClaimName set as shown in the approach taken by #hillel_guy.
The keycloak client mapper would be configured like this:
As already mentioned by #hillel_guy's answer, using an AbstractHttpConfigurer should be the way to go. This worked seamlessly for me with spring-boot 2.3.4 and spring-security 5.3.4.
See the spring-security API documentation for reference: OAuth2ResourceServerConfigurer
UPDATE
Full example, as asked in the comments:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String JWT_ROLE_NAME = "roles";
private static final String ROLE_PREFIX = "ROLES_";
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and().csrf().disable()
.cors()
.and().oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
// create a custom JWT converter to map the roles from the token as granted authorities
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JWT_ROLE_NAME); // default is: scope, scp
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(ROLE_PREFIX ); // default is: SCOPE_
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
In my case, I wanted to map roles from the JWT instead of scope.
Hope this helps.
2022 update
I maintain a set of tutorials and samples to configure resource-servers security for:
both servlet and reactive applications
decoding JWTs and introspecting access-tokens
default or custom Authentication implementations
any OIDC authorization-server(s), including Keycloak of course (most samples support multiple realms / identity-providers)
The repo also contains a set of libs published on maven-central to:
mock OAuth2 identities during unit and integration tests (with authorities and any OpenID claim, including private ones)
configure resource-servers from properties file (including source claims for roles, roles prefix and case processing, CORS configuration, session-management, public routes and more)
Sample for a servlet with JWT decoder
#EnableMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig {}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/sample/**
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.3</version>
</dependency>
No, nothing more requried.
Unit-tests with mocked authentication
Secured #Component without http request (#Service, #Repository, etc.)
#Import({ SecurityConfig.class, SecretRepo.class })
#AutoConfigureAddonsSecurity
class SecretRepoTest {
// auto-wire tested component
#Autowired
SecretRepo secretRepo;
#Test
void whenNotAuthenticatedThenThrows() {
// call tested components methods directly (do not use MockMvc nor WebTestClient)
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenAuthenticatedAsSomeoneElseThenThrows() {
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithSameUsernameThenReturns() {
assertEquals("Don't ever tell it", secretRepo.findSecretByUsername("ch4mpy"));
}
}
Secured #Controller (sample for #WebMvcTest but works for #WebfluxTest too)
#WebMvcTest(GreetingController.class) // Use WebFluxTest or WebMvcTest
#AutoConfigureAddonsWebSecurity // If your web-security depends on it, setup spring-addons security
#Import({ SecurityConfig.class }) // Import your web-security configuration
class GreetingControllerAnnotatedTest {
// Mock controller injected dependencies
#MockBean
private MessageService messageService;
#Autowired
MockMvcSupport api;
#BeforeEach
public void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final JwtAuthenticationToken auth = invocation.getArgument(0, JwtAuthenticationToken.class);
return String.format("Hello %s! You are granted with %s.", auth.getName(), auth.getAuthorities());
});
when(messageService.getSecret()).thenReturn("Secret message");
}
#Test
void greetWitoutAuthentication() throws Exception {
api.get("/greet").andExpect(status().isUnauthorized());
}
#Test
#WithMockAuthentication(authType = JwtAuthenticationToken.class, principalType = Jwt.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
void greetWithDefaultMockAuthentication() throws Exception {
api.get("/greet").andExpect(content().string("Hello user! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
}
Advanced use-cases
The most advanced tutorial demoes how to define a custom Authentication implementation to parse (and expose to java code) any private claim into things that are security related but not roles (in the sample it's grant delegation between users).
It also shows how to extend spring-security SpEL to build a DSL like:
#GetMapping("greet/on-behalf-of/{username}")
#PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(#PathVariable("username") String username) {
return ...;
}
If you are using Azure AD Oath there is a much easier way now:
http
.cors()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new AADJwtBearerTokenAuthenticationConverter("roles", "ROLE_"));
The ADDJwtBearerTokenAuthenticationConverter allows you to add your claim name as the first argument and what you want your role prefixed with as the second argument.
My import so you can find the library:
import com.azure.spring.aad.webapi.AADJwtBearerTokenAuthenticationConverter;

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";
}
}

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