
Setting up Spring Security for Vaadin applications
Warning | There was a security issue in the first version of the tutorial: The navigation listener that checks permissions for router navigation was missing. This will allow attackers to manipulate your login view to trigger router navigation and access secured pages! Please, check your code if an UI listener is registered as explained here. |
After discussing the goals and setting up the project base, we can finally start with the actual work!
Enable Spring Security
First, we have to add the needed Spring Security dependencies to our POM:
pom.xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
Second, we have to disable Spring’s MVC error handler. Since Vaadin 14 it causes strange reload behavior. Check this forum thread for details. Thank you all, for fighting this bug down!
Application.java
@SpringBootApplication(exclude = ErrorMvcAutoConfiguration.class)
public class Application {
Third, we will add a Vaadin aware Spring Security configuration via the SecurityConfiguration
class that uses some helpers you can check in the sources.
SecurityConfiguration.java
/**
* Require login to access internal pages and configure login form.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// Not using Spring CSRF here to be able to use plain HTML for the login page
http.csrf().disable() // (1)
// Register our CustomRequestCache that saves unauthorized access attempts, so
// the user is redirected after login.
.requestCache().requestCache(new CustomRequestCache()) // (2)
// Restrict access to our application.
.and().authorizeRequests()
// Allow all flow internal requests.
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll() // (3)
// Allow all requests by logged in users.
.anyRequest().authenticated() // (4)
// Configure the login page.
.and().formLogin().loginPage(LOGIN_URL).permitAll() // (5)
.loginProcessingUrl(LOGIN_PROCESSING_URL) // (6)
.failureUrl(LOGIN_FAILURE_URL)
// Configure logout
.and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL);
}
Vaadin has built-in Cross-Site Request Forgery already.
We add a customized request cache to filter out framework internal request. Check
CustomRequestCache
implementation for details.Permits a set of Vaadin related request types (check
SecurityUtils
for details).Force authentication for all views.
Configure the URL to the login page for redirects and permit access to everyone.
Configure the login URL Spring Security is expecting POST requests to (form submit).
Next, we have to make sure that resources Vaadin needs are bypassed and not affected by our security configuration above:
SecurityConfiguration.java
/**
* Allows access to static resources, bypassing Spring security.
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
// Vaadin Flow static resources // (1)
"/VAADIN/**",
// the standard favicon URI
"/favicon.ico",
// the robots exclusion standard
"/robots.txt",
// web application manifest // (2)
"/manifest.webmanifest",
"/sw.js",
"/offline-page.html",
// (development mode) static resources // (3)
"/frontend/**",
// (development mode) webjars // (3)
"/webjars/**",
// (production mode) static resources // (4)
"/frontend-es5/**", "/frontend-es6/**");
}
Mandatory.
Needed only when developing a Progressive Web Application.
Allows access to frontend resources in development mode.
Grants access to all bundled resources. This is important for your login view (if a Polymer template needs to be accessed) or for every other public page.
Secure Router Navigation
Finally, we have to secure router navigation by registerring a before navigation listener that checks for permissions. This is very important as Spring Security is not aware of the single page application behavior of a Vaadin application. That means AJAX requests as they are done during navigation via router links are not protected by the default Spring Security filters.
ConfigureUIServiceInitListener.java
@Component // (1)
public class ConfigureUIServiceInitListener implements VaadinServiceInitListener { // (1)
@Override
public void serviceInit(ServiceInitEvent event) {
event.getSource().addUIInitListener(uiEvent -> {
final UI ui = uiEvent.getUI();
ui.addBeforeEnterListener(this::beforeEnter); // (2)
});
}
/**
* Reroutes the user if (s)he is not authorized to access the view.
*
* @param event
* before navigation event with event details
*/
private void beforeEnter(BeforeEnterEvent event) {
if (!LoginView.class.equals(event.getNavigationTarget()) // (3)
&& !SecurityUtils.isUserLoggedIn()) { // (4)
event.rerouteTo(LoginView.class); // (5)
}
}
}
Allows adding the navigation listener globally to all UI instances by using a service init listener. Spring takes care of registering it.
Adds the before enter listener.
Ignores the login view itself.
Only redirects if user is not logged in. See below.
Actual rerouting the login view if needed.
SecurityUtils.java
static boolean isUserLoggedIn() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // (1)
return authentication != null // (2)
&& !(authentication instanceof AnonymousAuthenticationToken) // (3)
&& authentication.isAuthenticated(); // (4)
}
Gets the authentication token from the security context.
Fail if no authentication is available.
Fail for anonymous authentication tokens. Spring Security will add this type of token if all other authentication mechanism failed by default.
Fail if the authentication token is available but is not authenticated.
Once again, run mvn spring-boot:run
to build and start the web application and notice the redirection to /login. So far, so good.
Comments (48)