Shiro支持Servlet3.0的异步请求

Servlet3.0开始支持Request的异步处理,所谓异步处理就是服务端在收到请求之后,并不是直接开始调用业务代码开始存取数据等耗时操作,而是交给后端的线程池来处理,这样请求接收线程就可以继续接收新进来的请求。等线程池处理完之后,通过AsyncContext回调来返回数据给客户端。
异步请求使用场景:

  • 文件上传等耗时请求,可以采用异步处理,不至于因为同时多个人在上传文件而耗光web容器的线程,影响其它快速请求
  • 高并发服务器,对于高并发服务器,异步化必然选择,不但请求进来异步处理。在业务线程池调用其它服务的时候也需要异步,防止耗时操作耗光线程数,我们借用一张图来看一下:


    Shiro支持Servlet3.0的异步请求_第1张图片
    转载自拿铁咖啡公众号

Spring MVC异步支持

Spring MVC中已经对异步请求做了封装,只要在controller的方法中返回一个Callable或者DeferredResult就可以了,比如下面的例子:

@RestController
public class AsyncRequestController {

    @GetMapping("/async")
    public Callable doAsync(){
        return ()->{
            Thread.sleep(5000);
            return (UserDto)SecurityUtils.getSubject().getPrincipal();
        };
    }
}

对于异步请求的线程池初始化,可以在重写WebMvcConfigurationSupportconfigureAsyncSupport()方法。

@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport{
    @Override
    protected void configureAsyncSupport(AsyncSupportConfigurer configurer) {
         //设置一个3个线程的线程池来处理异步请求
        configurer.setTaskExecutor(new ConcurrentTaskExecutor(Executors.newFixedThreadPool(3)));
        //异步请求处理超时为30秒
        configurer.setDefaultTimeout(30000);
    }
}

Shiro针对异步请求的配置

还是以上次的shiro实现jwt认证授权的项目为例(传送门:https://www.jianshu.com/p/0b1131be7ace),如果我们按照原来的配置,请求上面的异步controller会出现下面的错误:

2018-09-24 00:26:31.659 [ERROR][http-nio-8080-exec-2]:o.a.c.c.C.[.[localhost].[/].[dispatcherServlet] [log:182] Servlet.service() for servlet [dispatcherServlet] threw exception
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton.  This is an invalid application configuration.
    at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123)
    at org.apache.shiro.subject.Subject$Builder.(Subject.java:626)
    at org.apache.shiro.SecurityUtils.getSubject(SecurityUtils.java:56)
    at org.apache.shiro.web.servlet.ShiroHttpServletRequest.getSubject(ShiroHttpServletRequest.java:89)
    at org.apache.shiro.web.servlet.ShiroHttpServletRequest.getSubjectPrincipal(ShiroHttpServletRequest.java:94)
    at org.apache.shiro.web.servlet.ShiroHttpServletRequest.getUserPrincipal(ShiroHttpServletRequest.java:112)
    at org.springframework.web.servlet.FrameworkServlet.getUsernameForRequest(FrameworkServlet.java:1093)
    at org.springframework.web.servlet.FrameworkServlet.publishRequestHandledEvent(FrameworkServlet.java:1078)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728)
    at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:649)
    at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:612)
    at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:567)
    at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:353)

上面错误的原因是我们的shiro filter没有对AsyncContext中的Request没有做拦截,造成SecurityManager为空。解决这个问题,只要在注册shiro Filter的地方做如下配置:

@Bean
    public FilterRegistrationBean filterRegistrationBean(SecurityManager securityManager,UserService userService) throws Exception{
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter((Filter)shiroFilter(securityManager, userService).getObject());
        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
        filterRegistration.setAsyncSupported(true);
        filterRegistration.setEnabled(true);
        //这里添加一下对DispatcherType.ASYNC的支持就可以了
        filterRegistration.setDispatcherTypes(DispatcherType.REQUEST,DispatcherType.ASYNC);

        return filterRegistration;
    }

Filter配置
在SpringMVC的实现中,对于Filter在进入异步请求之前会过一遍,异步请求之后又会过一遍。但是对于shiro这种鉴权的Filter,其实第二遍是没有必要的。所以我们需要在Filter中针对第二次过Filter的情况跳过。实现方式就是第一次进Filter的时候在request的Attribute中加一个属性,这样第二次进来的时候就会发现这个属性不为空,直接跳过,这也是servlet中OncePerRequestFilter的实现逻辑。以JwtAuthFilter为例:

public class JwtAuthFilter extends AuthenticatingFilter {
    @Override
    protected void postHandle(ServletRequest request, ServletResponse response){
        //设置一个标记位
        request.setAttribute("jwtShiroFilter.FILTERED", true);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断是不是第二次进入,是则直接返回
        Boolean afterFiltered = (Boolean)(request.getAttribute("jwtShiroFilter.FILTERED"));
        if( BooleanUtils.isTrue(afterFiltered))
            return true;
        ...
    }
}

同样的RolesFilter也添加类似的逻辑,具体请看源代码:https://github.com/chilexun/springboot-demo/tree/master/shiro-jwt-demo

[参考资料]
异步化,高并发大杀器 作者: 拿铁咖啡

你可能感兴趣的:(Shiro支持Servlet3.0的异步请求)