带有 JavaConfig 和 Spring 引导的 Apache Shiro JdbcRealm

Apache Shiro JdbcRealm with JavaConfig and Spring Boot

我正在尝试配置我的 Spring 引导应用程序以使用 Apache Shiro 作为其安全框架。我的一切都与 PropertiesRealm 一起工作,现在我正试图让它与 JdbcRealm 和 Spring Boot 的内置 H2 数据库一起工作。这是我的 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>

我的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)
);

我的data.sql:

insert into users (username, password, enabled) values ('user', '04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb', true);

insert into user_roles (username, role_name) values ('user', 'guest');

还有我的 WebSecurityConfig.java class 配置一切:

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;
    }
}

我在日志中没有看到任何错误。我尝试通过将以下内容添加到我的 application.properties 中来增加日志记录,但它并没有多大帮助。

logging.level.org.apache.shiro=debug

谢谢,

马特

出现了几个问题。

LifecycleBeanPostProcessor

问题是由于 LifecycleBeanPostProcessor 在您的配置 class 中定义。因为它是一个 BeanPostProcessor ,所以必须急切地对其进行初始化以处理所有其他 bean。此外,WebSecurityConfig 的其余部分需要尽快初始化,因为它可能会影响 LifecycleBeanPostProcessor

问题是自动装配功能尚不可用,因为它也是 BeanPostProcessor(即 AutowiredAnnotationBeanPostProcessor)。这意味着 DataSource 为空。

由于 JdbcRealm is going to throw a NullPointerException. This is in turn caught by AbstractAuthenticator and rethrown as an AuthenticationException. The DefaultWebSecurityManager (actually its parent DefaultSecurityManager) then catches it invokes onFailedLogin 为空,因此删除了 "remember me" cookie。

解决LifecycleBeanPostProcessor

最简单的解决方案是确保使用静态方法定义任何与基础结构相关的 bean。这通知 Spring 它不需要初始化整个配置 class(即 WebSecurityConfig)。再次

@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}

或者,您也可以在自己的配置中隔离与基础设施相关的 bean。

更新

ShiroFilterFactoryBean

我没有意识到 ShiroFilterFactoryBean 也实现了 BeanPostProcessorObjectFactory 也实现 BeanPostProcessor.

是非常有趣的情况

问题是这阻止了 data.sql 的加载,这意味着该应用程序在 table 中没有任何用户,因此身份验证将失败。

问题是 data.sql 是通过 DataSourceInitializedEvent 加载的。但是,由于 DataSource 的急切初始化(它是 BeanPostProcessor 的依赖项),DataSourceInitializedEvent 无法触发。这就是您在日志中看到以下内容的原因:

Could not send event to complete DataSource initialization (ApplicationEventMulticaster not initialized)

确保 data.sql 负载

我看到有几个选项可以加载插入语句。

data.sql->schema.sql

最简单的选择是将 data.sql 的内容移动到 schema.sql。 schema.sql 仍然加载,因为它不需要触发事件来处理它。 data.sql 需要一个事件,以便在 JPA 初始化架构时可以使用相同的机制加载数据。

修复排序

不幸的是,您不能简单地将 ShiroFilterFactoryBean 的定义设为静态,因为它依赖于其他 bean 定义。幸运的是,在这种情况下确实不需要 BeanPostProcessor。这意味着您可以将代码更改为 return 从等式中删除 BeanPostProcessor 的工厂 bean 的结果:

@Bean(name = "shiroFilter")
public AbstractShiroFilter shiroFilter() throws Exception {
    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 (AbstractShiroFilter) shiroFilter.getObject();
}

插入用户

data.sql 中的插入语句不正确。它需要包括 enabled 列。例如:

insert into users values ('admin', '22f256eca1f336a97eef2b260773cb0d81d900c208ff26e94410d292d605fed8',true);