理解这9大内置过滤器,才算是精通Shiro

小Hub领读:

权限框架通常都是一堆过滤器、拦截器的组合运用,在shiro中,有多少个内置的过滤器你知道吗?在哪些场景用那些过滤器,这篇文章但愿你能对shiro有个新的认识!javascript

别忘了,点个 [在看] 支持一下哈~html


前两篇原创shiro相关文章:前端

一、极简入门,Shiro的认证与受权流程解析java

二、只须要6个步骤,springboot集成shiro,并完成登陆git


咱们都知道shiro是个认证权限框架,除了登陆、退出逻辑咱们须要侵入项目代码以外,验证用户是否已经登陆、是否拥有权限的代码其实都是过滤器来完成的,能够这么说,shiro其实就是一个过滤器链集合。web

那么今天咱们详细讨论一下shiro底层到底给咱们提供了多少默认的过滤器供咱们使用,又都有什么用呢?带着问题,咱们先去shiro官网看看对于默认过滤器集的说明。面试

When running a web-app, Shiro will create some useful default Filter instances and make them available in the [main] section automatically. You can configure them in main as you would any other bean and reference them in your chain definitions.

The default Filter instances available automatically are defined by the DefaultFilter enum and the enum’s name field is the name available for configuration.ajax

翻译过来意思:spring

当运行web应用程序时,Shiro将建立一些有用的默认过滤器实例,并使它们在[main]部分自动可用。您能够像配置任何其余bean同样在main中配置它们,并在链定义中引用它们。apache

默认筛选器实例由DefaultFilter enum中定义,enum s name字段是可用于配置的名称。

因而我看了一下DefaultFilter的源码:

public enum DefaultFilter {

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);

    ...
}

终于知道咱们经常使用的anon、authc、perms、roles、user过滤器是哪里来的了!这些过滤器咱们都是能够直接使用的。但你要弄清楚这些默认过滤器,你还不得不去深刻了解一下shiro更底层为咱们提供的过滤器,基本咱们的这些默认过滤器都是经过继承这几个底层过滤器演变而来的。

那么这些过滤器都有哪些呢?咱们来看一个图:

上面我标记了7个咱们接下来要介绍的过滤器,咱们一个个来介绍,弄清楚这些过滤器以后,相信你对shiro的认识会更深一层了。具体authc、perms、roles等这些默认过滤器与这7个过滤器有什么关系你就会明白。

一、AbstractFilter

这个过滤器还得说说,shiro最底层的抽象过滤器,虽然咱们极少直接继承它,它经过实现Filter得到过滤器的特性。

完成一些过滤器基本初始化操做,FilterConfig:过滤器配置对象,用于servlet容器在初始化期间将信息传递给其余过滤器。

二、NameableFilter

命名过滤器,给过滤器定义名称!也是比较基层的过滤器了,未拓展其余功能,咱们不多会直接继承这个过滤器。为重写doFilter方法。

三、OncePerRequestFilter

重写doFilter方法,保证每一个servlet方法只会被过滤一次。能够看到doFilter方法中,第一行代码就是String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();而后经过request.getAttribute(alreadyFilteredAttributeName) != null来判断过滤器是否已经被调用过,从而保证过滤器不会被重复调用。

进入方法以前,先标记alreadyFilteredAttributeName为True,抽象doFilterInternal方法执行以后再remove掉alreadyFilteredAttributeName

因此OncePerRequestFilter过滤器保证只会被一次调用的功能,提供了抽象方法doFilterInternal让后面的过滤器能够重写,执行真正的过滤器处理逻辑。

protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException;

这个过滤器咱们已经能够开始在咱们的项目继承使用,好比拦截用户请求,判断用户是否已经登陆(携带token或cookie信息),若是未登陆则返回Json数据告知未登陆!

好比:
开源mblog博客项目中,过滤器就是继承OncePerRequestFilter。

/**
 * 公众号:MarkerHub
**/
public class AuthenticatedFilter extends OncePerRequestFilter {

    // 前端弹窗的js代码
    private static final String JS = "<script type='text/javascript'>var wp=window.parent; if(wp!=null){while(wp.parent&&wp.parent!==wp){wp=wp.parent;}wp.location.href='%1$s';}else{window.location.href='%1$s';}</script>";
    private String loginUrl = "/login";

    // 重写doFilterInternal方法
    @Override
    protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        Subject subject = SecurityUtils.getSubject();
        // 已经登录就跳过过滤器
        if (subject.isAuthenticated() || subject.isRemembered()) {
            chain.doFilter(request, response);
        } else {
            // 未登陆就返回json或者js代码
            WebUtils.saveRequest(request);
            String path = WebUtils.getContextPath((HttpServletRequest) request);
            String url = loginUrl;
            if (StringUtils.isNotBlank(path) && path.length() > 1) {
                url = path + url;
            }

            if (isAjaxRequest((HttpServletRequest) request)) {
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().print(JSON.toJSONString(Result.failure("您尚未登陆!")));
            } else {
                response.getWriter().write(new Formatter().format(JS, url).toString());
            }
        }
    }
}

未登陆状况,ajax请求过滤器返回您尚未登陆!提示,web请求则返回一段js代码,前端渲染会跳出一个登录窗口,这也就是未什么你们常遇到的点击登陆,当前跳出一个登录弹窗的一种实现方式!

效果:

四、AdviceFilter

看到Advice,很天然想到切面环绕编程,通常有pre、post、after几个方法。因此这个AdviceFilter过滤器就是提供了和AOP类似的切面功能。

继承OncePerRequestFilter过滤器重写doFilterInternal方法,咱们能够先看看:

能够看到上面4个序号:

  • 一、preHandle 前置过滤,默认true
  • 二、executeChain 执行真正代码过滤逻辑->chain.doFilter
  • 三、postHandle 后置过滤
  • 四、cleanup 其实主要逻辑是afterCompletion方法

因而,咱们从OncePerRequestFilter的一个doFilterInternal分化成了切面编程,更容易先后控制执行逻辑。因此若是继承AdviceFilter时候,咱们能够重写preHandle方法,判断用户是否知足已登陆或者其余业务逻辑,返回false时候表示不经过过滤器。

五、PathMatchingFilter

请求路径匹配过滤器,经过匹配请求url,判断请求是否须要过滤,若是url未在须要过滤的集合内,则跳过,不然进入isFilterChainContinued的onPreHandle方法。

咱们能够看下代码:

从上面3个步骤中能够看到,PathMatchingFilter提供的功能是:自定义匹配url,匹配上的请求最终跳转到onPreHandle方法。

这个过滤器为后面的经常使用过滤器提供的基础,好比咱们在config中配置以下

/login = anon
/admin/* = authc

拦截/login请求,通过AnonymousFilter过滤器,咱们能够看下

  • org.apache.shiro.web.filter.authc.AnonymousFilter
public class AnonymousFilter extends PathMatchingFilter {

    /**
     * 公众号:MarkerHub
    **/
    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) {
        // Always return true since we allow access to anyone
        return true;
    }
}

AnonymousFilter重写了onPreHandle方法,只不过直接返回了true,说明拦截的连接能够直接经过,不须要其余拦截逻辑。

而authc->FormAuthenticationFilter也是间接继承了PathMatchingFilter。

public class FormAuthenticationFilter extends AuthenticatingFilter

因此,须要拦截某个连接进行业务逻辑过滤的能够继承PathMatchingFilter方法拓展哈。

六、AccessControlFilter

访问控制过滤器。继承PathMatchingFilter过滤器,重写onPreHandle方法,又分出了两个抽象方法来控制

  • isAccessAllowed 是否容许访问
  • onAccessDenied 是否拒绝访问

因此,咱们如今能够经过重写这个抽象两个方法来控制过滤逻辑。另外多提供了3个方法,方便后面的过滤器使用。

/**
 * 公众号:MarkerHub
**/
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
    saveRequest(request);
    redirectToLogin(request, response);
}

protected void saveRequest(ServletRequest request) {
    WebUtils.saveRequest(request);
}

protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
    String loginUrl = getLoginUrl();
    WebUtils.issueRedirect(request, response, loginUrl);
}

其中redirectToLogin提供了调整到登陆页面的逻辑与实现,为后面的过滤器发现未登陆跳转到登陆页面提供了基础。

这个过滤器,咱们能够灵活运用。

七、AuthenticationFilter

继承AccessControlFilter,重写了isAccessAllowed方法,经过判断用户是否已经完成登陆来判断用户是否容许继续后面的逻辑判断。这里能够看出,从这个过滤器开始,后续的判断会与用户的登陆状态相关,直接继承这些过滤器,咱们不须要再本身手动去判断用户是否已经登陆。而且提供了登陆成功以后跳转的方法。

public abstract class AuthenticationFilter extends AccessControlFilter {
    public void setSuccessUrl(String successUrl) {
        this.successUrl = successUrl;
    }

    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        return subject.isAuthenticated();
    }
}

八、AuthenticatingFilter

继承AuthenticationFilter,提供了自动登陆、是否登陆请求等方法。

/**
 * 公众号:MarkerHub
**/
public abstract class AuthenticatingFilter extends AuthenticationFilter {
    public static final String PERMISSIVE = "permissive";

    //TODO - complete JavaDoc

    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);
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }
    }

    protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception;

    /**
     * 公众号:MarkerHub
    **/
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return super.isAccessAllowed(request, response, mappedValue) ||
                (!isLoginRequest(request, response) && isPermissive(mappedValue));
    }
    ...
}
  • executeLogin 执行登陆
  • onLoginSuccess 登陆成功跳转
  • onLoginFailure 登陆失败跳转
  • createToken 建立登陆的身份token
  • isAccessAllowed 是否容许被访问
  • isLoginRequest 是否登陆请求

这个方法提供了自动登陆的课程,好比咱们获取到token以后实行自动登陆,这场景仍是很场景的。

好比在开源项目renren-fast中,就是这样处理的:

public class OAuth2Filter extends AuthenticatingFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);

        if(StringUtils.isBlank(token)){
            return null;
        }

        return new OAuth2Token(token);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }

        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,若是token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if(StringUtils.isBlank(token)){
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());

            String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));

            httpResponse.getWriter().print(json);

            return false;
        }

        return executeLogin(request, response);
    }

/**
 *公众号:MarkerHub
**/
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        try {
            //处理登陆失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());

            String json = new Gson().toJson(r);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {

        }

        return false;
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest){
        //从header中获取token
        String token = httpRequest.getHeader("token");

        //若是header中不存在token,则从参数中获取token
        if(StringUtils.isBlank(token)){
            token = httpRequest.getParameter("token");
        }

        return token;
    }


}

onAccessDenied方法校验经过以后执行executeLogin方法完成自动登陆!

九、FormAuthenticationFilter

基于form表单的帐号密码自动登陆的过滤器,咱们只须要看这个方法就明白,和renren-fast的实现类似:

public class FormAuthenticationFilter extends AuthenticatingFilter {

    public static final String DEFAULT_USERNAME_PARAM = "username";
    public static final String DEFAULT_PASSWORD_PARAM = "password";
    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";

    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String username = getUsername(request);
        String password = getPassword(request);
        return createToken(username, password, request, response);
    }
    /**
     * 公众号:MarkerHub
    **/
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                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;
        }
    }
}

onAccessDenied调用executeLogin方法。默认的token是UsernamepasswordToken。

结束语

好了,今天先到这里啦,讲了多好内置的过滤器,代码有点多,大家能够用电脑打开文章,而后仔细研究,并回想本身使用shiro过滤器的时候,是否是和我讲的场景同样,结合起来。

这里是MarkerHub,我是吕一明,感谢关注与支持!后续更新请关注公众号:MarkerHub


推荐阅读:

分享一套SpringBoot开发博客系统源码,以及完整开发文档!速度保存!

Github上最值得学习的100个Java开源项目,涵盖各类技术栈!

2020年最新的常问企业面试题大全以及答案