1. Overview

In this article, we will explain about Spring security custom rolevoter example. Spring security provides role-based voting based on ULR or resources but sometimes we should require role voter more specific. We will also explain how we can implement spring security custom decision manager.

We are taking here an example to explain in more details, We have two roles in application admin and staff but staff cannot login in the application during the weekend (Sunday). If staff try to login during the weekend then an application will return 405 access denied status.

  • we have created accessDecisionManager which contains a list of voters.
  • We have created a class WeekOffVoter which implements AccessDecisionVoter interface, AccessDecisionVoter have a votemethod in which we should write our custom code to take a decision where allowed to access decision or not.
    • vote method may return three possible value:
      • ACCESS_DENIED: Deny to access resources
      • ACCESS_GRANTED : Grant to access resources
      • ACCESS_ABSTAIN : Not allowed nor Deny access resources, Decision will be take based on other voters

2. Example

spring security custom rolevoter

spring security custom rolevoter

2.1 pom.xml

spring-boot-starter-security requires for spring security other dependency is for spring boot.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>spring-boot-example</groupId>
    <artifactId>spring-boot-example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <description>Spring security custom rolevoter example</description>
    <!-- Inherit defaults from Spring Boot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.4.RELEASE</version>
    </parent>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.lambdaworks/lettuce -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>                <!-- for tomcat web container-->
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>  <!--starter require for spring boot spring security-->
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>              <!--fot jap compilation need provide scope runtime or provided because it available in tomcat -->
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>runtime</scope>     <!-- in my case provided not working so write runtime-->
        </dependency>
    </dependencies>
    <!-- Package as an executable jar -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.2 application.properties

spring.mvc.view.prefix: /WEB-INF/jsp/
spring.mvc.view.suffix: .jsp

2.3 SecurityConfiguration

  • Here we have created AccessDecisionManager Bean and this bean has been registered with accessDecisionManager:
package com.javadeveloperzone;
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.ConfigAttribute;
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.core.Authentication;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;

/**
 * Created by JavaDeveloperZone on 13-11-2017.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired      // here is configuration related to spring boot basic authentication
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password("mypassword").roles("STAFF")
            .and()
            .withUser("admin").password("mypassword").roles("ADMIN");// those are user name and password
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests().antMatchers("/loginFailed").permitAll()
            .and().authorizeRequests().antMatchers("/**").access("hasAnyRole('ADMIN','STAFF')")
            .and().authorizeRequests()
            .and()
            .formLogin()
            .defaultSuccessUrl("/loginSuccess")
            .failureUrl("/failed")
            .loginPage("/login")
            .permitAll();
        http.authorizeRequests()
            .accessDecisionManager(accessDecisionManager());        // passed custom access decision manager
    }
    @Bean
    public AccessDecisionManager accessDecisionManager() {
        java.util.List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(
                new WebExpressionVoter(),
                new RoleVoter(),
                new AuthenticatedVoter(),
                new WeekOffVoter()                          // create instance of WeekOffVoter
        );
        return new UnanimousBased(decisionVoters);
    }
}

2.4 WeekOffVoter

We have defined weekOffVoter class which implements an interface AccessDecisionVoter. vote method contains custom logic to deny login access during the week off for staff members:

package com.javadeveloperzone;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import java.time.LocalDateTime;
import java.util.Collection;
/**
 * Created by JavaDeveloperZone on 22-01-2018.
 */
class WeekOffVoter implements AccessDecisionVoter {
    @Override
    public int vote(Authentication authentication, Object object, Collection collection) {
        boolean isRoleUser = authentication.getAuthorities().stream()
                                           .filter(e -> e.getAuthority().equals("ROLE_STAFF"))
                                           .findAny().isPresent();                      // check is staff role
        if (isRoleUser) {
            if (LocalDateTime.now().getDayOfWeek().getValue() == 7)        // check for sunday
                return ACCESS_DENIED;                                   // deny if sunday
            else {
                return ACCESS_ABSTAIN;                  // not granted or not deny, Decision based on other voter
            }
        } else {
            return ACCESS_ABSTAIN;              // not granted or not deny, Decision based on other voter
        }
    }
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;            
    }
    @Override
    public boolean supports(Class clazz) {
        return true;                    
    }
}

2.5 Application

package com.javadeveloperzone;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
/**
 * Created by JavaDeveloperZone on 19-07-2017.
 */
@SpringBootApplication
@ComponentScan
// Using a root package also allows the @ComponentScan annotation to be used without needing to specify a basePackage attribute
public class Application {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class, args);            // it wil start application
    }
}

2.6 LoginController

package com.javadeveloperzone.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
/**
 * Created by JavaDeveloperZone on 19-07-2017.
 */
@Controller
public class LoginController {
    @RequestMapping(value = "/login")
    public String login() {
        return "login";
    }
    @RequestMapping(value = "/loginSuccess")
    public String loginSuccess(Principal principal, ModelMap modelMap) {
        modelMap.put("userName",principal.getName());
        return "welcome";
    }
    @RequestMapping(value = "/loginFailed")
    public String loginFailed() {
        return "loginFailed";
    }
}

2.7 login.jsp

<%--
  Created by IntelliJ IDEA.
  User: Java Developer Zone
  Date: 18-03-2017
  Time: 07:34
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spring security custom rolevoter example</title>
</head>
<body>
<H2>STAFF Can't Login on SUNDAY</H2>
<form name='f' action='/login' method='POST'>
    User: <input type='text' name='username' value=''>
    Password: <input type='password' name='password'/>
    <input type="hidden"
           name="${_csrf.parameterName}"
           value="${_csrf.token}"/>
    <input name="submit" type="submit" value="Login"/>
</form>
</body>
</html>

2.8 welcome.jsp

<%--
  Created by IntelliJ IDEA.
  User: Java Developer Zone
  Date: 18-03-2017
  Time: 07:34
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spring security custom rolevoter example</title>
</head>
<body>
Welcome, ${userName}
</br>
<a href="/logout">Logout</a>
</body>
</html>

2.9 loginFailed.jsp

<%--
  Created by IntelliJ IDEA.
  User: Java Developer Zone
  Date: 18-03-2017
  Time: 07:34
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spring security custom rolevoter example</title>
</head>
<body>
Login Failed.
</body>
</html>

Output:

Let’s try staff login on Sunday:

Spring security custom rolevoter example - Login

Spring security custom rolevoter example – Login

 

It will throw 405 - Access is denied because of custom role voter deny it.

Spring security custom rolevoter example - Access Deny

Spring security custom rolevoter example – Access Deny

3. Conclusion

In this example, We learned about how we can implement role-based custom authorization to allowed or deny access the resources.  We can write our custom code using custom voter implementation in spring security.

4. References

Was this post helpful?

2 comments. Leave new

Is it possible to implement in spring oAuth 2?

Leave a Reply

Your email address will not be published. Required fields are marked *