专栏首页代码人生shiro源码解析-doFilter

shiro源码解析-doFilter

shiro应该算的上java中最流行的权限框架了,使用的多了,便想着研究一下源码,看它究竟怎么运行的。

doFilter是shiro对于每个请求都会走的一个效验过程。它的流程如下

DelegatingFilterProxy开始,执行dofilter(),这里是一个代理模式,执行的是WebApplicationContext中的filter执行的dofilter方法,这个filter就是shiroFilter.从OncePerRequestFilter开始跟踪,它的执行dofilter()。在dofilter()中,只有这个一个判断,就是已经执行过shiro filter的请求不会再去执行shiro filter,实现方式是在http attribute中set一个属性(filter类名.FILTERED),即shiroFilter.FILTERED。接着执行invokeDelegate调用代理对象的dofilter()方法

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
    	synchronized (this.delegateMonitor) {
    		delegateToUse = this.delegate;
    		if (delegateToUse == null) {
    			WebApplicationContext wac = findWebApplicationContext();
    			if (wac == null) {
    				throw new IllegalStateException("No WebApplicationContext found: " +
    						"no ContextLoaderListener or DispatcherServlet registered?");
    			}
    			delegateToUse = initDelegate(wac);
    		}
		    this.delegate = delegateToUse;
    	}
    }

    // 调用代理对象的dofilter方法
    invokeDelegate(delegateToUse, request, response, filterChain);
}
protected void invokeDelegate(
      Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
   delegate.doFilter(request, response, filterChain);
}

delegatefilter的跟踪从AbstractShiroFilter这个父类开始,它是OncePerRequestFilter的子类,OncePerRequestFilter会调用doFilterInternal抽象方法,AbstractShiroFilter去实现它。在doFilterInternal方法中,它会包装request,response,接着将包装后的request,response处理,使用的ServletRequestWrapperServletResponseWrapper,但是包装对象还是原始的request和response,没有替代。这么做的原因应该是便于自定义扩展,比如做一些针对流的操作,流不可重复读写,而采用其他对象包装后,可以把读写后的内容到另一个流里,再去返回。接着生成一个请求对应Subject,让这个Subject去执行新线程的executeChain执行链。

为什么不用线程池去管理线程,而要采用新建线程这种粗暴的方式执行filterChain链,难道Subject的数量是限制了? 在源码中,线程并没有做大小的限制,subject也没有做数量大小的限制,来一个请求就新增一个subject。这里我们采用的方式是重写SubjectFactory,通过redis去缓存principals,根据缓存principals,判断缓存authenticated,通过上面的参数,new Subject。继承Subject,加上user属性,缓存user。

executeChain()方法中,它会在根据请求url,在与FilterChainManager中所有的shiroFilter的chainName(定义的url名)比较,如果存在匹配,这返回对应的filterList。默认使用LinkedHashMap去存url与filterList。并将匹配filter包装到一个FilterChain对象中。接着执行FilterChain

final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

final Subject subject = createSubject(request, response);

//noinspection unchecked
subject.execute(new Callable() {
    public Object call() throws Exception {
        updateSessionLastAccessTime(request, response);
        executeChain(request, response, chain);
        return null;
    }
});

ProxiedFilterChain是FilterChain的shiro实现类,用于filters链的实现,实现方式很巧妙,在chain执行一个filter,并自增下标,在filter.doFilter()中又调用chain.doFilter,实现链的接下来filter的调用。这个做法是不怎么标准的“责任链模式”。

public class ProxiedFilterChain implements FilterChain {

    private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class);
    private FilterChain orig; //web.xml配置的filter
    private List<Filter> filters; //shiro的filter
    private int index = 0; //执行到了哪一个filter

    public ProxiedFilterChain(FilterChain orig, List<Filter> filters) {
        // init property
    }

    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (this.filters == null || this.filters.size() == this.index) {
            this.orig.doFilter(request, response);
        } else {
            this.filters.get(this.index++).doFilter(request, response, this);
        }
    }
}

这次执行的就是匹配filter执行dofilter,我们这里匹配的filter是AuthenticatingFilter下的子类。它会先走AdviceFilterdoFilterInternal()。在doFilterInternal(),依次调用preHandleexecuteChainpostHandle三个方法,就好像AOP的切面中的执行前,执行,执行后三个方法。preHandle就是这个filter验证的逻辑,executeChain将调用chain.dofilter(),继续执行下一个filter,postHandle()是递归回来后,去执行什么操作,默认不执行任何操作。这样执行链chain中所有filter,设计精巧,脑洞清奇。如果某个filter失效了,直接在preHandle()中用webTool.write写返回,或默认跳转页面。

public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
        throws ServletException, IOException {
    Exception exception = null;
    try {
        boolean continueChain = preHandle(request, response);
        if (log.isTraceEnabled()) {
            log.trace("Invoked preHandle method.  Continuing chain?: [" + continueChain + "]");
        }
        if (continueChain) {
            executeChain(request, response, chain);
        }
        postHandle(request, response);
        if (log.isTraceEnabled()) {
            log.trace("Successfully invoked postHandle method");
        }
    } catch (Exception e) {
        exception = e;
    } finally {
        cleanup(request, response, exception);
    }
}

调用preHandle会先进入AccessControlFilter重写的preHandle方法,它会判断允许通过处理或禁止通过处理。用||判断,所以isAccessAllowed通过了就不会再走onAccessDenied()

这里有一个设计的bug,就是访问login接口的时候带正确token,它在isAccessAllowed方法中返回true,不会进入onAccessDenied。而生成并返回新token的处理一般就是在onAccessDenied中,它复杂未通过效验的处理。 解决方法是,重写isAccessAllowed方法,在方法中加上isLoginUrl()的判断,如果是loginUrl,返回false,让它走isAccessAllowed

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

isAccessAllowed方法

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    return super.isAccessAllowed(request, response, mappedValue) ||
                (!isLoginRequest(request, response) && isPermissive(mappedValue));
}

AuthorizationFilter的onAccessDenied方法

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {

    Subject subject = getSubject(request, response);
    // If the subject isn't identified, redirect to login URL
    if (subject.getPrincipal() == null) {
        saveRequestAndRedirectToLogin(request, response);
    } else {
        // If subject is known but not authorized, redirect to the unauthorized URL if there is one
        // If no unauthorized URL is specified, just return an unauthorized HTTP status code
        String unauthorizedUrl = getUnauthorizedUrl();
        //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:
        if (StringUtils.hasText(unauthorizedUrl)) {
            WebUtils.issueRedirect(request, response, unauthorizedUrl);
        } else {
            WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
    return false;
}

这是子AuthorizationFilter的onAccessDenied

Subject subject = getSubject(request, response);
// If the subject isn't identified, redirect to login URL
if (subject.getPrincipal() == null) {
    saveRequestAndRedirectToLogin(request, response);
} else {
    // If subject is known but not authorized, redirect to the unauthorized URL if there is one
    // If no unauthorized URL is specified, just return an unauthorized HTTP status code
    String unauthorizedUrl = getUnauthorizedUrl();
    //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:
    if (StringUtils.hasText(unauthorizedUrl)) {
        WebUtils.issueRedirect(request, response, unauthorizedUrl);
    } else {
        WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

实际上在我们项目中,只有一个filter执行,在preHandle()方法中验证是否通过效验,如果没有通过,使用调用login接口,接着直接webTool.write,返回response,结束请求。

如果return ture,它接着执行filter,并由tomcat的filter WsFilter,进入程序接口。

dofilter流程图

注意 spring的filter源码也是这么处理的,从OncePerRequestFilter开始,做法一致。

总体来说,shiro的dofilter流程如下。首先进入shiro的主filter,这个filter的作用是获得该url与shiro适配的filters,并把这个适配的List<filter>放入filterChain中去继续执行。接着执行filteChain中的下一个filter,这个filter是FilterChain中的,它是shiro中筛选url的filter。这些filter都继承AdviceFilter,去执行doFilterInternal()doFilterInternal()依次执行preHandleexecuteChainpostHandle三个方法,分别是判断是否执行(具体filter的业务就是重写preHandle,),执行链中的下一个filter,filter执行结束后的操作。AdviceFilter有的executeChain会调用下一个filter,从而实现所有filter的效验。AccessControlFilter

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 数据一致包装策略

    对于controller返回的结果(http接口),虽然一直有约定的json结构,但是在代码层面还是由程序员手动去拼写。这样有个缺点,就是程序员的不稳定性,在这...

    逝兮诚
  • 亿级数据mysql优化

    用户分析系统以用户的心跳数据为依据,统计查询用户的各种情况。心跳数据很多,经过去重,去无效,数据量还是在2亿/月的水平。普通的查询在这个量级的数据库上根本查不出...

    逝兮诚
  • hashCode,MD5,SHA-1的区别和碰撞量级

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    逝兮诚
  • 零基础学习爬虫并实战

    总第63篇 本篇主要从爬虫是什么、爬虫的一般流程、爬虫各个流程的实现方法、爬虫实例四个方面分享零基础了解爬虫,并进行简单的实战。 在阅读下面之前,我们...

    张俊红
  • [javaweb]Java过滤器与包装设计模式的实用案例.

    一枝花算不算浪漫
  • 大数据开发工程师学习路线分享

    大数据是对海量数据存储、计算、统计、分析等一系列处理手段,处理的数据量是TB级,甚至是PB或EB级的数据,是传统数据处理手段无法完成的,大数据涉及分布式计算、高...

    用户2292346
  • 算法细节系列(21):贪心有理?

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    用户1147447
  • LeetCode 41 First Missing Positive

    ShenduCC
  • 聊聊rocketmq broker的CONSUMER_SEND_MSG_BACK

    本文主要研究一下rocketmq broker的CONSUMER_SEND_MSG_BACK

    codecraft
  • KafkaController分析2-NetworkClient分析InFlightRequests类

    扫帚的影子

扫码关注云+社区

领取腾讯云代金券