Installation
Spring und Vaadin haben jeweils eigene Security Plugins. Eine offizielle Integration gibt es noch nicht, aber auf der Vaadin4Spring Github Seite gibt es eine funktionierende Lösung in zwei Varianten:
- Shared Security verwendet Spring zur Absicherung der Vaadin GUIs. Der Vorteil ist, dass alle Funktionen von Spring zur Verfügung stehen, insbesondere auch für APIs der Webapp, die nicht über Vaadin gehen (z.B. REST, Spring Thymeleaf Pages usw.). Nachteil ist, dass Vaadins Push Service nicht genutzt werden kann.
- Managed Security verwendet die Vaadin Security Implementierung und ist daher primär für reine Spring-Vaadin Webapps geeignet.
In unserem Beispielprojekt verwenden wir Shared Security.
Da diese Erweiterung nicht mit gradle aus MavenCentral oder einem anderen zentralen Archiv im Internet geladen werden kann, erfordert die Installation einige Schritte. Als IDE wird weiterhin IntelliJ IDEA verwendet.
Repository von https://github.com/peholmst/vaadin4spring clonen. | ![]() |
Jar Archive der benötigten Unterprojekte [vaadin-spring-ext-core] und [vaadin-spring-ext-security] mit Run Maven Package erstellen | ![]() |
Die erstellten Jar Dateien in den Libs Ordner des Spring Projekts kopieren. | ![]() |
Die Jar Archive in die build.groovy einbinden. Dabei können entweder alle Dateinamen aufgelistet werden,compile files('libs/vaadin-spring-ext-core-0.0.7-SNAPSHOT.jar',oder es werden alle Jar Dateien mit *.jar eingebunden compile files('libs/*.jar') |
|
In den IntelliJ Module Settings den libs Ordner als Library hinzufügen, damit die Dateien zur Laufzeit gefunden werden. | ![]() |
Konfiguration
Jetzt werden die benötigten Spring Beans mit Hilfe einer Konfigurationsklasse bereitgestellt. Die Klasse SecurityConfiguration wird im Basisverzeichnis der Applikation abgelegt und dadurch von Spring automatisch gefunden. Wichtig ist, mit der @Import
Annotation (Zeile 9) die Konfigurationsklassen aus vaadin4spring mit einzubeziehen. Durch Auswahl von VaadinSharedSecurityConfiguration
oder VaadinManagedSecurityConfiguration
wird die entsprechende Security Implementierung aktiviert. In den Zeilen 17 – 19 werden zum Testen erst einmal zwei Benutzer angelegt. Das werden wir später durch eine datenbankbasierte Benutzerverwaltung ersetzen. In Zeile 37 wird die URL der Login-Seite festgelegt, auf die nicht authentifizierte Benutzer weitergeleitet werden. Zeile 65 konfiguriert das redirect nach erfolgreichem Login.
/** * Configure Spring Security. Adapted from Vaadin4Spring security sample * @author Petter Holmström (petter@vaadin.com) * @author Georg Beier * */ @Configuration @EnableWebSecurity @Import({VaadinExtensionsConfiguration.class, VaadinSharedSecurityConfiguration.class}) @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true) @EnableVaadinSharedSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("user").roles("USER") .and() .withUser("admin").password("admin").roles("ADMIN"); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); // Use Vaadin's built-in CSRF protection instead http.authorizeRequests() .antMatchers("/login/**").anonymous() .antMatchers("/vaadinServlet/UIDL/**").permitAll() .antMatchers("/vaadinServlet/HEARTBEAT/**").permitAll() .anyRequest().authenticated(); http.httpBasic().disable(); http.formLogin().disable(); http.logout() .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll(); http.exceptionHandling() .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")); http.rememberMe().rememberMeServices(rememberMeServices()).key("myAppKey"); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/VAADIN/**"); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public RememberMeServices rememberMeServices() { // Is there some way of exposing the RememberMeServices instance that the remember me // configurer creates by default? TokenBasedRememberMeServices services = new TokenBasedRememberMeServices("myAppKey", userDetailsService()); services.setAlwaysRemember(true); return services; } @Bean(name = VaadinSharedSecurityConfiguration.VAADIN_AUTHENTICATION_SUCCESS_HANDLER_BEAN) VaadinAuthenticationSuccessHandler vaadinAuthenticationSuccessHandler( HttpService httpService, VaadinRedirectStrategy vaadinRedirectStrategy) { return new VaadinUrlAuthenticationSuccessHandler(httpService, vaadinRedirectStrategy, "/"); } }
Nach Aktivierung von Spring Security in gradle.config
compile("org.springframework.boot:spring-boot-starter-security")
wird man beim Zugriff automatisch auf die Login-Seite umgeleitet.
Login Seite
Ein Beispiel für eine Login-Seite ist auf Petter Holmströms GitHub Site Vaadin4Spring (LoginUI.java) zu finden. Diese Seite lässt sich mit dem VaadinBuilder leicht in Groovy nachbauen. Die eigentliche Login-Funktionalität wird in Zeile 52f. an die vaadinSecurity Bean delegiert.
/** * a simple login page * Created by georg beier on 17.12.2015. */ @SpringUI(path = 'login') @Theme(ValoTheme.THEME_NAME) class LoginView extends UI { @Autowired private VaadinSecurity vaadinSecurity private VaadinBuilder loginBuilder private widgets = [:] @Override protected void init(VaadinRequest request) { setContent(initLayout()) } private Component initLayout() { loginBuilder = new VaadinBuilder() Component root root = loginBuilder."$C.vlayout"('rootLayout', [uikey : 'root', alignment: Alignment.TOP_CENTER]) { "$C.vlayout"([uikey : 'loginLayout', sizeUndefined: null, alignment: Alignment.MIDDLE_CENTER]) { "$F.label"([uikey : 'loginFailedLabel', styleName: ValoTheme.LABEL_FAILURE, visible : false, sizeUndefined: null, alignment: Alignment.BOTTOM_CENTER]) "$F.label"([uikey : 'loggedOutLabel', styleName: ValoTheme.LABEL_SUCCESS, visible : false, sizeUndefined: null, alignment: Alignment.BOTTOM_CENTER]) "$C.formlayout"([uikey : 'form', sizeUndefined: null, alignment: Alignment.TOP_CENTER]) { "$F.text"('Username', [uikey: 'userName']) "$F.password"('Password', [uikey: 'password']) "$F.checkbox"('Remember me', [uikey: 'rememberMe']) "$F.button"('Login', [uikey : 'loginButton', styleName : ValoTheme.BUTTON_PRIMARY, disableOnClick: true, clickShortcut : ShortcutAction.KeyCode.ENTER, clickListener : { login(it) }]) } } } widgets = loginBuilder.uiComponents root } private void login(def event) { try { vaadinSecurity.login(widgets['userName'].value, widgets['password'].value, widgets['rememberMe'].value); Notification.show('Login successful', "user ${widgets['userName'].value}", Notification.Type.HUMANIZED_MESSAGE) } catch (AuthenticationException ex) { widgets['userName'].focus(); widgets['userName'].selectAll(); widgets['password'].setValue(""); widgets['loginFailedLabel'].setValue(String.format("Login failed: %s", ex.getMessage())); widgets['loginFailedLabel'].setVisible(true); if (widgets['loggedOutLabel'] != null) { widgets['loggedOutLabel'].setVisible(false); } } catch (Exception ex) { Notification.show("An unexpected error occurred", ex.getMessage(), Notification.Type.ERROR_MESSAGE); log.error("Unexpected error while logging in", ex); } finally { widgets['loginButton'].enabled = true } } }
Logout
Die Logout URL ist in Zeile 33 in der Konfiguration festgelegt. Zum Logout kann man also …/logout in die Adresszeile des Browsers eingeben (unschön!), einen entsprechenden Link auf der Webseite unterbringen (schon besser) oder ein Menu oder Button dafür vorsehen. Das Beispiel zeigt, wie mit dem VaadinBuilder ein Vaadin MenuBar mit dem entsprechenden Logout Element angelegt wird. Die command-Closure in Zeile 5 ruft direkt die logout-Methode der vaadinSecurity Bean auf.
@Override Component build() { vaadin."$C.vlayout"() { "$F.menubar"([uikey: MENU]){ "$F.menuitem" ('Logout', [command: {vaadinSecurity.logout()}]) } "$C.panel"('Projekte', [spacing: true, margin: true]) { // Caption shows on the tab "$F.tree"('Projekte, Backlogs und Sprints', [uikey: PTREE, caption: 'MenuTree']) } } }
Ressourcen schützen
Grundsätzlich können Ressourcen durch Spring Security programmatisch durch Abfrage der Informationen über den aktuellen Benutzer und seine Rechte und deklarativ über Annotationen an Methoden von Spring Beans geschützt werden. Hier sollen nur einfache Beispiele beschrieben werden, die im Beispielprojekt verwendet werden. Zu Spring Security gibt es neben der ausführlichen Referenzdokumentation (auch im pdf Format) auch viele Tutorials im Internet, z.B. bei WebSystique.
Secured Methods
In unserem Beispielprojekt soll erreicht werden, dass nur Administratoren Projekte bearbeiten oder neu anlegen können. Dafür bekommt die entsprechende Methode im ProjectService eine Annotation:
@PreAuthorize("hasRole('ROLE_ADMIN')") public ProjectDto.QFull createOrUpdateProject(CSet command) { ....
Wenn die Bedingung nicht gegeben ist, wird eine Exception geworfen. Eine gute GUI sollte diesen Methodenaufruf gar nicht zulassen, aber sicher ist sicher. Daher wird die Exception vorsichtshalber in der UI-Klasse ScrumView abgefangen:
protected void init(VaadinRequest request) { page.title = 'spring-vaadin-groovy demo' setContent(initBuilder()) initComponents() errorHandler = { handleError(it) } } .... private void handleError(def event){ if (SecurityExceptionUtils.isAccessDeniedException(event.getThrowable())) { Notification.show("Sorry, you don't have access to do that."); } else { Notification.show("Something went wrong: $event"); } }
Mit Annotationen kann darüber hinaus auch nach dem Methodenaufruf eine Exception ausgelöst, eine Ergebnisliste der Methode gefiltert werden und vieles mehr.
Zugriff auf Benutzerinformationen
Nur für die Benutzerrolle zulässige Elemente werden sichtbar gemacht:
"$C.hlayout"([uikey : 'buttonfield', spacing: true, gridPosition: [0, 3, 1, 3]]) { "$F.button"('New', [uikey : 'newbutton', visible : authorizationService.hasRole('ROLE_ADMIN'), disableOnClick: true, clickListener : { sm.execute(Event.Create) }]) "$F.button"('Edit', [uikey : 'editbutton', visible : authorizationService.hasRole('ROLE_ADMIN'), disableOnClick: true, clickListener : { sm.execute(Event.Edit) }])
Nicht zulässige Operationen können abgefangen werden. Besser ist es natürlich, den Benutzer gar nichts verbotenes machen zu lassen!
protected createemptymode() { authorizationService.roles def user = authorizationService.user if (authorizationService.hasRole('ROLE_ADMIN')) { super.createemptymode() } else { Notification.show("Sorry, you don't have the rights to do that."); newButton.enabled = true sm.currentState } }
Ein Service als Spring Bean erleichtert den etwas umständlichen Zugriff auf die Spring Security Objekte. Damit können die für die jeweilige App notwendigen Autorisierungsfunktionen bequem bereitgestellt werden.
@Service class AuthorizationService implements IAuthorizationService{ @Override boolean hasRole(String role) { Collection authorities = SecurityContextHolder.context.authentication.authorities authorities.any {GrantedAuthority au -> au.authority == role } } .... }