I'm trying to configure my Spring Boot application to use Apache Shiro as its security framework. I have everything working with a PropertiesRealm, now I'm trying to get it working with a JdbcRealm and Spring Boot's built-in H2 database. Here's my dependencies in my pom.xml:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
My schema.sql:
create table if not exists users (
username varchar(256),
password varchar(256),
enabled boolean
);
create table if not exists user_roles (
username varchar(256),
role_name varchar(256)
);
My data.sql:
insert into users (username, password, enabled) values ('user', '04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb', true);
insert into user_roles (username, role_name) values ('user', 'guest');
And my WebSecurityConfig.java class that configures everything:
package security;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.h2.server.web.WebServlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class WebSecurityConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
Map<String, String> filterChainDefinitionMapping = new HashMap<>();
filterChainDefinitionMapping.put("/api/health", "authc,roles[guest],ssl[8443]");
filterChainDefinitionMapping.put("/login", "authc");
filterChainDefinitionMapping.put("/logout", "logout");
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMapping);
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/login");
Map<String, Filter> filters = new HashMap<>();
filters.put("anon", new AnonymousFilter());
filters.put("authc", new FormAuthenticationFilter());
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/login?logout");
filters.put("logout", logoutFilter);
filters.put("roles", new RolesAuthorizationFilter());
filters.put("user", new UserFilter());
shiroFilter.setFilters(filters);
return shiroFilter;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(jdbcRealm());
return securityManager;
}
@Autowired
private DataSource dataSource;
@Bean(name = "realm")
@DependsOn("lifecycleBeanPostProcessor")
public JdbcRealm jdbcRealm() {
JdbcRealm realm = new JdbcRealm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
realm.setCredentialsMatcher(credentialsMatcher);
realm.setDataSource(dataSource);
realm.init();
return realm;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public ServletRegistrationBean h2servletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(new WebServlet());
registration.addUrlMappings("/console/*");
return registration;
}
}
I'm not seeing any errors in my logs. I tried cranking up the logging my adding the following to my application.properties, but it doesn't help much.
logging.level.org.apache.shiro=debug
Thanks,
Matt
There are a couple problems that are happening.
LifecycleBeanPostProcessor
The problem is due to the fact that
LifecycleBeanPostProcessor
is defined in your config class. Since it is aBeanPostProcessor
it must be initialized eagerly to process all other beans. Furthermore, the rest ofWebSecurityConfig
needs to be initialized eagerly since it may impactLifecycleBeanPostProcessor
.The problem is that the autowired feature is not yet available because it is a
BeanPostProcessor
(i.e.AutowiredAnnotationBeanPostProcessor
) too. This means theDataSource
is null.Since it is null the
JdbcRealm
is going to throw aNullPointerException
. This is in turn caught byAbstractAuthenticator
and rethrown as anAuthenticationException
. TheDefaultWebSecurityManager
(actually its parentDefaultSecurityManager
) then catches it invokesonFailedLogin
which removes the "remember me" cookie.Solving LifecycleBeanPostProcessor
The easiest solution is to ensure any infrastructure related beans are defined with a static method. This informs Spring that it does not need to initialize the entire configuration class (i.e.
WebSecurityConfig
). AgainAlternatively, you can also isolate infrastructure related beans in their own configuration.
UPDATE
ShiroFilterFactoryBean
I didn't realize that
ShiroFilterFactoryBean
implementsBeanPostProcessor
also. It is pretty interesting case for anObjectFactory
to also implementBeanPostProcessor
.The problem is that this is preventing the loading of data.sql which means the application does not have any users in the table so authentication will fail.
The issue is that data.sql is loaded via a
DataSourceInitializedEvent
. However, due to the eager initialization of theDataSource
(it was a dependency of aBeanPostProcessor
) theDataSourceInitializedEvent
cannot be fired. This is why you see the following in the logs:Ensuring data.sql Loads
There are a few options that I see to get the insert statements to load.
data.sql->schema.sql
The easiest option is to move the contents of data.sql to schema.sql. The schema.sql is still loaded since it does not require an event to be fired to process it. The data.sql requires an event so that the same mechanism can be used to load data when JPA initializes the schema.
Fixing the Ordering
Unfortunately, you cannot simply make the definition for
ShiroFilterFactoryBean
static since it relies on other bean definitions. Fortunately, there really is no need for theBeanPostProcessor
in this instance. This means you can change your code to return the result of the factory bean which removes theBeanPostProcessor
from the equation:insert into user
The insert statement found in data.sql is incorrect. It needs to include the
enabled
column. For example: