Spring Boot with VueJS with Spring Security On
Configuring Spring Boot With Vue JS
Dan Vega has an wonderful article on configuring Vue on the frontend and Spring Boot on the backend using a monolithic approach, this design is suitable for a team of full-stack developers who will be working on both sides of the application.
I would recommend to go through the article first. https://www.danvega.dev/blog/full-stack-java.
What happens after adding Spring Security
After Integrating Spring Security into the application, by default, Spring Security will protect all the routes, which will lead to frontend path (/
) getting protected as well.
This means that any request made to the application require authentication, including requests to frontend which is not what we want.
Solution
So what can we do? Whitelisting the root (/
) path is not a good idea beacuse doing so will also expose the protected controller to unauthorized user. A more secure and recommended approach is to serve frontend from a different path, such as /app
and then whitelist the /app
path in Spring Security configuration to allow unauthenticated access to frontend.
Vue Configuration
So let's add the configuration in Vue application that will make the vue application be served from /app
path. In /frontend
folder edit vue.config.js
file
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
publicPath: '/app/',
devServer: {
client: {
overlay: false,
},
port: 3000,
proxy: {
'/api': {
target: 'http://192.168.43.28:8080',
ws: true,
changeOrigin: true
}
}
}
})
It will make the app to be served from a sub-path. For example, if the app is deployed at https://www.foobar.com/my-app/
, set publicPath
to '/my-app/'
.
Spring Boot configuration
As discussed earlier, by default, Spring Boot serves all static content under the root part of the request, /**
. We can change it via the spring.mvc.static-path-pattern
configuration property.
Change your application.properties
spring.mvc.static-path-pattern=/app/*
or if you are using yaml
configuration change your application.yaml or application.yml
spring:
mvc:
static-path-pattern: /app/**
Now that we have made the frontend to be served from "/app" sub-path, we need to whitelist the path.Made the following changes as per configuration.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {
private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**",
"/app/**",
};
private final JWTAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(req ->
req
.requestMatchers(WHITE_LIST_URL).permitAll()
.requestMatchers("/api/v1/user/**").hasAnyRole(ADMIN.name(),USER.name())
.requestMatchers("/api/v1/admin/**").hasAnyRole(ADMIN.name())
.anyRequest()
.authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
;
return http.build();
}
}
Your configuration file may be different, we just need to whitelist the /app
. Here is my configuration on gitlab.
🎉 we have done it.
But wait, try to refresh the frontend and it will most probably fail. Why this happens?
This is because our vuejs application is pure client side SPA(Single Page Application), there is only one index.html file, and every request is hitting that html file and all subsequent requests are handled by the client-side JavaScript code. When we refresh the page, the browser sends a request to the server for the requested URL, but since there is no file for that URL, the server returns a 404 error.This is why refreshing the page in a Vue.js SPA can result in a failure.
So, if we redirect every request coming to /app/**
we can forward it to index.html
file it should work. Create an FrontendForwardController for that.
@Controller
public class FrontendForwardController {
// TODO
// Better regex needed
@GetMapping({ "/app","/app/","/app/problems/**","/app/problems","/app/auth","/app/auth/**","/app/admin","/app/admin/**"})
public String forward() {
return "forward:/app/index.html";
}
}
The above controller forwarding some frontend request like /app
or /app/problems/**
to index.html
.
There is a problem though in this approach, whenever we add a new route to our frontend we need to allow it here at FrontendForwardController
to forward the request to index.html.
public class FrontendForwardController {
@GetMapping("/app/**")
public String forward(){
return "forward:/app/index.html";
}
}
We can't make the FrontendForwardController like above, because it will result in an endless recursion.
That's all. Happy coding Bijit