带有 JavaConfig 和 Spring Boot 的 Apache Shiro JdbcRealm

Posted

技术标签:

【中文标题】带有 JavaConfig 和 Spring Boot 的 Apache Shiro JdbcRealm【英文标题】:Apache Shiro JdbcRealm with JavaConfig and Spring Boot 【发布时间】:2015-10-02 00:24:37 【问题描述】:

我正在尝试将我的 Spring Boot 应用程序配置为使用 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 类:

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

谢谢,

马特

【问题讨论】:

这似乎是由于 Spring 配置的数据源无法被 Apache Shiro 使用。如果我将 application.properties 配置为与 mysql 通信,它仍然无法正常工作。如果我在 JdbcRealm bean 的配置中手动创建一个 DataSource - 它可以工作。 如果您只需要一个 bean 的 DataSource,我建议使用 @Bean 方法注入,因为它允许容器实例化您的 @Configuration 类,而不必过早解决依赖关系.话虽这么说,让我感到困惑的是,如果您创建自己的 DataSource... 我让 Shiro 在我的 application.properties 和 Apache Shiro 中使用 Spring 配置的数据源工作。但是,我必须实现自己的 AuthorizingRealm,而不是使用开箱即用的 JdbcRealm。不过这没什么大不了的。 【参考方案1】:

有几个问题正在发生。

LifecycleBeanPostProcessor

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

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

因为它是空的JdbcRealm is going to throw a NullPointerException。这又是caught by AbstractAuthenticator 和rethrown as an AuthenticationExceptionDefaultWebSecurityManager(实际上是它的父 DefaultSecurityManager)然后捕获它 invokes onFailedLogin,这会删除“记住我”cookie。

解决 LifecycleBeanPostProcessor

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

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

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

更新

ShiroFilterFactoryBean

我没有意识到ShiroFilterFactoryBean 也实现了BeanPostProcessorObjectFactory 也实现 BeanPostProcessor 是非常有趣的案例。

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

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

无法发送事件以完成 DataSource 初始化(ApplicationEventMulticaster 未初始化)

确保 data.sql 加载

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

data.sql->schema.sql

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

修复排序

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

@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);

【讨论】:

我试过了,我可以证明在 JdbcRealm 上设置的 dataSource 不为空。但是,身份验证仍然不起作用。你可以在github.com/mraible/java-webapp-security-examples/tree/… 看到我的@WebSecurityConfig。 这里的提交显示了我使用 MySQL 的工作示例与您建议的 Spring Boot 配置的数据源:github.com/mraible/java-webapp-security-examples/commit/… @MattRaible 还有两个问题,我更新了答案以反映必要的更改。我还添加了一个带有必要更改(和一些测试)的 PR github.com/mraible/java-webapp-security-examples/pull/1 @MattRaible 请分享一次代码库。我正在努力设置

以上是关于带有 JavaConfig 和 Spring Boot 的 Apache Shiro JdbcRealm的主要内容,如果未能解决你的问题,请参考以下文章

Spring重温--Spring JavaConfig

Spring JavaConfig实例

spring_JavaConfig

Spring的Xml和JavaConfig 扩展你选哪一个?

为啥@JavaConfig 在 Spring MVC 中不起作用?

基于javaConfig和注解配置Spring Security