Apache Shiro SpringBoot 集成原理

前言

shiro 框架支持多种使用场景,并且都提供了默认配置,本篇文章会介绍 shiro 的配置原理和如何与 springboot 集成的。

默认配置类

shiro 支持运行在 Web 环境下和非 Web 环境下,每种环境都有着不一样的默认配置。

非 Web 环境

AbstractShiroConfiguration提供了非 Web 环境下的的默认配置。它提供了securityManager方法来创建SecurityManager实例,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class AbstractShiroConfiguration {
    
    protected SessionsSecurityManager securityManager(List<Realm> realms) {
        // 使用createSecurityManager创建securityManager
        SessionsSecurityManager securityManager = createSecurityManager();
        // 进行securityManager的配置
        securityManager.setAuthenticator(authenticator());
        securityManager.setAuthorizer(authorizer());
        securityManager.setRealms(realms);
        securityManager.setSessionManager(sessionManager());
        securityManager.setEventBus(eventBus);

        if (cacheManager != null) {
            securityManager.setCacheManager(cacheManager);
        }

        return securityManager;
    }
}

可以看到它在创建SecurityManager时,使用了很多配置参数,这些参数都是以函数的方式返回,比如authenticator函数返回Authenticator实例。这样设计使得子类就可以复写该方法,实现自定义参数配置,并且方法名和参数类型名称是相同的,这样刚好符合 spring bean 的配置规范。

下面是AbstractShiroConfiguration提供的默认参数表,只是列举了一些重要的参数,

方法名 返回类型 含义
createSecurityManager DefaultSecurityManager SecurityManager 实例,作为shiro的核心门面类
authenticator ModularRealmAuthenticator Authenticator 实例,作为用户身份认证
authorizer ModularRealmAuthorizer Authorizer 实例,作为用户权限检查
sessionManager DefaultSessionManager SessionManager 实例,作为 session 管理

Web 环境下

AbstractShiroWebConfiguration作为web 场景下的默认配置,它集成了AbstractShiroConfiguration,复写了一些参数的方法。下面是它的默认参数表

方法名 返回类型 含义
createSecurityManager DefaultWebSecurityManager SecurityManager 实例,作为shiro的核心门面类
sessionManager ServletContainerSessionManager SessionManager 实例,作为 session 管理

shiro 配置类

classDiagram
	class AbstractShiroConfiguration
	class ShiroConfiguration
	
	AbstractShiroConfiguration <|-- ShiroConfiguration
	
    class AbstractShiroWebConfiguration
	class ShiroWebConfiguration
	class ShiroWebAutoConfiguration
	
	AbstractShiroConfiguration <|-- AbstractShiroWebConfiguration
	AbstractShiroWebConfiguration <|-- ShiroWebConfiguration
	AbstractShiroWebConfiguration <|-- ShiroWebAutoConfiguration
	
	class ShiroAutoConfiguration
	AbstractShiroConfiguration <|-- ShiroAutoConfiguration

上面已经讲解了AbstractShiroConfigurationAbstractShiroWebConfiguration两个抽象类的原理,图中其他的类都对应了不同的使用场景。

配置类 集成框架 使用环境
ShiroConfiguration spring 非 Web 环境
ShiroWebConfiguration spring Web 环境
ShiroAutoConfiguration spring boot 非 Web 环境
ShiroWebAutoConfiguration spring boot Web 环境

这些类都添加了@Configuration注解,并且参数方法也都提供了@Bean 注解,这样方便集成 spring。下面只会讲解 springboot web 环境下的配置原理,这种场景使用最为常见。

SpringBoot web 集成示例

虽然网上关于 spring boot 集成 shiro 的配置教程很多,但是个人觉得都不够简洁,并没有理解到 shiro 的设计思想,所以这里单独拿出来讲一下。

在 pom.xml 添加 shiro-spring 依赖

1
2
3
4
5
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.5.2</version>
</dependency>

自定 Realm

自定义Realm,负责提供身份信息和权限信息。下面假设AccountService类实现了用户相关的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyRealm extends AuthorizingRealm {

    @Autowired
    AccountService service;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = principalCollection.getPrimaryToken();
        return service.getPermission(username);
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        String password = String.valueOf(token.getPassword());
        if (!service.checkUserPassword(username, password)) {
            throw new AuthenticationException();
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, "my_realm");
        return info;
    }

}

自定义 Filter

这里自定义 Filter 是为了实现 Restful 风格的接口,当发现用户没有登录,就会返回 json 数据,status 为 1 的错误码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class RestFilter extends AuthenticationFilter {

    public final String LOGIN_REQUIRED_RESPONSE = "{\"status\" : 1}";

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        PrintWriter out = servletResponse.getWriter();
        servletResponse.setContentType("application/json;charset=utf-8");
        servletResponse.setCharacterEncoding("UTF-8");
        out.print(LOGIN_REQUIRED_RESPONSE);
        out.flush();

        return false;
    }

}

配置类

现在来到了最核心的配置,首先返回了自定义 Realm 和 Filter。需要注意下 Filter,这里单独配置了它不会被注册到 Serlvet 里。因为我们自定义的 filter 只能由 shiro 的 主 filter 管理,所以不能注册到 Serlvet,在这篇文章有详细介绍到, {% post_link shiro-architecture %} 。

还有需要定义ShiroFilterChainDefinition,这里配置了根据请求路径,来选择哪条 FilterChain。比如login路径的请求,会被只包含anon的 FilterChain 处理,表示匿名用户都能访问。其余路径的请求,则由包含RestFilter的 FilterChain 处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
public class ShiroConfig {

    @Bean
    public Realm realm() {
        return new MyRealm();
    }

    @Bean
    public RestFilter myauth() {
        return new RestFilter();
    }

    @Bean
    public FilterRegistrationBean restFilterRegistration(RestFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(filter);
        registration.setEnabled(false);
        return registration;
    }


    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        chainDefinition.addPathDefinition("/login", "anon");
        chainDefinition.addPathDefinition("/**", "myauth");
        return chainDefinition;
    }

}

添加 controller 示例

这里写个简单的controller,实现登录和访问页面的接口。比如访问/hello路径,则该请求需要先通过身份认证,才能访问,否则会返回{"status": 1}的响应。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class AccountController {
    
    @PostMapping(value = "/login")
    public Response login(@RequestBody LoginRequest request, HttpServletResponse response) {
        Subject user = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(request.getUsername(), request.getPassword());
        try {
            user.login(token);
        } catch (AuthenticationException errro) {
            return Response.PASSWORD_FAIL_RESPONSE;
        }

        return Response.OK_RESPONSE;
    }
    
    @GetMapping(path = "/hello")
    public Response get() {
        Response result = new Response();
        result.setCode(0);
        result.setData("hello");
        return result;
    }
}

springboot shiro 集成原理

当我们引入了shiro-spring-boot-web-starter模块后,springboot 会根据该模块的配置,自动引入ShiroWebAutoConfigurationShiroWebFilterConfiguration配置。

Filter 配置

ShiroWebFilterConfiguration是用来配置 shiro Filter。可以看到它使用了很多条件注解,并且可以看到它使用FilterRegistrationBean注册了 shiro 主 Filter。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
@ConditionalOnProperty(
    name = {"shiro.web.enabled"},
    matchIfMissing = true
)
public class ShiroWebFilterConfiguration extends AbstractShiroWebFilterConfiguration {
    public ShiroWebFilterConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean
    protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
        return super.shiroFilterFactoryBean();
    }

    @Bean(
        name = {"filterShiroFilterRegistrationBean"}
    )
    @ConditionalOnMissingBean
    protected FilterRegistrationBean filterShiroFilterRegistrationBean() throws Exception {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[]{DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR});
        filterRegistrationBean.setFilter((AbstractShiroFilter)this.shiroFilterFactoryBean().getObject());
        filterRegistrationBean.setOrder(1);
        return filterRegistrationBean;
    }
}

我们需要继续看看父类是如何创建出ShiroFilterFactoryBean实例的,下面只是列举了一些重要的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class AbstractShiroWebFilterConfiguration {

    @Autowired
    protected SecurityManager securityManager;

    @Autowired
    protected ShiroFilterChainDefinition shiroFilterChainDefinition;

    @Autowired
    protected Map<String, Filter> filterMap;
    
    protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        filterFactoryBean.setSecurityManager(securityManager);
        filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
        filterFactoryBean.setFilters(filterMap);

        return filterFactoryBean;
    }
}

可以看到ShiroFilterChainDefinition实例,使用了AutoWired自动装配。我们在ShiroConfig配置类定义的ShiroFilterChainDefinition实例,会被自动装配到这里。

还需要主意filterMap属性,它是Map<String, Filter>类型。它会自动扫描所有的Filter实例,并且添加到 map 里。所以我们在ShiroConfig配置类定义的RestFilter 被添加到了这里。

Shiro core 配置

ShiroWebAutoConfiguration负责 shiro 核心类型的配置,它继承AbstractShiroWebConfiguration,将父类的参数方法都提供了Bean注解。还需要注意到securityManager方法,它接收的参数类型是List<Realm>。我们在ShiroConfig配置类定义的MyRleam会被添加到该列表里。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@AutoConfigureBefore({ShiroAutoConfiguration.class})
@ConditionalOnProperty(
    name = {"shiro.web.enabled"},
    matchIfMissing = true
)
public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    protected Authenticator authenticator() {
        return super.authenticator();
    }
    
    //.....
    
    @Bean
    @ConditionalOnMissingBean
    protected SessionsSecurityManager securityManager(List<Realm> realms) {
        return super.securityManager(realms);
    }
    
}
updatedupdated2023-07-022023-07-02