Spring/Vaadin Security Integration

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.va4spsec01
Jar Archive der benötigten Unterprojekte
[vaadin-spring-ext-core] und
[vaadin-spring-ext-security] mit Run Maven Package erstellen
va4spsec02
Die erstellten Jar Dateien in den Libs Ordner des Spring Projekts kopieren. va4spsec03
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',
'libs/vaadin-spring-ext-security-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.va4spsec06

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
        }
    }
    ....
}

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.