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_多端认证鉴权_过滤器链 分支上.

相关推荐
方圆想当图灵11 分钟前
缓存之美:万文详解 Caffeine 实现原理(下)
java·redis·缓存
doubt。23 分钟前
【BUUCTF】[RCTF2015]EasySQL1
网络·数据库·笔记·mysql·安全·web安全
栗豆包26 分钟前
w175基于springboot的图书管理系统的设计与实现
java·spring boot·后端·spring·tomcat
Maybe_ch1 小时前
群晖部署-Calibreweb
数据库·群晖·nas
小辛学西嘎嘎1 小时前
MVCC在MySQL中实现无锁的原理
数据库·mysql
等一场春雨1 小时前
Java设计模式 十四 行为型模式 (Behavioral Patterns)
java·开发语言·设计模式
CC呢1 小时前
基于STM32单片机火灾安全监测一氧化碳火灾
数据库·mongodb
萧若岚1 小时前
Elixir语言的Web开发
开发语言·后端·golang
Channing Lewis1 小时前
flask实现重启后需要重新输入用户名而避免浏览器使用之前已经记录的用户名
后端·python·flask
Channing Lewis2 小时前
如何在 Flask 中实现用户认证?
后端·python·flask