拦截器的使用,源码分析

问题

拦截器的使用场景是一个很常见的问题,下面是一些常见企业应用。

场景

拦截器配置

根据拦截器的name来看作用是:

  1. 拦截未登陆用户
  2. 账号安全相关
  3. 权限相关
  4. 防止CSRF攻击
  5. 风控相关

使用

实现HandlerInterceptor接口

HandlerInterceptor接口定义如下:

public interface HandlerInterceptor {

    /**
     * Intercept the execution of a handler. Called after HandlerMapping determined
     * an appropriate handler object, but before HandlerAdapter invokes the handler.
     * <p>DispatcherServlet processes a handler in an execution chain, consisting
     * of any number of interceptors, with the handler itself at the end.
     * With this method, each interceptor can decide to abort the execution chain,
     * typically sending a HTTP error or writing a custom response.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler chosen handler to execute, for type and/or instance evaluation
     * @return {@code true} if the execution chain should proceed with the
     * next interceptor or the handler itself. Else, DispatcherServlet assumes
     * that this interceptor has already dealt with the response itself.
     * @throws Exception in case of errors
     */
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception;

    /**
     * Intercept the execution of a handler. Called after HandlerAdapter actually
     * invoked the handler, but before the DispatcherServlet renders the view.
     * Can expose additional model objects to the view via the given ModelAndView.
     * <p>DispatcherServlet processes a handler in an execution chain, consisting
     * of any number of interceptors, with the handler itself at the end.
     * With this method, each interceptor can post-process an execution,
     * getting applied in inverse order of the execution chain.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler handler (or {@link HandlerMethod}) that started async
     * execution, for type and/or instance examination
     * @param modelAndView the {@code ModelAndView} that the handler returned
     * (can also be {@code null})
     * @throws Exception in case of errors
     */
    void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception;

    /**
     * Callback after completion of request processing, that is, after rendering
     * the view. Will be called on any outcome of handler execution, thus allows
     * for proper resource cleanup.
     * <p>Note: Will only be called if this interceptor's {@code preHandle}
     * method has successfully completed and returned {@code true}!
     * <p>As with the {@code postHandle} method, the method will be invoked on each
     * interceptor in the chain in reverse order, so the first interceptor will be
     * the last to be invoked.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler handler (or {@link HandlerMethod}) that started async
     * execution, for type and/or instance examination
     * @param ex exception thrown on handler execution, if any
     * @throws Exception in case of errors
     */
    void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception;

}

在调用下一个拦截器之前会先执行preHandle方法,从下一个拦截器返回之后会执行postHandle方法,最后会执行afterCompletion方法。我么只需要重写preHandler和postHandler方法。

xml文件配置

如上上图所示,将自己定义的实现了HandlerInterceptor方法的拦截器配置到"Interceptors"标签中即可。

拦截器场景分析

登陆拦截
public class LoginInterceptor implements HandlerInterceptor {

    private static final Logger logger= LoggerFactory.getLogger(LoginInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        User user = ContextUtil.getCurrentUser();
        if (user == null) {
            redirectLogin(request, response);
            return false;
        }
        if(user.getUserType() == UserTypeEnum.SHOP.getCode()){
            SSOHelper.logoutV2(request, response);
            redirectLogin(request, response);
            return false;
        }
        logger.info("[LoginInterceptor - preHandle ] requestUrl:{} currentUser: {}  ",request.getRequestURI(),hipacUser
                .getUserNickName());
        LoginUtil.setCurrentUser(user);
        return true;
    }

    private void redirectLogin(HttpServletRequest request, HttpServletResponse response) throws IOException,
                                                                                       ServletException {
        if (ContextUtil.isAjax()) {
            // ajax请求
            PrintWriter out = response.getWriter();
            response.setStatus(403);
            out.print("_NO_LOGIN_SESSION");
            out.flush();
            return;
        } else {
            try {
                StringBuilder sb=new StringBuilder(request.getRequestURI());
                sb.append("?");
                Enumeration<String> names = request.getParameterNames();
                while (names.hasMoreElements()){
                    String key = names.nextElement();
                    sb.append(key).append("=").append(request.getParameter(key)).append("&");
                }
                response.sendRedirect(request.getContextPath() + "/admin/toIndex.do");
            }catch (Exception e){
                logger.error("[LoginInterceptor - redirectLogin ] fail ",e);
                response.sendRedirect(request.getContextPath() + "/admin/toIndex.do");
            }
            return;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
                                                                                                                       throws Exception {
        LoginUtil.removeCurrentUser();
    }
}

可以看到以上拦截器的作用的检查用户是否存在,如果不存在会重定向到登陆页面。
其中获取用户信息的方法如下:

    /**
     * pc端查询登录用户信息
     *
     * @return
     */
    public static User getCurrentUser() {
        String userId = getCurrentUserId();
        if (StringUtils.isBlank(userId)) {
            return null;
        }
        UserService userService = ApplicationContextUtil.getBean(UserService.class);
        return userService.getUserById(userId);
    }
账号安全拦截器

如果用户被打标成登陆密码过于简单,会引导用户去修改密码。
实现如下:

public class AccountSecurityInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(AccountSecurityInterceptor.class);

    @Autowired
    private TagQueryApi         tagQueryApi;

    @Autowired
    private UserQueryApi        userQueryApi;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        String userId = ContextUtil.getCurrentUser().getId();
        ResultData<UserTO> userTOResultData = userQueryApi.getUserInfoById(userId);
        if(null == userTOResultData || null == userTOResultData.getData()){
            logger.error("[LoginInterceptor - redirectSecurity]用户信息获取失败.");
            return true;
        }
        ResultData<TTagValueTO> resultDate = tagQueryApi.queryTag(userId, WebConstants.USER_IS_UPDATE_PASS);
        if (null == resultDate || !resultDate.isSuccess()) {
            logger.error("[LoginInterceptor - redirectSecurity]获取用户打标接口失败.");
            return true;
        }
        //if (null != resultDate && null != resultDate.getData()) {
        if (null == resultDate.getData()) {
            if (ContextUtil.isAjax()) {
                // ajax请求
                PrintWriter out = response.getWriter();
                response.setStatus(403);
                out.print("LOW_ACCOUNT_SECURITY");
                out.flush();
            } else {
                response.sendRedirect(request.getContextPath() + "/admin/toAccountSecurity.do");
            }
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                Exception ex) throws Exception {
    }
CSRF防御拦截器

对list之外的请求进行过滤,防止CSRF攻击。

public class CSRFInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(CSRFInterceptor.class);

    public static final List<String> list = Lists.newArrayList(
                                                "http://","http://");
    // 顶级域名
    public static final String DOMAIN = "";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 验证 Referer来源
        String referer = request.getHeader("Referer");
        String domain="http://";
        String httpsDomain = "https://";
        if (StringUtils.isNotBlank(domain) && StringUtils.isNotBlank(referer)  && !(referer.startsWith(domain) || referer.startsWith(httpsDomain))){
            String host = getHost(referer);
            if(StringUtils.isNotBlank(host) && host.endsWith(DOMAIN)){
                return true;
            }
            for (String s : list) {
                if (referer.startsWith(s)){
                    return true;
                }
            }
            throw new Exception(302,"不可信的网站来源!referer="+referer+",handler="+handler);
        }
        return true;
    }
    private String getHost(String url){
        try {
            URL u = new URL(url);
            String host = u.getHost();
            host.replaceAll("/","");
            return u.getHost();
        } catch (MalformedURLException e) {
            return null;
        }
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

源码分析

springMVC中的核心就是DispatchServlet类,该类的核心又是doDispatch方法。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            ModelAndView mv = null;
            Exception dispatchException = null;

            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // Determine handler for the current request.
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
//这里先调用preHandle方法
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // Actually invoke the handler.
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(request, mv);
//这里调用posthandle方法
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
//触发执行afterCompletion方法
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Error err) {
            triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

总结

正如拦截器的名字,应用到的主要功能是对不合法,或者没有权限,或者不安全的账号进行过滤或者引导。下面会单独写一篇文章介绍接口权限的实现。

谢谢阅读!禁止转载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • |-1-更新内容[6.从仓库解析依赖的机制(重要)] 1Maven仓库作用 仓库用来存储所有项目使用到构件,在ma...
    zlcook阅读 6,018评论 0 25
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,733评论 6 342
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一种新的协议。它实...
    香橙柚子阅读 23,697评论 8 183
  • 千家榆火寒食近,万里杨花拂地垂。 逢君偶问相忆否,回身摇手掠湿眉。
    宁为肉死不为菜活阅读 265评论 0 0