自定义拦截器

  • 自定义拦截器可以扩展一些功能,如,从Subject获取用户身份信息绑定到Request、验证码验证、在线用户信息的保存等。
扩展OncePerRequestFilter
  • OncePerRequestFilter保证请求只调用一次doFilterInternal()。
public class MyOncePerRequestFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(ServletRequest request, ServletResponse response,
      FilterChain chain) throws ServletException, IOException {
        System.out.println("once per request filter");
        chain.doFilter(request, response);
    }
}
  • shiro.ini: 指定拦截器为自定义拦截器
[main]
;注册Filter
myFilter1=com.haien.shiroChapter8.web.filter.MyOncePerRequestFilter

;或在这里注册
#[filters]
#myFilter1=com.haien.shiroChapter8.web.filter.MyOncePerRequestFilter

;然后在urls配置url与filter的映射关系即可
[urls]
;访问任意url,后台调用myFilter1拦截器,该拦截器在控制台打印一行字
/**=myFilter1

扩展AdviceFilter

  • 提供了aop的功能。其中preHandle()返回false将中断后续拦截器链的执行。
public class MyAdviceFilter extends AdviceFilter {
    //根据返回值决定是否继续处理,true:继续过滤过滤链,可以通过它实现权限控制。
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response)
      throws Exception {
        System.out.println("====预处理/前置处理====");

        //返回false将中断后续拦截器链的执行
        return true; 
    }

    //执行完过滤器后正常返回则调用该方法。
    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) 
      throws Exception {
        System.out.println("====后处理/后置返回处理====");
    }

    //不过最后有没有异常都会执行,完成如清理资源等功能。
    @Override
    public void afterCompletion(ServletRequest request, ServletResponse response, 
      Exception exception) throws Exception {
        System.out.println("====完成处理/后置最终处理====");
    }
}
  • shiro.ini
[filters]
myFilter1=com.haien.shiroChapter8.web.filter.MyOncePerRequestFilter
myFilter2=com.haien.shiroChapter8.web.filter.MyAdviceFilter
[urls]
/**=myFilter1,myFilter2
  • 运行结果

扩展PathMatchingFilter

  • 继承了AdviceFilter,提供了url模式过滤的功能,适用于需要对指定请求进行处理的情况。
public class MyPathMatchingFilter extends PathMatchingFilter {
    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, 
      Object mappedValue) throws Exception {
       System.out.println("url matches,config is " + Arrays.toString((String[])mappedValue));
       return true;
    }
}
  • preHandle:将当前url与已有url进行匹配,成功则调用onPreHandle();否则直接返回true。
  • onPreHandle: 被调用后获取ini配置文件中为拦截器配置的参数,默认什么都不处理直接返回true。

  • shiro.ini

[filters]
myFilter3=com.github.zhangkaitao.shiro.chapter8.web.filter.MyPathMatchingFilter
[urls]
/**= myFilter3[config]
  • config:拦截器的参数,指定权限或角色,多个参数用逗号分隔。onPreHandle使用mappedVaule接收该参数。

  • 打印结果:url matches,config is null

扩展AccessControllerFilter

  • 继承了PathMatchingFilter,扩展了两个方法:
public boolean onPreHandle(ServletRequest request, ServletResponse response, 
  Object mappedValue) throws Exception {
    return isAccessAllowed(request, response, mappedValue)
        || onAccessDenied(request, response, mappedValue);
}
  • isAccessAllowed:是否允许访问,返回true表示允许。
  • onAccessDenied:拒绝访问时是否需要继续处理,返回true表示是,则返回上一级调用链,继续过滤链执行,false不用继续处理了,已在方法内处理好了(如重定向到另一个页面,也就不能继续执行过滤链)。
  • 这俩方法的实现有两种模型:
  1. isAccessAllowed直接返回false,然后校验逻辑全在onAccessDenied实现。会在需要权限的方法执行前调用这俩,比如判断用户是否已登录,未登录则重定向至登录页面,正在登录则调用以下login()接收表单验证,已登录则继续过滤链,最终访问到目标方法。
  2. isAccessAllowed实现校验逻辑,onAccessDenied做校验失败处理,如,把失败信息存入request。代码示例:ideaProjects/shiro-chapter22/jcaptcha/JCaptchaValidateFilter
  • 自定义AccessControllerFilter:
/**
 * @Author haien
 * @Description 以下两个方法同时被onPreHandle()调用
 * @Date 2019/3/1
 **/
public class MyAccessControlFilter extends AccessControlFilter {
    /**
     * @Author haien
     * @Description 是否允许访问,true表示允许
     * @Date 2019/3/1
     * @Param [request, response, mappedValue]
     * @return boolean
     **/
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        System.out.println("access allowed");
        return true;
    }

    /**
     * @Author haien
     * @Description 拒绝访问时是否自己处理,返回true表示自己不处理且继续执行拦截器,
     *              false表示自己已经处理了(如重定向到另一页面)
     * @Date 2019/3/1
     * @Param [request, response]
     * @return boolean
     **/
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //访问未被拒绝的话不会执行该方法
        System.out.println("访问拒绝也不自己处理,继续拦截器链的执行");
        return true;

        /*or
        ...重定向到另一页面...
        return false;
        */
    }
}
  • shiro.ini:
[filters]
myFilter4=com.haien.shiroChapter8.web.filter.MyAccessControlFilter
[urls]
/**=myFilter4
  • 打印结果:access allowed

基于表单的拦截器

  • 之前我们已经接触过Shiro内置的基于表单的拦截器了,具体事例参见笔记:Shiro第七章-与web集成。其实就是截获表单、封装成token传给Subject.login(token)执行登录而已。
  • 现在我们自定义一个:
public class FormLoginFilter extends PathMatchingFilter {
    private String loginUrl = "/login.jsp";
    private String successUrl = "/";

    /**
    * 总方法
    */
    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, 
      Object mappedValue) throws Exception {

        //1、判断是否已登录且非登录请求,是则直接进入过滤链下一步
        if(SecurityUtils.getSubject().isAuthenticated() && !isLoginRequest(req)) {
            return true;//已经登录过且非登录请求
        }

        //2、未登录则判断是否为登录请求,是,则若是get请求,继续过滤链(跳转登录
        //页面),若是post请求,认为是表单验证请求,进行表单验证,执行subject.login();
        //否,若是get方法的其他页面请求则保存当前请求并重定向到登录页面,非get请求可能报错吧
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        if(isLoginRequest(req)) { //是登录请求
            if("post".equalsIgnoreCase(req.getMethod())) { //form表单提交
                boolean loginSuccess = login(req); //登录
                if(loginSuccess) { //登录成功
                    return redirectToSuccessUrl(req, resp);
                }
            }

            //是get请求|登录失败,继续过滤器链,可能是被分配到controller处理/login
            return true;
        } 
        else { //不是登录请求,保存当前地址并重定向到登录界面
            saveRequestAndRedirectToLogin(req, resp);
            return false;
        }
    }

    /**
    * 登录成功后调用,若有之前的请求则重定向到它,否则到默认成功页面
    */
    private boolean redirectToSuccessUrl(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
        WebUtils.redirectToSavedRequest(req, resp, successUrl);
        return false;
    }

    /**
    * 保存当前请求并跳转登录页面
    */
    private void saveRequestAndRedirectToLogin(HttpServletRequest req,
      HttpServletResponse resp) throws IOException {
        WebUtils.saveRequest(req);
        WebUtils.issueRedirect(req, resp, loginUrl);
    }

    /**
    * 表单验证,执行登录方法
    */
    private boolean login(HttpServletRequest req) {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        try {
            SecurityUtils.getSubject().login(new UsernamePasswordToken(
                username, password));
        } catch (Exception e) {
            req.setAttribute("shiroLoginFailure", e.getClass());
            return false;
        }
        return true;
    }

    /**
    * 判断是否登录请求
    */
    private boolean isLoginRequest(HttpServletRequest req) {
        return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));
    }
}
  • 第一步,直接结束的前提除了是否已登录,还要有是否为登录请求,可能用户已登录但仍然进入登录页面重新登录;否则直接返回后被前端url为/login的controller处理,而该controller只负责失败处理,会重新重定向到登录页面,导致死循环。
  • 表单验证和登录成功的重定向已经实现了,剩下的就是请求登录页面的get请求和登录失败的处理需要自己在controller中实现。
  • 也可以继承AuthenticatingFilter来实现,它提供了很多登录相关的基础代码。另外可以参考Shiro内置FormAuthenticationFilter源码,思路是一样的。

  • 测试:访问/test.jsp,自动跳转登录页面,登录成功后跳回test.jsp。

任意角色授权拦截器
  • Shiro内置的roles(RolesAuthorizationFilter)拦截器验证用户是否拥有指定的所有角色,但没有提供能验证用户拥有任意角色的拦截器。
  • 我们自定义一个:
public class AnyRolesFilter extends AccessControlFilter {
    private String unauthorizedUrl = "/unauthorized.jsp";
    private String loginUrl = "/login.jsp";

    /**
     * 权限验证
     */
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
      Object mappedValue) throws Exception {
        String[] roles = (String[])mappedValue;

        //如果没有设置角色参数,默认成功
        if(roles == null) {
            return true;
        }

        //判断用户是否拥有所需权限
        for(String role : roles) {
            if(getSubject(request, response).hasRole(role)) { //未登录情况获取不到任何角色
                return true;
            }
        }

        //拒绝访问
        return false;//跳到onAccessDenied处理
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) 
      throws Exception {
        Subject subject = getSubject(request, response);

        //未登录
        if (subject.getPrincipal() == null) {
            //重定向到登录页面
            saveRequest(request); 
            WebUtils.issueRedirect(request, response, loginUrl);
        } 

        //已登录但授权失败
        else {
            //如果有未授权页面跳转过去
            if (StringUtils.hasText(unauthorizedUrl)) {
                WebUtils.issueRedirect(request, response, unauthorizedUrl);
            }

            //否则返回401未授权状态码
            else {
                WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
            }
        }

        return false;
    }
} 
  • 401
  • 可以在web.xml指定方法返回401时映射的错误页面

    <error-page>
        <error-code>401</error-code>
        <location>/unauthorized.jsp</location>
    </error-page>
    
  • 代码实例:ideaProjects/shiroChapter8

  • 参考文章