6_springboot_shiro_jwt_多端认证鉴权_过滤器链

1. Shiro过滤器链

​ Shiro中可以配置多个Filter,那么Shiro是如何管理这些过滤器的?主要是靠ShiroFilterFactoryBean 它是一个Spring Bean,用于在Spring应用中配置Shiro的Web过滤器链。过滤器链是一系列按照特定顺序排列的过滤器,每个过滤器负责处理特定类型的请求。通过配置 ShiroFilterFactoryBean,我们可以定义哪些URL路径应被哪些过滤器处理,以及过滤器之间的执行顺序。过滤器链的配置通常以Map的形式提供,其中键是URL模式,值是对应的过滤器名称或过滤器链定义。

2. ShiroFilterFactoryBean

shiro-spring-boot-web-starter 中有自动配置,可以找到 ShiroWebFilterConfiguration 类,其中有一个bean的定义:

java 复制代码
@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 方法:

java 复制代码
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:

java 复制代码
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:

java 复制代码
@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 执行流程分析

  1. OncePerRequestFilter::doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)

    标记该filter是否处理过请求。处理过则继续执行过滤器链上的其它filter. 它的功能和类的名字一样,只处理一次请求。

    java 复制代码
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        	...
                doFilterInternal(request, response, filterChain);
            ...
        }
    }

    doFilterInternal 方法被定义为抽象方法,它被子类(AbstractShiroFilter)实现,是真正的处理过滤器逻辑方法

  2. AbstractShiroFilter::doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)

    在这个类中出现了WebSecurityManager ,它的子类SpringShiroFilter 在ShiroFilterFactoryBean 中被创建的时候,WebSecurityManager 对象被set进来了。

    java 复制代码
     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) :

java 复制代码
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来完成的:

java 复制代码
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方法,

java 复制代码
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 容器中了。

java 复制代码
public class AuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter {
    ...
}

经过了上面的分析,我们只需要注册一个SpringShiroFilter到Servlet容器即可,其它的Shiro中用到的自定义过滤器,我们只需要将他们放入到 过滤器管理器(DefaultFilterChainManager)中即可。

按照这个思路,将项目中的 com.qinyeit.shirojwt.demos.configuration.ShiroConfiguration 改成如下配置:

java 复制代码
@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_多端认证鉴权_过滤器链 分支上.

相关推荐
风_流沙4 分钟前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
亽仒凣凣12 分钟前
Windows安装Redis图文教程
数据库·windows·redis
亦世凡华、20 分钟前
MySQL--》如何在MySQL中打造高效优化索引
数据库·经验分享·mysql·索引·性能分析
YashanDB23 分钟前
【YashanDB知识库】Mybatis-Plus调用YashanDB怎么设置分页
数据库·yashandb·崖山数据库
颜淡慕潇27 分钟前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
ProtonBase33 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
乐之者v40 分钟前
leetCode43.字符串相乘
java·数据结构·算法
suweijie7684 小时前
SpringCloudAlibaba | Sentinel从基础到进阶
java·大数据·sentinel
公贵买其鹿5 小时前
List深拷贝后,数据还是被串改
java
云和数据.ChenGuang6 小时前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite