关于shiro:理解这9大内置过滤器才算是精通Shiro

48次阅读

共计 11029 个字符,预计需要花费 28 分钟才能阅读完成。

我的公众号:MarkerHub,Java 网站:https://markerhub.com

更多精选文章请点击:Java 笔记大全.md

小 Hub 领读:

权限框架个别都是一堆过滤器、拦截器的组合使用,在 shiro 中,有多少个内置的过滤器你晓得吗?在哪些场景用那些过滤器,这篇文章心愿你能对 shiro 有个新的意识!

别忘了,点个 [在看] 反对一下哈~


前两篇原创 shiro 相干文章:

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

2、只须要 6 个步骤,springboot 集成 shiro,并实现登录


咱们都晓得 shiro 是个认证权限框架,除了登录、退出逻辑咱们须要侵入我的项目代码之外,验证用户是否曾经登录、是否领有权限的代码其实都是过滤器来实现的,能够这么说,shiro 其实就是一个过滤器链汇合。

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

  • http://shiro.apache.org/web.h…

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.

翻译过去意思:

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

默认筛选器实例由 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 个过滤器有什么关系你就会明确。

1、AbstractFilter

这个过滤器还得说说,shiro 最底层的形象过滤器,尽管咱们极少间接继承它,它通过实现 Filter 取得过滤器的个性。

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

2、NameableFilter

命名过滤器,给过滤器定义名称!也是比拟基层的过滤器了,未拓展其余性能,咱们很少会间接继承这个过滤器。为重写 doFilter 办法。

3、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。

  • https://gitee.com/mtons/mblog
/**
 * 公众号: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 代码,前端渲染会跳出一个登陆窗口,这也就是未什么大家常遇到的点击登录,以后跳出一个登陆弹窗的一种实现形式!

成果:

4、AdviceFilter

看到 Advice,很天然想到切面盘绕编程,个别有 pre、post、after 几个办法。所以这个 AdviceFilter 过滤器就是提供了和 AOP 类似的切面性能。

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

能够看到下面 4 个序号:

  • 1、preHandle 前置过滤,默认 true
  • 2、executeChain 执行真正代码过滤逻辑 ->chain.doFilter
  • 3、postHandle 后置过滤
  • 4、cleanup 其实次要逻辑是 afterCompletion 办法

于是,咱们从 OncePerRequestFilter 的一个 doFilterInternal 分化成了切面编程,更容易前后管制执行逻辑。所以如果继承 AdviceFilter 时候,咱们能够重写 preHandle 办法,判断用户是否满足已登录或者其余业务逻辑,返回 false 时候示意不通过过滤器。

5、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 办法拓展哈。

6、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 提供了调整到登录页面的逻辑与实现,为前面的过滤器发现未登录跳转到登录页面提供了根底。

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

7、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();}
}

8、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 中,就是这样解决的:

  • https://gitee.com/renrenio/re…
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 办法实现主动登录!

9、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,我是吕一明,感激关注与反对!

参考:

https://www.cnblogs.com/LeeSc…


(完)

举荐浏览

Java 笔记大全.md

太赞了,这个 Java 网站,什么我的项目都有!https://markerhub.com

这个 B 站的 UP 主,讲的 java 真不错!

正文完
 0