1. Shiro过滤器链
Shiro中可以配置多个Filter,那么Shiro是如何管理这些过滤器的?主要是靠ShiroFilterFactoryBean
它是一个Spring Bean,用于在Spring应用中配置Shiro的Web过滤器链。过滤器链是一系列按照特定顺序排列的过滤器,每个过滤器负责处理特定类型的请求。通过配置 ShiroFilterFactoryBean
,我们可以定义哪些URL路径应被哪些过滤器处理,以及过滤器之间的执行顺序。过滤器链的配置通常以Map的形式提供,其中键是URL模式,值是对应的过滤器名称或过滤器链定义。
2. ShiroFilterFactoryBean
shiro-spring-boot-web-starter 中有自动配置,可以找到 ShiroWebFilterConfiguration
类,其中有一个bean的定义:
@Bean
@ConditionalOnMissingBean
@Override
protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
return super.shiroFilterFactoryBean();
}
// 创建ShiroFilterFactoryBean对象
protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setLoginUrl(loginUrl);
filterFactoryBean.setSuccessUrl(successUrl);
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setShiroFilterConfiguration(shiroFilterConfiguration());
filterFactoryBean.setGlobalFilters(globalFilters());
filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
if (filterMap != null) {
filterFactoryBean.setFilters(filterMap);
}
return filterFactoryBean;
}
实例化ShiroFilterFactoryBean
的时候,设置了登录url,登录成功后的url,从spirng容器中取出了 securityManager,还中取出过滤器的配置(一个Map对象)。
那么它到底是干什么的?
可以看到它实现了Spring框架的FactoryBean和BeanPostProcessor接口。
2.1 实现 FactoryBean接口
实现Spring的FactoryBean接口,意味着Spring框架会调用其中的getObject()
方法来获取bean的实例对象。这个方法实现的时候,最终调用了:createInstance
方法:
protected AbstractShiroFilter createInstance() throws Exception {
// 如果没有获取到securityManager则抛出异常
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
// 创建过滤器链管理器
FilterChainManager manager = createFilterChainManager();
// 过滤器链路径解析
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
// 实例化 SpringShiroFilter 并注入了SecurityManager,DefaultFilterChainManager以及PathMatchingFilterChainResolver,完成了SpringShiroFilter的单例构造
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver, getShiroFilterConfiguration());
}
2.2 创建过滤器链管理器
在上面的 createInstance()
方法中(第14行),创建了FilterChainManager:
protected FilterChainManager createFilterChainManager() {
// 在它的构造方法中,加入了所有的默认的过滤器
DefaultFilterChainManager manager = new DefaultFilterChainManager();
...
return manager;
}
2.3 实现BeanPostProcessor接口
BeanPostProcessor 接口也称为Bean后置处理器,它是Spring中定义的接口,在Spring容器的创建过程中(具体为Bean初始化前后)会回调BeanPostProcessor中定义的两个方法
-
postProcessBeforeInitialization(Object bean,String beanName)
会在每一个bean对象的初始化方法调用之前回调。 在ShiroFilterFactoryBean的实现中,通过拦截Filter类型的bean,完成全局属性的注入,包括设置:loginUrl、successUrl、unauthorizedUrl;
-
postProcessAfterInitialization(Object bean,String beanName)
会在每个bean对象的初始化方法调用之后被回调。在ShiroFilterFactoryBean的实现中,它直接返回了bean,没有做任何处理。
3. SpringShiroFilter
ShiroFilterFactoryBean 创建的实例Bean实例对象就是 SpringShiroFilter
,它的继承关系:
它是一个javax.servlet.Filter的实现,那它是如何注册到Servlet容器中的呢?一般我们在SpringMVC应用中,注册一个Servlet Filter可以借助于FilterRegistrationBean
来实现,它实现了ServletContextInitializer
这个接口。容器在启动的时候,会将所有实现了 ServletContextInitializer
接口的bean注册到Spring容器中。
3.1 注册Filter
shiro-spring-boot-web-starter 中有自动配置,可以找到 ShiroWebFilterConfiguration
类,其中就定义了FilterRegistrationBean
这个bean:
@Bean(name = REGISTRATION_BEAN_NAME)
@ConditionalOnMissingBean(name = REGISTRATION_BEAN_NAME)
// 将AbstractShiroFilter(实际类型是SpringShiroFilter)注册到Servlet容器中
protected FilterRegistrationBean<AbstractShiroFilter> filterShiroFilterRegistrationBean() throws Exception {
FilterRegistrationBean<AbstractShiroFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD,
DispatcherType.INCLUDE, DispatcherType.ERROR);
filterRegistrationBean.setFilter((AbstractShiroFilter) shiroFilterFactoryBean().getObject());//这里获取到的就是SpringShiroFilter
filterRegistrationBean.setName(FILTER_NAME);
filterRegistrationBean.setOrder(1);//数字越小,越优先
return filterRegistrationBean;
}
在前面的案例中,我们还自定义了AuthenticationFilter
并使用 FilterRegistrationBean 注册到了Servlet 容器中了。因为 SpringShiroFilter 的order设置为了1,所以所有的请求必先经过它, 也就是说,请求需要穿过两个filter,第一个是 SpringShiroFilter,第二个才是我们定义的filter
3.2 执行流程分析
-
OncePerRequestFilter::doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
标记该filter是否处理过请求。处理过则继续执行过滤器链上的其它filter. 它的功能和类的名字一样,只处理一次请求。
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { ... doFilterInternal(request, response, filterChain); ... } }
doFilterInternal 方法被定义为抽象方法,它被子类(AbstractShiroFilter)实现,是真正的处理过滤器逻辑方法
-
AbstractShiroFilter::doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
在这个类中出现了
WebSecurityManager
,它的子类SpringShiroFilter
在ShiroFilterFactoryBean 中被创建的时候,WebSecurityManager
对象被set进来了。protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { ... final Subject subject = createSubject(request, response); subject.execute((Callable<Void>) () -> { updateSessionLastAccessTime(request, response); executeChain(request, response, chain); return null; }); ... }
可以看到,这里创建了Subject, 并且去更新session的最后访问时间,然后就将请求交给了下一个过滤器了。
3.3 小结
shiro中默认有一个过滤器SpringShiroFilter
, 它被实例化的时候需要被“注入” SecurityManager
,需要被放入到Spring 容器和 注册到Servlet容器中。这个过程相对比较复杂,就使用了 ShiroFilterFactoryBean
来进行创建
当请求到来时,它是第一个经过的过滤器,它的主要任务就是创建出Subject 对象,并更新session的最后访问时间,然后请求就被交给了其它过滤器了。
4. Subject对象的创建
跟踪createSubject(request,response)
:
public abstract class AbstractShiroFilter extends OncePerRequestFilter {
...
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
...
public WebSubject buildWebSubject() {
Subject subject = super.buildSubject();
...
return (WebSubject) subject;
}
...
// 可以看到 subject对象的创建,交给了securityManager来完成。
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
}
在交给securityManager创建subject之前,AbstractShiroFilter 中准备了一个 SubjectContext
实际类型是 DefaultSubjectContext
这一点可以在 Subject.Builder来中的newSubjectContextInstance
方法中找到。
它实际上是一个java.util.Map,它就是用来保存当前subject中的数据的,下面是map中的key:
- SECURITY_MANAGER (securityManager对象)
- SESSION_ID (sessionId)
- SUBJECT(subject)
- PRINCIPALS 身份信息
- SESSION 会话
- AUTHENTICATED 是否认证
- AUTHENTICATION_INFO (reaml 中的 AuthenticationInfo,即认证信息)
- AUTHENTICATION_TOKEN (提交的认证token信息)
这个对象刚被创建出来的时候,里面的数据是空的。但是随着调用链的深入,这些信息将会被逐步填充进去
真正创建Subject对象是由SecurityManager来完成的:
public class DefaultSecurityManager extends SessionsSecurityManager {
...
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = copy(subjectContext);
// securityManager对象填充到 subjectContext (它是一个Map)中
context = ensureSecurityManager(context);
// 将session填充到subjectContext中。
// 跟踪这个方法,最终会到达 sessionManager.getSession(Session key) ,此时在sessionManager中,就可以从请求中获取sessionID或者从保存的session中获取session对象。
context = resolveSession(context);
// 身份信息
context = resolvePrincipals(context);
/// subject对象
Subject subject = doCreateSubject(context);
save(subject);
return subject;
}
...
}
跟踪源码发现,实际被创建出来的 Subject对象是 :WebDelegatingSubject
,初始化的数据都是从 subjectContext中取出来的,但到目前为止,SubjectContext中仅仅只有 securityManager.
resolveSession(context)
这个方法最终会调用 SessionManager的Session getSession(SessionKey key)
方法来获取session
5. 绑定subject对象到当前线程
过滤器中创建了 Subject对象以后,紧接着执行execute方法,
public abstract class AbstractShiroFilter extends OncePerRequestFilter {
...
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
...
final Subject subject = createSubject(request, response);
//使用subject.execute来执行代码,保证线程与数据的绑定与解绑
subject.execute((Callable<Void>) () -> {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
});
...
}
}
Subjet会被绑定到 ThreadContext 中, ThreadContext封装了 ThreadLocal,实际使用的是
java.lang.InheritableThreadLocal
所以不管是在父线程还是子线程中,调用SecurityUtils.getSubject()
都能获取到当前用户的信息。
6. 经过其它过滤器
在经过其它过滤器的时候,因为前面已经将Subject对象绑定到了当前线程中,所以其它地方获取的都是同一个Subject对象。
7. 自己配置ShiroFilterFactoryBean
前面我们自己定义的AuthenticationFilter,利用了 FilterRegistrationBean,将其注册到了Servlet 容器中了。
public class AuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter {
...
}
经过了上面的分析,我们只需要注册一个SpringShiroFilter到Servlet容器即可,其它的Shiro中用到的自定义过滤器,我们只需要将他们放入到 过滤器管理器(DefaultFilterChainManager)中即可。
按照这个思路,将项目中的 com.qinyeit.shirojwt.demos.configuration.ShiroConfiguration
改成如下配置:
@Configuration
@Slf4j
public class ShiroConfiguration {
...
/**
* 自定义拦截器
*
* @return
*/
private Map<String, Filter> getCustomerShiroFilter() {
AuthenticationFilter authcFilter = new AuthenticationFilter();
// 设置登录请求的URL
authcFilter.setLoginUrl("/login");
Map<String, Filter> filters = new HashMap<>();
filters.put("authc", authcFilter);
return filters;
}
/**
* URL配置
*
* @return
*/
private ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
/**
* 重要配置
* ShiroFilter 的 FactoryBean
*
* @param securityManager
* @return
*/
@Bean
protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());
filterFactoryBean.setFilters(getCustomerShiroFilter());
return filterFactoryBean;
}
...
}
代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 6_springboot_shiro_jwt_多端认证鉴权_过滤器链 分支上.