This project implements a web application that allows a user to:
- register to the site by providing his/her personal info
- log-in to the site
- log-out of the site
- change his/her personal info
- change his/her password
The application also shows internationalization (i18n). The main challenge in this type of integration is how to define authorization for different pages and how to handle situations where a page is accessed with insufficient credentials, i.e. how to forward the user to a login page or similar.
Other projects have integrated Vaadin to Spring Security, notably these:
- http://morevaadin.com/content/spring-security-integration/
- http://vaadin.xpoft.ru/
Both of these alternatives have shortcomings that have been worked around in this project + this one is written in Scala.
The problem
As described in (1), Spring Security (SS) works by securing subcontexts (
myhost.com/public vs.
myhost.com/private) whereas Vaadin uses fragments for different views (
myhost.com/#!public vs.
myhost.com/#!private). Fragments are a client side part of the URI that is not sent to the server, so SS cannot distinguish between the last two URIs.
Normally SS handles authentication and authorization (a&a) with exception handlers in a filter chain, so if a user tries to access a page (s)he is not authenticated for, SS will throw an exception and an exception handler will forward the user to a login page. This is illustrated below:
-> user sends request
-> servlet invoked
-> exception filter entered
=> page generation (with a&a that throws exceptions)
-> exception filter catches a&a exception and sets up a login page forward
-> servlet returns a redirect message to the browser
-> user's browser goes to a login page (
that has a different URI i.e. subcontext)
If we want to implement all views (like the login and registration views that don't require authentication) and other views (that do require authentication and authorization) with Vaadin, we cannot use spring security's exception handling mechanism because the exception handling happens after the Vaadin page generation and needs to forward to another subcontext. If you try to forward to another fragment, you'll get a redirect loop:
The other projects mentioned above work around this by setting up a login page that is not a Vaadin view: The login page is implemented with
FreeMarker or some other technology where the page can have a distinct subcontext.
Implementation
To avoid using different subcontexts, the access management has to be done in the Vaadin
Navigator during the view generation. Here's a high level description of how this is done:
- When a user navigates to a view, that view is provided by the Spring application context
- The view object is defined with special annotations describing the roles the user must have in order to access the view
- If the user does not have the proper role, the access manager throws an exception that is caught in a custom implementation of the navigator, which will then provide a login view
- When the user provides his/her credentials, SS stores these credentials for the duration of the session (to be read by the access manager)
Spring integration with application context generated views
The integration of Spring to Vaadin starts with an extention of
VaadinServlet. This is implemented in
net.sf.vaadinturvaa.spring.SpringVaadinServlet. This implementation is pretty much copied from project (2) and loads the Spring application context as well as sets up a
UIProvider for the application with
net.sf.vaadinturvaa.spring.SpringUIProvider.
The Vaadin views are implemented in the
net.sf.vaadinturvaa.views package and are marked with the following annotations:
Authentication
When a user register on the site, (s)he creates an account with personal information and a password. The password is hashed and stored to the database. When the user logs in, the password entered on the login form is hashed and compared to the hash in the database. If they match, the user is authenticated and a token is stored in the
SecurityContextHolder:
SecurityContextHolder.getContext().setAuthentication(auth)
Custom Navigator and error handling
When a page is accessed in Vaadin, the
UI component defined for the
VaadinServlet in the web.xml is invoked. More specifically, the
Navigator configured in the
UI's
init method is invoked to access the appropriate view. The view instances are managed by Spring and returned by a custom implementation of Navigator. SS's
AccessDecisionManager is responsible for making sure that the user accessing the view has proper credentials and if not, will throw an appropriate exception.
These exceptions are handled in class
net.sf.vaadinturvaa.core.NavigatorFactory. This class returns Navigator instances that have a
net.sf.vaadinturvaa.core.NavigationErrorHandlingStrategy. This is defined as follows:
abstract class NavigationErrorHandlingStrategy {
def apply(navigationState: String, navigateTo: String => Unit)
}
The apply method takes the name of a navigation state and a function that takes the navigation state as parameter. This mirrors the way in which the Navigator object is invoked when navigating from one view to the next. The default implementation of this class is:
class DefaultNavigationErrorHandlingStrategy(val authenticationFailureView: String,
val accessDeniedUnauthenticatedView: String,
val accessDeniedAuthenticatedView: String)
extends NavigationErrorHandlingStrategy {
override def apply(navigationView: String, navigateTo: String => Unit) {
try {
navigateTo(navigationView)
}
catch {
case e: AuthenticationException => navigateTo(authenticationFailureView)
case e: AlreadyLoggedInException => navigateTo(accessDeniedAuthenticatedView)
case e: AccessDeniedException =>
if(AuthenticationHelper.areWeAuthenticated)
navigateTo(accessDeniedAuthenticatedView)
else navigateTo(accessDeniedUnauthenticatedView)
}
}
}
This handles three different errors: unauthenticated user, insufficient privileges and a special case in which the user tries to access the login page when already logged in.
The
NavigationErrorHandlingStrategy is used by the
ExceptionHandlingNavigator returned by the
NavigatorFactory:
private class ExceptionHandlingNavigator(ui: UI,
container: SingleComponentContainer,
errorHandling: NavigationErrorHandlingStrategy)
extends Navigator(ui, container) {
addProvider(viewProvider)
override def navigateTo(navigationState: String) {
errorHandling(navigationState, super.navigateTo(_))
}
}
The
viewProvider is an instance of
ViewProvider that gets the view instances from the Spring application context.
Internationalization
i18n is made of two parts: localized view labels and localized validation messages. View labels come from
ViewMessages_xx.properties files and validation messages come from
ValidationMessages_xx.properties files. View labels are made available to the application with a
MessageSource and all view implementations get access to it by extending the
net.sf.vaadinturvaa.views.MessageSourced trait. Validation messages are accessed directly by Vaadin's
BeanValidator.
Running the app
To run the app you need to
- start the H2 database as a separate process (./h2.sh -tcpAllowOthers -tcpPort 8043) or start it along with the rest of the application (see root-context.xml). You also need to place the accounts.h2.db file in your home directory (or modify the jdbc.properties file accordingly).
- build the app as a WAR file and start it in a Servlet engine or from the project directory with mvn jetty:run.
- go to http://localhost:9090/#!register and register away.