diff --git a/pom.xml b/pom.xml
index eee9f07ab9..9979ff95a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,6 +107,7 @@
spring-security-basic-auth
spring-security-custom-permission
+ spring-security-custom-voter
spring-security-mvc-custom
spring-security-mvc-digest-auth
spring-security-mvc-ldap
diff --git a/spring-security-custom-voter/pom.xml b/spring-security-custom-voter/pom.xml
new file mode 100644
index 0000000000..727efa48ab
--- /dev/null
+++ b/spring-security-custom-voter/pom.xml
@@ -0,0 +1,96 @@
+
+
+ 4.0.0
+
+ org.baeldung
+ spring-security-custom-voter
+ 0.0.1-SNAPSHOT
+ war
+
+ spring-security-custom-voter
+ Custom AccessDecisionVoter with Spring Security
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.4.1.RELEASE
+
+
+
+
+ UTF-8
+ UTF-8
+ 3.0.1
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ junit
+ junit
+ test
+
+
+
+ org.hamcrest
+ hamcrest-core
+ test
+
+
+
+ org.hamcrest
+ hamcrest-library
+ test
+
+
+
+ org.springframework
+ spring-test
+ 4.3.3.RELEASE
+
+
+
+ org.springframework.security
+ spring-security-test
+ 4.1.3.RELEASE
+ test
+
+
+
+ org.springframework.security
+ spring-security-web
+ 4.1.3.RELEASE
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/spring-security-custom-voter/src/main/java/org/baeldung/Application.java b/spring-security-custom-voter/src/main/java/org/baeldung/Application.java
new file mode 100644
index 0000000000..a9d6f3b8b1
--- /dev/null
+++ b/spring-security-custom-voter/src/main/java/org/baeldung/Application.java
@@ -0,0 +1,12 @@
+package org.baeldung;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/spring-security-custom-voter/src/main/java/org/baeldung/security/MinuteBasedVoter.java b/spring-security-custom-voter/src/main/java/org/baeldung/security/MinuteBasedVoter.java
new file mode 100644
index 0000000000..ae698789e1
--- /dev/null
+++ b/spring-security-custom-voter/src/main/java/org/baeldung/security/MinuteBasedVoter.java
@@ -0,0 +1,39 @@
+package org.baeldung.security;
+
+import org.springframework.security.access.AccessDecisionVoter;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class MinuteBasedVoter implements AccessDecisionVoter {
+ @Override
+ public boolean supports(ConfigAttribute attribute) {
+ return true;
+ }
+
+ @Override
+ public boolean supports(Class clazz) {
+ return true;
+ }
+
+ @Override
+ public int vote(Authentication authentication, Object object, Collection collection) {
+ String role = authentication
+ .getAuthorities()
+ .stream().map(GrantedAuthority::getAuthority)
+ .filter("ROLE_USER"::equals)
+ .findAny()
+ .orElseGet(() -> "ROLE_ADMIN");
+
+ if ("ROLE_USER".equals(role) && LocalDateTime.now().getMinute() % 2 != 0) {
+ return ACCESS_DENIED;
+ }
+
+ return ACCESS_ABSTAIN;
+ }
+}
diff --git a/spring-security-custom-voter/src/main/java/org/baeldung/security/WebSecurityConfig.java b/spring-security-custom-voter/src/main/java/org/baeldung/security/WebSecurityConfig.java
new file mode 100644
index 0000000000..b3fb196424
--- /dev/null
+++ b/spring-security-custom-voter/src/main/java/org/baeldung/security/WebSecurityConfig.java
@@ -0,0 +1,69 @@
+package org.baeldung.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.AccessDecisionManager;
+import org.springframework.security.access.AccessDecisionVoter;
+import org.springframework.security.access.vote.AuthenticatedVoter;
+import org.springframework.security.access.vote.RoleVoter;
+import org.springframework.security.access.vote.UnanimousBased;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.access.expression.WebExpressionVoter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+ @Autowired
+ public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
+ // @formatter: off
+ auth.inMemoryAuthentication()
+ .withUser("user").password("pass").roles("USER")
+ .and()
+ .withUser("admin").password("pass").roles("ADMIN");
+ // @formatter: on
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ // @formatter: off
+ http
+ // needed so our login could work
+ .csrf()
+ .disable()
+ .authorizeRequests()
+ .anyRequest()
+ .authenticated()
+ .accessDecisionManager(accessDecisionManager())
+ .antMatchers("/").hasAnyRole("ROLE_ADMIN", "ROLE_USER")
+ .and()
+ .formLogin()
+ .permitAll()
+ .and()
+ .logout()
+ .permitAll()
+ .deleteCookies("JSESSIONID")
+ .logoutSuccessUrl("/login");
+ // @formatter: on
+ }
+
+ @Bean
+ public AccessDecisionManager accessDecisionManager() {
+ // @formatter: off
+ List> decisionVoters =
+ Arrays.asList(
+ new WebExpressionVoter(),
+ new RoleVoter(),
+ new AuthenticatedVoter(),
+ new MinuteBasedVoter());
+ // @formatter: on
+ return new UnanimousBased(decisionVoters);
+ }
+}
diff --git a/spring-security-custom-voter/src/main/java/org/baeldung/security/XmlSecurityConfig.java b/spring-security-custom-voter/src/main/java/org/baeldung/security/XmlSecurityConfig.java
new file mode 100644
index 0000000000..45e095c66e
--- /dev/null
+++ b/spring-security-custom-voter/src/main/java/org/baeldung/security/XmlSecurityConfig.java
@@ -0,0 +1,15 @@
+package org.baeldung.security;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ImportResource;
+
+/**
+ * Created by ambrusadrianz on 09/10/2016.
+ */
+@Configuration
+//@ImportResource({"classpath:spring-security.xml"})
+public class XmlSecurityConfig {
+ public XmlSecurityConfig() {
+ super();
+ }
+}
diff --git a/spring-security-custom-voter/src/main/java/org/baeldung/web/MvcConfig.java b/spring-security-custom-voter/src/main/java/org/baeldung/web/MvcConfig.java
new file mode 100644
index 0000000000..5d38dca1ff
--- /dev/null
+++ b/spring-security-custom-voter/src/main/java/org/baeldung/web/MvcConfig.java
@@ -0,0 +1,18 @@
+package org.baeldung.web;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+/**
+ * Created by ambrusadrianz on 30/09/2016.
+ */
+
+@Configuration
+public class MvcConfig extends WebMvcConfigurerAdapter {
+ @Override
+ public void addViewControllers(ViewControllerRegistry registry) {
+ registry.addViewController("/").setViewName("private");
+ }
+}
diff --git a/spring-security-custom-voter/src/main/resources/spring-security.xml b/spring-security-custom-voter/src/main/resources/spring-security.xml
new file mode 100644
index 0000000000..117638289e
--- /dev/null
+++ b/spring-security-custom-voter/src/main/resources/spring-security.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-security-custom-voter/src/main/resources/templates/private.html b/spring-security-custom-voter/src/main/resources/templates/private.html
new file mode 100644
index 0000000000..5af8c7a13e
--- /dev/null
+++ b/spring-security-custom-voter/src/main/resources/templates/private.html
@@ -0,0 +1,10 @@
+
+
+
+ Private
+
+
+Congrats!
+
+
\ No newline at end of file
diff --git a/spring-security-custom-voter/src/main/webapp/WEB-INF/web.xml b/spring-security-custom-voter/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..d69bce35ae
--- /dev/null
+++ b/spring-security-custom-voter/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,47 @@
+
+
+
+ Spring Secured Application
+
+
+
+ mvc
+ org.springframework.web.servlet.DispatcherServlet
+ 1
+
+
+ mvc
+ /
+
+
+
+ contextClass
+
+ org.springframework.web.context.support.AnnotationConfigWebApplicationContext
+
+
+
+ contextConfigLocation
+ org.baeldung.spring.web.config
+
+
+
+ org.springframework.web.context.ContextLoaderListener
+
+
+
+
+
+ springSecurityFilterChain
+ org.springframework.web.filter.DelegatingFilterProxy
+
+
+ springSecurityFilterChain
+ /*
+
+
+
\ No newline at end of file
diff --git a/spring-security-custom-voter/src/test/java/org/baeldung/web/LiveTest.java b/spring-security-custom-voter/src/test/java/org/baeldung/web/LiveTest.java
new file mode 100644
index 0000000000..3ddeab5920
--- /dev/null
+++ b/spring-security-custom-voter/src/test/java/org/baeldung/web/LiveTest.java
@@ -0,0 +1,42 @@
+package org.baeldung.web;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @WebAppConfiguration @SpringBootTest public class LiveTest {
+ @Autowired private WebApplicationContext context;
+
+ private MockMvc mvc;
+
+ @Before public void setup() {
+ mvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
+ }
+
+ @Test public void givenUnauthenticatedUser_whenAccessingMainPage_thenRedirect() throws Exception {
+ mvc.perform(get("/")).andExpect(status().is3xxRedirection());
+ }
+
+ @Test public void givenValidUsernameAndPassword_whenLogin_thenOK() throws Exception {
+ mvc.perform(formLogin("/login").user("user").password("pass")).andExpect(authenticated());
+ }
+
+ @Test public void givenAuthenticatedAdmin_whenAccessingMainPage_thenOK() throws Exception {
+ mvc.perform(get("/").with(user("admin").password("pass").roles("ADMIN"))).andExpect(status().isOk());
+ }
+}