diff --git a/pom.xml b/pom.xml
index 16b6775..8d09937 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,12 @@
org.springframework.security
spring-security-config
+
+
+ org.ilay
+ ilay
+ 3.0-Final
+
diff --git a/src/main/java/org/vaadin/paul/spring/MainView.java b/src/main/java/org/vaadin/paul/spring/MainView.java
index e514b43..aeac080 100644
--- a/src/main/java/org/vaadin/paul/spring/MainView.java
+++ b/src/main/java/org/vaadin/paul/spring/MainView.java
@@ -2,20 +2,25 @@
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
-import org.springframework.beans.factory.annotation.Autowired;
-
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.dom.Element;
+import com.vaadin.flow.dom.ElementFactory;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.PWA;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.annotation.Secured;
@Route
@PWA(name = "Project Base for Vaadin Flow with Spring", shortName = "Project Base")
public class MainView extends VerticalLayout {
-
public MainView(@Autowired MessageBean bean) {
Button button = new Button("Click me",
e -> Notification.show(bean.getMessage()));
add(button);
+
+ // simple link to the logout endpoint provided by Spring Security
+ Element logoutLink = ElementFactory.createAnchor("logout", "Logout");
+ getElement().appendChild(logoutLink);
}
}
diff --git a/src/main/java/org/vaadin/paul/spring/MessageBean.java b/src/main/java/org/vaadin/paul/spring/MessageBean.java
index 3fd9b32..9332c88 100644
--- a/src/main/java/org/vaadin/paul/spring/MessageBean.java
+++ b/src/main/java/org/vaadin/paul/spring/MessageBean.java
@@ -5,7 +5,6 @@
@Service
public class MessageBean {
-
public String getMessage() {
return "Button was clicked at " + LocalTime.now();
}
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/ConfigureUIServiceInitListener.java b/src/main/java/org/vaadin/paul/spring/app/security/ConfigureUIServiceInitListener.java
deleted file mode 100644
index ae1f1a0..0000000
--- a/src/main/java/org/vaadin/paul/spring/app/security/ConfigureUIServiceInitListener.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.vaadin.paul.spring.app.security;
-
-import com.vaadin.flow.component.UI;
-import com.vaadin.flow.router.BeforeEnterEvent;
-import com.vaadin.flow.server.ServiceInitEvent;
-import com.vaadin.flow.server.VaadinServiceInitListener;
-import org.springframework.stereotype.Component;
-import org.vaadin.paul.spring.ui.views.LoginView;
-
-@Component
-public class ConfigureUIServiceInitListener implements VaadinServiceInitListener {
-
- @Override
- public void serviceInit(ServiceInitEvent event) {
- event.getSource().addUIInitListener(uiEvent -> {
- final UI ui = uiEvent.getUI();
- ui.addBeforeEnterListener(this::beforeEnter);
- });
- }
-
- /**
- * 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())
- && !SecurityUtils.isUserLoggedIn()) {
- event.rerouteTo(LoginView.class);
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/CustomRequestCache.java b/src/main/java/org/vaadin/paul/spring/app/security/CustomRequestCache.java
index 31576f9..a1483c1 100644
--- a/src/main/java/org/vaadin/paul/spring/app/security/CustomRequestCache.java
+++ b/src/main/java/org/vaadin/paul/spring/app/security/CustomRequestCache.java
@@ -1,6 +1,11 @@
package org.vaadin.paul.spring.app.security;
+import com.vaadin.flow.server.VaadinServletRequest;
+import com.vaadin.flow.server.VaadinServletResponse;
+import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.SavedRequest;
+import org.vaadin.paul.spring.ui.views.LoginView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -8,7 +13,7 @@
/**
* HttpSessionRequestCache that avoids saving internal framework requests.
*/
-class CustomRequestCache extends HttpSessionRequestCache {
+public class CustomRequestCache extends HttpSessionRequestCache {
/**
* {@inheritDoc}
*
@@ -24,4 +29,22 @@ public void saveRequest(HttpServletRequest request, HttpServletResponse response
}
}
+ /**
+ * Unfortunately, it's not that easy to resolve the redirect URL from the saved request. But with some
+ * casting (we always use {@link DefaultSavedRequest}) and mangling we are able to get the request URI.
+ */
+ public String resolveRedirectUrl() {
+ SavedRequest savedRequest = getRequest(VaadinServletRequest.getCurrent().getHttpServletRequest(), VaadinServletResponse.getCurrent().getHttpServletResponse());
+ if(savedRequest instanceof DefaultSavedRequest) {
+ final String requestURI = ((DefaultSavedRequest) savedRequest).getRequestURI();
+ // check for valid URI and prevent redirecting to the login view
+ if (requestURI != null && !requestURI.isEmpty() && !requestURI.contains(LoginView.ROUTE)) {
+ return requestURI.startsWith("/") ? requestURI.substring(1) : requestURI;
+ }
+ }
+
+ // if everything fails, redirect to the main view
+ return "";
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/NameBasedEvaluator.java b/src/main/java/org/vaadin/paul/spring/app/security/NameBasedEvaluator.java
new file mode 100644
index 0000000..282669c
--- /dev/null
+++ b/src/main/java/org/vaadin/paul/spring/app/security/NameBasedEvaluator.java
@@ -0,0 +1,24 @@
+package org.vaadin.paul.spring.app.security;
+
+import com.vaadin.flow.router.Location;
+import com.vaadin.flow.router.NotFoundException;
+import org.ilay.Access;
+import org.ilay.AccessEvaluator;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+
+public class NameBasedEvaluator implements AccessEvaluator {
+ @Override
+ public Access evaluate(Location location, Class navigationTarget, SecuredForPaul annotation) {
+ if(SecurityUtils.isUserLoggedIn()) {
+ final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ final UserDetails principal = (UserDetails)authentication.getPrincipal();
+ if(principal.getUsername().equals("paul")) {
+ return Access.granted();
+ }
+ }
+
+ return Access.restricted(NotFoundException.class);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/RoleBasedEvaluator.java b/src/main/java/org/vaadin/paul/spring/app/security/RoleBasedEvaluator.java
new file mode 100644
index 0000000..f58787a
--- /dev/null
+++ b/src/main/java/org/vaadin/paul/spring/app/security/RoleBasedEvaluator.java
@@ -0,0 +1,22 @@
+package org.vaadin.paul.spring.app.security;
+
+import com.vaadin.flow.router.Location;
+import com.vaadin.flow.router.NotFoundException;
+import org.ilay.Access;
+import org.ilay.AccessEvaluator;
+import org.vaadin.paul.spring.ui.views.LoginView;
+
+public class RoleBasedEvaluator implements AccessEvaluator {
+ @Override
+ public Access evaluate(Location location, Class navigationTarget, SecuredByRole annotation) {
+ if(!SecurityUtils.isAccessGranted(navigationTarget, annotation)) {
+ if(SecurityUtils.isUserLoggedIn()) {
+ return Access.restricted(NotFoundException.class);
+ } else {
+ return Access.restricted(LoginView.ROUTE);
+ }
+ }
+
+ return Access.granted();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/SecuredByRole.java b/src/main/java/org/vaadin/paul/spring/app/security/SecuredByRole.java
new file mode 100644
index 0000000..ebe5557
--- /dev/null
+++ b/src/main/java/org/vaadin/paul/spring/app/security/SecuredByRole.java
@@ -0,0 +1,12 @@
+package org.vaadin.paul.spring.app.security;
+
+import org.ilay.NavigationAnnotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@NavigationAnnotation(RoleBasedEvaluator.class)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SecuredByRole {
+ String value() default "";
+}
\ No newline at end of file
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/SecuredForPaul.java b/src/main/java/org/vaadin/paul/spring/app/security/SecuredForPaul.java
new file mode 100644
index 0000000..ad93c40
--- /dev/null
+++ b/src/main/java/org/vaadin/paul/spring/app/security/SecuredForPaul.java
@@ -0,0 +1,11 @@
+package org.vaadin.paul.spring.app.security;
+
+import org.ilay.NavigationAnnotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@NavigationAnnotation(NameBasedEvaluator.class)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SecuredForPaul {
+}
\ No newline at end of file
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/SecurityConfiguration.java b/src/main/java/org/vaadin/paul/spring/app/security/SecurityConfiguration.java
index ba60333..ee7bc80 100644
--- a/src/main/java/org/vaadin/paul/spring/app/security/SecurityConfiguration.java
+++ b/src/main/java/org/vaadin/paul/spring/app/security/SecurityConfiguration.java
@@ -2,6 +2,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -10,6 +11,7 @@
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.vaadin.paul.spring.ui.views.LoginView;
/**
* Configures spring security, doing the following:
@@ -22,10 +24,18 @@
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
- private static final String LOGIN_PROCESSING_URL = "/login";
- private static final String LOGIN_FAILURE_URL = "/login";
- private static final String LOGIN_URL = "/login";
- private static final String LOGOUT_SUCCESS_URL = "/login";
+ private static final String LOGOUT_SUCCESS_URL = "/";
+
+ @Bean
+ @Override
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+
+ @Bean
+ public CustomRequestCache requestCache() {
+ return new CustomRequestCache();
+ }
/**
* Require login to access internal pages and configure login form.
@@ -37,7 +47,7 @@ protected void configure(HttpSecurity http) throws Exception {
// Register our CustomRequestCache, that saves unauthorized access attempts, so
// the user is redirected after login.
- .requestCache().requestCache(new CustomRequestCache())
+ .requestCache().requestCache(requestCache())
// Restrict access to our application.
.and().authorizeRequests()
@@ -49,8 +59,7 @@ protected void configure(HttpSecurity http) throws Exception {
.anyRequest().authenticated()
// Configure the login page.
- .and().formLogin().loginPage(LOGIN_URL).permitAll().loginProcessingUrl(LOGIN_PROCESSING_URL)
- .failureUrl(LOGIN_FAILURE_URL)
+ .and().formLogin().loginPage("/" + LoginView.ROUTE).permitAll()
// Configure logout
.and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL);
@@ -59,13 +68,28 @@ protected void configure(HttpSecurity http) throws Exception {
@Bean
@Override
public UserDetailsService userDetailsService() {
- UserDetails user =
+ // typical logged in user with some privileges
+ UserDetails normalUser =
User.withUsername("user")
.password("{noop}password")
- .roles("USER")
+ .roles("User")
+ .build();
+
+ // admin user with all privileges
+ UserDetails adminUser =
+ User.withUsername("admin")
+ .password("{noop}password")
+ .roles("User", "Admin")
+ .build();
+
+ // admin user with all privileges
+ UserDetails paulUser =
+ User.withUsername("paul")
+ .password("{noop}password")
+ .roles("User")
.build();
- return new InMemoryUserDetailsManager(user);
+ return new InMemoryUserDetailsManager(normalUser, adminUser, paulUser);
}
/**
diff --git a/src/main/java/org/vaadin/paul/spring/app/security/SecurityUtils.java b/src/main/java/org/vaadin/paul/spring/app/security/SecurityUtils.java
index a91ca00..d7810bc 100644
--- a/src/main/java/org/vaadin/paul/spring/app/security/SecurityUtils.java
+++ b/src/main/java/org/vaadin/paul/spring/app/security/SecurityUtils.java
@@ -4,9 +4,12 @@
import com.vaadin.flow.shared.ApplicationConstants;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.http.HttpServletRequest;
+import java.util.Arrays;
+import java.util.List;
import java.util.stream.Stream;
/**
@@ -39,10 +42,26 @@ static boolean isFrameworkInternalRequest(HttpServletRequest request) {
* Tests if some user is authenticated. As Spring Security always will create an {@link AnonymousAuthenticationToken}
* we have to ignore those tokens explicitly.
*/
- static boolean isUserLoggedIn() {
+ public static boolean isUserLoggedIn() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null
&& !(authentication instanceof AnonymousAuthenticationToken)
&& authentication.isAuthenticated();
}
+
+ /**
+ * Checks if access is granted for the current user for the given secured view,
+ * defined by the view class.
+ *
+ * @param securedClass View class
+ * @param annotation
+ * @return true if access is granted, false otherwise.
+ */
+ public static boolean isAccessGranted(Class> securedClass, SecuredByRole annotation) {
+ // lookup needed role in user roles
+ final List allowedRoles = Arrays.asList(annotation.value());
+ final Authentication userAuthentication = SecurityContextHolder.getContext().getAuthentication();
+ return userAuthentication.getAuthorities().stream().map(GrantedAuthority::getAuthority)
+ .anyMatch(allowedRoles::contains);
+ }
}
diff --git a/src/main/java/org/vaadin/paul/spring/ui/views/AdminView.java b/src/main/java/org/vaadin/paul/spring/ui/views/AdminView.java
new file mode 100644
index 0000000..71e12b5
--- /dev/null
+++ b/src/main/java/org/vaadin/paul/spring/ui/views/AdminView.java
@@ -0,0 +1,18 @@
+package org.vaadin.paul.spring.ui.views;
+
+import com.vaadin.flow.component.html.Label;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.Route;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.vaadin.paul.spring.app.security.SecuredByRole;
+
+@Route("admin")
+@SecuredByRole("ROLE_Admin")
+public class AdminView extends VerticalLayout {
+ @Autowired
+ public AdminView() {
+ Label label = new Label("Looks like you are admin!");
+ add(label);
+ }
+
+}
diff --git a/src/main/java/org/vaadin/paul/spring/ui/views/LoginView.java b/src/main/java/org/vaadin/paul/spring/ui/views/LoginView.java
index b3c71a9..b7492cb 100644
--- a/src/main/java/org/vaadin/paul/spring/ui/views/LoginView.java
+++ b/src/main/java/org/vaadin/paul/spring/ui/views/LoginView.java
@@ -1,31 +1,62 @@
package org.vaadin.paul.spring.ui.views;
-import com.vaadin.flow.component.AttachEvent;
-import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Tag;
-import com.vaadin.flow.component.dependency.HtmlImport;
-import com.vaadin.flow.component.page.Viewport;
-import com.vaadin.flow.component.polymertemplate.PolymerTemplate;
-import com.vaadin.flow.router.AfterNavigationEvent;
-import com.vaadin.flow.router.AfterNavigationObserver;
+import com.vaadin.flow.component.UI;
+import com.vaadin.flow.component.login.LoginOverlay;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
-import com.vaadin.flow.server.InitialPageSettings;
-import com.vaadin.flow.server.PageConfigurator;
-import com.vaadin.flow.templatemodel.TemplateModel;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.vaadin.paul.spring.app.security.CustomRequestCache;
@Tag("sa-login-view")
@Route(value = LoginView.ROUTE)
@PageTitle("Login")
-public class LoginView extends Component {
+public class LoginView extends VerticalLayout {
public static final String ROUTE = "login";
- public interface Model extends TemplateModel {
- void setError(boolean error);
- }
+ private LoginOverlay login = new LoginOverlay();
+
+ @Autowired
+ public LoginView(AuthenticationManager authenticationManager,
+ CustomRequestCache requestCache) {
+ // configures login dialog and adds it to the main view
+ login.setOpened(true);
+ login.setTitle("Spring Secured Vaadin");
+ login.setDescription("Login Overlay Example");
+
+ add(login);
+
+ login.addLoginListener(e -> {
+ try {
+ // try to authenticate with given credentials, should always return !null or throw an {@link AuthenticationException}
+ final Authentication authentication = authenticationManager
+ .authenticate(new UsernamePasswordAuthenticationToken(e.getUsername(), e.getPassword()));
+
+ // if authentication was successful we will update the security context and redirect to the page requested first
+ if(authentication != null ) {
+ login.close();
+ SecurityContextHolder.getContext().setAuthentication(authentication);
- @Override
- protected void onAttach(AttachEvent attachEvent) {
- getElement().setText("Do you wonder why you do not see any login UI? Well, this is the master branch only containing the security setup. For different UI implementations check the other branches :)");
+ //Access to view by role
+ if (authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).anyMatch(role -> role.equals("ROLE_Admin"))) {
+ UI.getCurrent().navigate(AdminView.class);
+ } else {
+ UI.getCurrent().navigate(requestCache.resolveRedirectUrl());
+ }
+ }
+ } catch (AuthenticationException ex) {
+ // show default error message
+ // Note: You should not expose any detailed information here like "username is known but password is wrong"
+ // as it weakens security.
+ login.setError(true);
+ }
+ });
}
}
diff --git a/src/main/java/org/vaadin/paul/spring/ui/views/PaulView.java b/src/main/java/org/vaadin/paul/spring/ui/views/PaulView.java
new file mode 100644
index 0000000..2482ccf
--- /dev/null
+++ b/src/main/java/org/vaadin/paul/spring/ui/views/PaulView.java
@@ -0,0 +1,17 @@
+package org.vaadin.paul.spring.ui.views;
+
+import com.vaadin.flow.component.html.Label;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.Route;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.vaadin.paul.spring.app.security.SecuredForPaul;
+
+@Route("backdoor")
+@SecuredForPaul
+public class PaulView extends VerticalLayout {
+ @Autowired
+ public PaulView() {
+ Label label = new Label("Hello Paul!");
+ add(label);
+ }
+}