Search code examples
javaspringspring-bootspring-securityspring-security-acl

Spring Boot with AclPermissionEvaluator resulting in IllegalStateException: No ServletContext set


Hello experts,

I'm currently learning Spring Boot and I want to use it with Spring Security ACL. Following the documentation of Spring Security and a tutorial on Baeldung.com, I think I got an understanding about what is needed. I also looked into the DMS example of Spring. I stumbled across another example by searching for a solution.

Based on this information, I have build my application. For reference, you can find the current application on GitHub.

The current issue

When I start the application, I'm getting a java.lang.IllegalStateException: No ServletContext set thrown. As far as I understand, this is due to the fact that during Spring Boot's auto-configure magic, my @Configuration annotated classes are initialized before the ServletContext is initialized.

Link to full stack trace on pastebin.

What I have done so far

Based on my research (mostly on StackOverflow) and my current understanding of the issue, it should help to put the responsible Beans into an own @Configuration annotated class. Unfortunately, here I'm currently lost. Related questions led me to this thinking (see this question, or this one.)

My environment

  • macOS 10.12.6
  • jdk1.8.0_144
  • IntelliJ IDEA 2017.3.3 (Ultimate Edition)
  • Spring Boot v1.5.9.RELEASE

Relevant project files

pom.xml

<?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>de.moritzrupp</groupId>
<artifactId>traderdiary</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Trader Diary</name>
<description>Trader Diary is an easy to use web application to create a journal 
    about your trades with great reporting on top of it.</description>

<licenses>
    <license>
        <name>GNU General Public License (GPL) v3.0</name>
        <url>https://www.gnu.org/licenses/gpl-3.0.txt</url>
    </license>
</licenses>

<developers>
    <developer>
        <id>moritzrupp</id>
        <name>Moritz Rupp</name>
        <email>[email protected]</email>
        <url>https://www.moritzrupp.de</url>
        <timezone>DE</timezone>
    </developer>
</developers>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-rest-hal-browser</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-acl</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
    </dependency>
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache-core</artifactId>
        <version>2.6.11</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.restdocs</groupId>
        <artifactId>spring-restdocs-mockmvc</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

application.properties

# --------------------------------------------
# Datasource Properties
# --------------------------------------------
spring.h2.console.enabled=true
spring.h2.console.path=/h2

spring.datasource.url=jdbc:h2:mem:trader-diary-h2-db
spring.datasource.platform=h2
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect

TraderDiaryApplication.java

@SpringBootApplication
public class TraderDiaryApplication {

    public static void main(String[] args) {
        SpringApplication.run(TraderDiaryApplication.class, args);
    }
}

DataSourceConfig.java

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource traderDiaryDataSource(DataSourceProperties 
        dataSourceProperties) {

        return dataSourceProperties.initializeDataSourceBuilder().build();
    }
}

WebSecurityConfig.java

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("traderDiaryDataSource")
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // I think it is not relevant for the issue, see GitHub repo
        ...
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        // I think it is not relevant for the issue, see GitHub repo
        ...
    }
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(...) {

        // I think it is not relevant for the issue, see GitHub repo
        ...
    }

    @Bean
    public MethodSecurityExpressionHandler aclExpressionHandler() {

        DefaultMethodSecurityExpressionHandler expressionHandler = 
            new DefaultMethodSecurityExpressionHandler();

        AclPermissionCacheOptimizer permissionCacheOptimizer = 
            new AclPermissionCacheOptimizer(aclService());

        expressionHandler.setPermissionEvaluator(permissionEvaluator());
        expressionHandler
            .setPermissionCacheOptimizer(permissionCacheOptimizer);
        return expressionHandler;
    }

    @Bean
    public PermissionEvaluator permissionEvaluator() {
        return new AclPermissionEvaluator(aclService());
    }

    @Bean
    public JdbcMutableAclService aclService() {
        return new JdbcMutableAclService(dataSource, lookupStrategy(), 
            aclCache());
    }

    @Bean
    public LookupStrategy lookupStrategy() {
        return new BasicLookupStrategy(dataSource, aclCache(), 
            aclAuthorizationStrategy(), new ConsoleAuditLogger());
    }

    @Bean
    public EhCacheBasedAclCache aclCache() {
        return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), 
            permissionGrantingStrategy(), aclAuthorizationStrategy());
    }

    @Bean
    public EhCacheFactoryBean aclEhCacheFactoryBean() {
        EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
        ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
        ehCacheFactoryBean.setCacheName("aclCache");
        return ehCacheFactoryBean;
    }

    @Bean
    public EhCacheManagerFactoryBean aclCacheManager() {
        return new EhCacheManagerFactoryBean();
    }

    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy() {
        return new DefaultPermissionGrantingStrategy(new 
            ConsoleAuditLogger());
    }

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        return new AclAuthorizationStrategyImpl(
            new SimpleGrantedAuthority("ROLE_ADMIN"));
    }

    /* This is due to an earlier issue: DataSource required */
    @Configuration
    protected static class AclMethodSecurityConfig extends 
        GlobalMethodSecurityConfiguration {

        @Autowired
        @Qualifier("daoAuthenticationProvider")
        private AuthenticationProvider authenticationProvider;

        @Autowired
        @Qualifier("aclExpressionHandler")
        private MethodSecurityExpressionHandler aclExpressionHandler;



        @Autowired
        public void configureAuthManager(AuthenticationManagerBuilder 
            authenticationManagerBuilder) {

            authenticationManagerBuilder
                .authenticationProvider(authenticationProvider);
        }

        @Override
        protected MethodSecurityExpressionHandler 
            createExpressionHandler() {
                return aclExpressionHandler;
        }
    }
}

Thanks a lot for all input! If any further information is required, I will happily provide it.

Thanks and best regards Moritz


Solution

  • Hey all,

    I managed to resolve the issue myself. I did a step-by-step approach with commenting/uncommenting all the MethodSecurity related stuff.

    I pin-pointed it down to the creation of the DefaultMethodSecurityExpressionHandler. This was causing the IllegalStateException: No ServletContext set.

    Then, I have created a new Class MethodSecurityConfig.java and I put all the related code there. Now the application is starting again and I can continue with the developed.

    @Rakesh: Thanks for your input!

    MethodSecurityConfig.java

    @Configuration
    public class MethodSecurityConfig {
    
        private DataSource dataSource;
    
        @Autowired
        @Qualifier("traderDiaryDataSource")
        public void setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        @Bean
        public MethodSecurityExpressionHandler aclExpressionHandler() {
            DefaultMethodSecurityExpressionHandler expressionHandler =
                new DefaultMethodSecurityExpressionHandler();
    
            AclPermissionCacheOptimizer permissionCacheOptimizer =
                new AclPermissionCacheOptimizer(aclService());
    
                expressionHandler.setPermissionEvaluator(permissionEvaluator());
                expressionHandler
                    .setPermissionCacheOptimizer(permissionCacheOptimizer);
            return expressionHandler;
        }
    
        @Bean
        public PermissionEvaluator permissionEvaluator() {
            return new AclPermissionEvaluator(aclService());
        }
    
        @Bean
        public JdbcMutableAclService aclService() {
            return new JdbcMutableAclService(dataSource,
                lookupStrategy(), aclCache());
        }
    
        @Bean
        public LookupStrategy lookupStrategy() {
            return new BasicLookupStrategy(dataSource, aclCache(),
                aclAuthorizationStrategy(), new ConsoleAuditLogger());
        }
    
        @Bean
        public EhCacheBasedAclCache aclCache() {
            return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), 
                permissionGrantingStrategy(), aclAuthorizationStrategy());
        }
    
        @Bean
        public EhCacheFactoryBean aclEhCacheFactoryBean() {
            EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
            ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
            ehCacheFactoryBean.setCacheName("aclCache");
            return ehCacheFactoryBean;
        }
    
        @Bean
        public EhCacheManagerFactoryBean aclCacheManager() {
            return new EhCacheManagerFactoryBean();
        }
    
        @Bean
        public PermissionGrantingStrategy permissionGrantingStrategy() {
            return new DefaultPermissionGrantingStrategy(
                new ConsoleAuditLogger());
        }
    
        @Bean
        public AclAuthorizationStrategy aclAuthorizationStrategy() {
            return new AclAuthorizationStrategyImpl(
                new SimpleGrantedAuthority("ROLE_ADMIN"));
        }
    }
    

    WebSecurityConfig.java

    @Configuration
    @EnableGlobalMethodSecurity(securedEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        ...
    
        @Configuration
        protected static class AclMethodSecurityConfig
            extends GlobalMethodSecurityConfiguration {
    
            private MethodSecurityExpressionHandler aclExpressionHandler;
    
            @Autowired
            @Qualifier("aclExpressionHandler")
            public void setAclExpressionHandler(
                MethodSecurityExpressionHandler aclExpressionHandler) {
    
                this.aclExpressionHandler = aclExpressionHandler;
            }
    
            @Override
            protected MethodSecurityExpressionHandler createExpressionHandler() {
                return aclExpressionHandler;
            }
        }
    }