zoukankan      html  css  js  c++  java
  • shiro相关Filter

    0前言

    shiro中的filer是使用servlet中的filter接口扩展的,但是shiro代理了servlet 原生的filter,请求到了后会先执行shiro的filter,再执行原生的filter。

    前面文章项目启动时shiro加载过程中介绍过,shiro只配置了shiroFilter一个拦截器入口,那么shiro是怎样执行内部那么多filter的?shiro内部访问控制filter又是如何实现的呢?

    带着上述两个问题,我们梳理下shiro的filter实现和工作流程。

    filter看翻译是过滤器的意思,但是经常有人把它翻译成拦截器,SpringMVC 中的 Interceptor才是拦截器。我也经常搞错,但是这里我们严谨亿点点。

    1shiro的filter实现

    这里照搬一下zhangkaitao博客中的图。

     (1)Filter

    servlet的filter,不熟悉的可以看一下servlet规范。

    (2)AbstractFilter

    shiro实现,主要用来配置FilterConfig,init()方法里提供了一个模板方法onFilterConfigSet()供子类实现。

    (3)NameableFilter

    给filter起名字的,如shiro自带的anon、authc,名字要求唯一,不然会有问题。

    (4)OncePerRequestFilter

    用于防止多次执行 Filter 的;也就是说一次请求只会走一次过滤器链;另外提供 enabled 属性,表示是否开启该过滤器实例,默认 enabled=true 表示开启,如果不想让某个过滤器工作,可以设置为 false 即可。可以简单看一下dofilter实现,源码107行

        public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
    //首先检查一下有没有执行过这个filter,通过在request里面设置attribute实现
    if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation
    //在判断一下这个过滤器需不需要执行enable属性为true才需要执行 if (/* added in 1.2: */ !isEnabled(request, response) || /* retain backwards compatibility: */ shouldNotFilter(request) ) { log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", getName());
    //不需要执行就到过滤器链的下一个过滤器 filterChain.doFilter(request, response); }
    else { // Do invoke this filter... log.trace("Filter '{}' not yet executed. Executing now.", getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try {
    //重点,shiro的过滤器都走这个逻辑,这是一个模板方法,具体实现交给子类实现 doFilterInternal(request, response, filterChain); }
    finally { // Once the request has finished, we're done and we don't // need to mark as 'already filtered' any more. request.removeAttribute(alreadyFilteredAttributeName); } } }

    OncePerRequestFilter算是一个比较重要的Filter实现,shiro的所有filter只有这一次dofilter实现。OncePerRequestFilter子类分成了两个分支:

    一个是AdviceFilter,shiro内部授权和验证的filter实现都是在这一分支。

    一个是AbstractShiroFilter,也就是web.xml中配置的shiro入口filter。

    下面先分析一下AdviceFilter这条线路

     (5)AdviceFilter

     该类提供了Filter的Aop风格支持,类似于 SpringMVC 中的 Interceptor:

    //在过滤器执行链前执行,异常会中断过滤器链后续执行(类似于aop的前置增强)
    boolean
    preHandle(ServletRequest request, ServletResponse response) throws Exception

    //在过滤器执行链后执行(类似aop的后置增强)
    void postHandle(ServletRequest request, ServletResponse response) throws Exception

    //最终执行,可以用于资源清理(类似于aop的后置最终增强)
    void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;

    前面说过OncePerRequestFilter的dofilter调用了一个模板方法doFilterInternal,AdviceFilter正是通过实现doFilterInternal实现类似aop功能,AdviceFilter类源码124行

        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里面调用了afterCompletion方法 cleanup(request, response, exception); } }

    (6)PathMatchingFilter

    PathMatchingFilter 提供了基于 Ant 风格的请求路径匹配功能及过滤器参数解析的功能.。

    PathMatchingFilter重写了父类AdviceFilter的preHandle方法,PathMatchingFilter类源码163行

        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
    
    //appliedPath就是配置中指定的需要拦截的url
    if (this.appliedPaths == null || this.appliedPaths.isEmpty()) { if (log.isTraceEnabled()) { log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately."); } return true; } for (String path : this.appliedPaths.keySet()) {
    //处理请求的路径匹配需要拦截的路径 if (pathsMatch(path, request)) { log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path); Object config = this.appliedPaths.get(path); return isFilterChainContinued(request, response, path, config); } } //no path matched, allow the request to go through: return true; }

    主要执行的内容交给isFilterChainContinued,PathMatchingFilter类源码192行

        private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
                                               String path, Object pathConfig) throws Exception {
    
    //isEnabled判断这个过滤器生不生效,这里会调用OncePerRequestFilter的实现
    if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2 if (log.isTraceEnabled()) { log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}]. " + "Delegating to subclass implementation for 'onPreHandle' check.", new Object[]{getName(), path, pathConfig}); }
    //重点要关注的方法 return onPreHandle(request, response, pathConfig); } if (log.isTraceEnabled()) { log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}]. " + "The next element in the FilterChain will be called immediately.", new Object[]{getName(), path, pathConfig}); } //This filter is disabled for this specific request, //return 'true' immediately to indicate that the filter will not process the request //and let the request/response to continue through the filter chain: return true; }

    onPreHandle方法处理匹配路径时需要处理的过滤器逻辑,PathMatchingFilter这里默认返回true,主要交给子类实现。

    AnonymousFilter,也就是配置里的anon,就是实现PathMatchingFilter,我们可以看到anon的onPreHandle方法啥也没干(没有拦截逻辑),直接返回true。

    public class AnonymousFilter extends PathMatchingFilter {
    
        /**
         * Always returns <code>true</code> allowing unchecked access to the underlying path or resource.
         *
         * @return <code>true</code> always, allowing unchecked access to the underlying path or resource.
         */
        @Override
        protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) {
            // Always return true since we allow access to anyone
            return true;
        }
    
    }

    (7)AccessControlFilter

    AccessControlFilter 提供了访问控制的基础功能,比如是否允许访问/当访问拒绝时如何处理等。

    这是比较重要的过滤器实现,shiro自己内部访问控制Filter都是基于该Filter实现,我们自己定义的Filter也可以直接扩展该类,(不需要复杂访问控制逻辑自定义Filter也可以直接扩展PathMatchingFilter )

    AccessControlFilter重写了父类PathMatchingFilter的onPreHandle方法,相当于该过滤器的入口。AccessControlFilter类源码161行。

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

    isAccessAllowed方法返回true表示通过访问控制验证,否则才会执行后面的onAccessDenied,onAccessDenied表示访问拒绝时需要执行的逻辑。

    isAccessAllowed和onAccessDenied都是需要子类扩展的。这里以authc为例看看怎么实现的。authc表示FormAuthenticationFilter。

    FormAuthenticationFilter继承AuthenticatingFilter,AuthenticatingFilter又继承AuthenticationFilter,最后AuthenticationFilter继承AccessControlFilter

    首先isAccessAllowed实现

    //AuthenticatingFilter第121行

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


    //
    AuthenticatingFilter父类AuthenticationFilter第79行,判断有没有登陆
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    Subject subject = getSubject(request, response);
    return subject.isAuthenticated();
    }
     

    如果登陆了,直接返回true,否则看看是不是请求的登陆url或者配置了许可的请求,满足其一就返回true

    然后是onAccessDenied实现

    onAccessDenied没有那么多弯弯道道,直接由FormAuthenticationFilter实现,源码148行

        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            //如果请求路径是登陆请求,有没有显示的配置loginUrl,没有的话默认/login.jsp
            if (isLoginRequest(request, response)) {
    //登陆请求必须是post的
    if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); }
    //执行login逻辑,这里用户可以不用自己写登陆入口,前提是需要配置loginUrl
    return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } //其他请求会保留现场,并重定向到登陆页面 saveRequestAndRedirectToLogin(request, response); return false; } }

    下面看一下authc的登陆实现executeLogin,源代码AuthenticatingFilter类第44行

        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            AuthenticationToken token = createToken(request, response);
            if (token == null) {
                String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                        "must be created in order to execute a login attempt.";
                throw new IllegalStateException(msg);
            }
            try {
                Subject subject = getSubject(request, response);
                subject.login(token);
    //登陆成功后会重定向到登陆前请求的页面,successUrl只是个备选重定向地址
    return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) {
    //失败的话会在request的attribute里面记录失败异常,请求会继续回到当前页
    return onLoginFailure(token, e, request, response); } }

    若不是登陆请求saveRequestAndRedirectToLogin则会存下request,并重定向到登陆页面loginUrl

    如果不想用shiro提供的登陆逻辑呢。而且现在项目基本都是前后端分离,使用异步请求,路由的逻辑一般都会交给前端处理,重定向不是那么友好。

    我想到的有三个思路:

    1)第一个是authc不拦截登陆url

    自己在controller里实现登陆的逻辑,但是authc拦截器会拦截其他的url,没有登陆的话请求后端其他接口还是会重定向到loginUrl。

    这里后端可以配一个假的loginUrl,重定向到“前端host/noauth”,然后前端要起一个代理服务打到“后端服务/noauth”,后端response返回一个401给前端,前端接收到401响应自己路由到登陆页面。

    这样的话前端要起一个代理服务,并且后端还是重定向了,只能说没有走authc的executeLogin。可以但是没有必要

    2)自定义扩展AccessControlFilter 

    可以参考FormAuthenticationFilter实现,没有认证时不重定向,直接response设置401即可。授权模块的AuthorizationFilter就是这么做的,参考源码AuthorizationFilter类106行

        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;
        }

    配置了unauthorizedUrl就走重定向逻辑,没有配置就直接返回response 401

    3)干脆不用shiro的访问控制Filter

    自己写一个Filter或者实现SpringMVC 中的 Interceptor,自己做登陆拦截或者权限拦截也很nice

    到此,我们回答了一个问题:shiro内部访问控制filter又是如何实现的

    这些Filter都没有配置到web.xml,spring也没有显示处理这部分filter配置。那么这些filter是如何生效的,我们知道web项目唯一配置的Filter是ShiroFilter,

    也许我们可以从shiro Filter的另一个实现分支AbstractShiroFilter中找到答案。

    2shiro的Filter工作流程

    先看入口ShiroFilter,shiroFilter只是提供了init方法的实现,用于绑定SecurityManager和FilterChainResolver,主要的功能还是在其父类AbstractShiroFilter实现的。

    AbstractShiroFilter直接继承了OncePerRequestFilter,所以我们还是从模板方法doFilterInternal突破,请求会到这里来,源代码AbstractShiroFilter第350行。

        protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
                throws ServletException, IOException {
    
            Throwable t = null;
    
            try {
    //HttpServletRequest会被包装为ShiroHttpServletRequest
    final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);

    //HttpServletResponse会被包装为ShiroHttpServletResponse
    final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
    //为没有请求/响应对创建一个WebSubject实例,这个实例会在整个请求响应执行过程中使用
    final Subject subject = createSubject(request, response); //noinspection unchecked subject.execute(new Callable() { public Object call() throws Exception {
    //更新session访问时间,只会管理shiro自己的session(HttpSession不会管)
    //通过调用session.touch()实现 updateSessionLastAccessTime(request, response);

    //执行过滤器逻辑 executeChain(request, response, chain);
    return null; } }); } catch (ExecutionException ex) { t = ex.getCause(); } catch (Throwable throwable) { t = throwable; } if (t != null) { if (t instanceof ServletException) { throw (ServletException) t; } if (t instanceof IOException) { throw (IOException) t; } //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one: String msg = "Filtered request failed."; throw new ServletException(msg, t); } }

    next

    //源代码AbstractShiroFilter第446行
    protected
    void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException {

    //注意,这里会代理FilterChain FilterChain chain
    = getExecutionChain(request, response, origChain); chain.doFilter(request, response); }


    //源代码AbstractShiroFilter第406行
    protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
    FilterChain chain = origChain;

    //获取初始化时注入的FilterChainResolver
    FilterChainResolver resolver = getFilterChainResolver();
    if (resolver == null) {
    log.debug("No FilterChainResolver configured. Returning original FilterChain.");
    return origChain;
    }

    //重点,生成代理的FilterChain
    FilterChain resolved = resolver.getChain(request, response, origChain);
    if (resolved != null) {
    log.trace("Resolved a configured FilterChain for the current request.");
    chain = resolved;
    } else {
    log.trace("No FilterChain configured for the current request. Using the default.");
    }

    return chain;
    }
     

     源代码PathMatchingFilterChainResolver类第93行

        public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {

    //(1)FilterChainManager维护了shiro默认Filter和用户自定义Filter,
    //还维护了filterChains表示用户配置的pathPattern到多个filter的对应关系 FilterChainManager filterChainManager
    = getFilterChainManager(); if (!filterChainManager.hasChains()) { return null; } String requestURI = getPathWithinApplication(request); //the 'chain names' in this implementation are actually path patterns defined by the user. We just use them //as the chain name for the FilterChainManager's requirements for (String pathPattern : filterChainManager.getChainNames()) { // If the path does match, then pass on to the subclass implementation for specific checks:

    //如果请求路径和pathPattern匹配 if (pathMatches(pathPattern, requestURI)) { if (log.isTraceEnabled()) { log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " + "Utilizing corresponding filter chain..."); }

    //代理原生的FilterChain
    return filterChainManager.proxy(originalChain, pathPattern); } } return null; }

    这里说明下FilterChainManager 中维护的filterChains,表示用户配置的pathPattern到多个filter的对应关系。pathPattern是用户配置的url,如/login、/role/*等,filter表示这些路径请求需要执行的过滤器如authc、anon等。

    下面跟进filterChainManager是如何代理的,

    //源代码DefaultFilterChainManager类第318行
    public
    FilterChain proxy(FilterChain original, String chainName) { NamedFilterList configured = getChain(chainName); if (configured == null) { String msg = "There is no configured chain under the name/key [" + chainName + "]."; throw new IllegalArgumentException(msg); }
    //
    return configured.proxy(original); }


    //源代码SimpleNamedFilterList类,第78行
    public FilterChain proxy(FilterChain orig) {
    return new ProxiedFilterChain(orig, this);
    }
     

    最终找到了代理类ProxiedFilterChain

    public class ProxiedFilterChain implements FilterChain {
    
        //TODO - complete JavaDoc
    
        private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class);
    
        private FilterChain orig;

    //filters表示本次请求路径匹配的pathPattern对应的所有Filter
    private List<Filter> filters; private int index = 0; public ProxiedFilterChain(FilterChain orig, List<Filter> filters) { if (orig == null) { throw new NullPointerException("original FilterChain cannot be null."); } this.orig = orig; this.filters = filters; this.index = 0; } public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.filters == null || this.filters.size() == this.index) { //we've reached the end of the wrapped chain, so invoke the original one: if (log.isTraceEnabled()) { log.trace("Invoking original filter chain."); }

    //如果匹配到shiro的Filter为空,或者匹配到shiro的filter都执行完了,才会执行servlet原本的Filter逻辑
    this.orig.doFilter(request, response); } else { if (log.isTraceEnabled()) { log.trace("Invoking wrapped filter at index [" + this.index + "]"); }

    //这里开始一个个按顺序执行shiro自己的Filter逻辑
    this.filters.get(this.index++).doFilter(request, response, this); } } }

    这就是ShiroFilter整个代理执行过程。

  • 相关阅读:
    在虚拟机VM中安装的Ubuntu上安装和配置Hadoop
    初识Hadoop
    Hold住:坚持的智慧
    《人生若只如初见》读后感
    EAS部署:linux 下安装EAS后启动不了服务
    修改Tomcat默认端口
    IntelliJ IDEA工具使用总结
    Mac OSX 包管理工具
    Mac 下安装、卸载Java 7
    MySQL快速命令
  • 原文地址:https://www.cnblogs.com/ouym/p/14791473.html
Copyright © 2011-2022 走看看