Controller与Filter如何交替使用

阅读本文,需要具备以下基础知识:

  1. Java Web开发基础知识;
  2. Servlet及其Filter过滤机制;
  3. 了解SpringMVC/Springboot框架 ,了解Controller基本使用,Controller本质是Servlet;

一、背景

存在这样的一些业务场景:

  1. Filter用于@RestController注解下的Url拦截(比如白名单校验、鉴权等业务)的,校验成功后需要返回JSON,校验失败时则跳转至鉴权失败页面,而不是需要返回失败的JSON。比如微信小程序登录接口,需要先获取code,根据code再去获取openId,必须要把openId返回给小程序,小程序才可以正常使用,所以需要响应相对应的JSON;如果openId获取失败,则没有必要返回失败信息了,直接拦截所有请求,重定向到自定义登录url的入口,这样后面所有的请求都不需要考虑认证失败这种情况了;
  2. Filter用于@Controller的Url跳转(比如登录):成功时跳转至成功页面,失败时跳转至失败页面,但是现在要求失败时必须要加上失败提示,而不应该做二次跳转。

二、目标

  1. Java Web绕不开Controller(Servlet)和Filter,虽然之前都用过,但是都没有好好深入去了解,借此机会好好分析下二者的联系与区别;
  2. 解决上述2个业务场景下的问题,顺便总结下该如何灵活运用Controller(Servlet)和Filter;

三、步骤

  1. 以前也经常看,链接为:Servlet和Filter区别,但是概念老是记不住。总体来说,Servlet,一般是指HttpServlet。HttpServlet主要有如下默认实现:
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
       throws ServletException, IOException{
    ...
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
 		throws ServletException, IOException{
 	...
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
}

这些接口通常用来处理不同的类型请求。但是处理的流程大致相同。都是从HttpServletRequest中拿请求参数,使用HttpServletResponse响应结果至前台;
2. Filter一般是指HttpFilter。HttpFilter的核心代码如下:

protected void doFilter(HttpServletRequest request, HttpServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    chain.doFilter(request, response);
}

HttpFilter使用了责任链设计模式,在存在多个filter时,如果本过滤器处理完毕无异常需要终止,则调用chain.doFilter(request, response),由后续的filter继续处理,如果此处处理异常,则直接return,这样就跳过了FilterChain,非常灵活,而且不需要也不知道上下游的filter是什么,要怎么处理;
3. 纵观Servlet和Filter二者的请求参数都有HttpServletRequest,和HttpServletResponse 2个参数,而且在一个请求链里面,这2个参数都是同一个对象。那能不能在Filter中去响应内容呢?能不能在Filter中去做跳转呢?
4. 针对上面2个业务场景,业务场景1就是微信认证,拿到tokenId并返回,我使用的框架是Springboot+shiro(关于这块我另外写了一个专栏),为了优雅地使用shiro,在认证Filter中去认证获取openId。shiro原Filter是用来跳转的,成功后跳转至成功页面,失败时跳转至失败页面,但是我必须要返回openId,二者权衡,就覆写了Filter认证成功的分支代码,通过Filter中的HttpServletResponse把JSON响应到前端,失败时,继续跳转至登录页面。核心代码如下:

public class MyTokenAuthenticationFilter extends FormAuthenticationFilter
{
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception
    {
        boolean result = super.executeLogin(request, response);
        Session session = SecurityUtils.getSubject().getSession(true);

        WxToken wxToken = SessionUtils.getSessionObj(session);
        LOGGER.info("current token:{}", wxToken);

        String sessionId = session.getId().toString();
        wxToken.setSession_key(sessionId);
        String json = JsonUtils.toJson(wxToken);
        LOGGER.info("new token:{}", json);

        response.setContentType(ContentType.APPLICATION_JSON.toString());
        Writer writer = response.getWriter();
        writer.write(json);
        writer.flush();
        return result;
    }

    /**
     * 覆写该方法,不让跳转至主页
     *
     * @param token
     * @param subject
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
        ServletResponse response)
    {
        return false;
    }

说明下,super.executeLogin(request, response)中包含了认证成功和失败的方法,分别为:onLoginSuccess、onLoginFailure,源码如下:

protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                 ServletRequest request, ServletResponse response) throws Exception {
    issueSuccessRedirect(request, response);
    //we handled the success redirect directly, prevent the chain from continuing:
    return false;
}

protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                 ServletRequest request, ServletResponse response) {
    if (log.isDebugEnabled()) {
        log.debug( "Authentication exception", e );
    }
    setFailureAttribute(request, e);
    //login failed, let request continue back to the login page:
    return true;
}

覆写后,成功时就不会跳转了,失败时会继续跳转。
至此,场景1的问题解决。

  1. 业务场景2是PC Web端通过shiro 的FormAuthenticationFilter来做认证过滤。我使用的框架是Springboot+shiro(关于这块我另外写了一个专栏)。通过此Filter,登录成功后,返回至业务主页,登录失败后,继续跳转至登录页面。后面业务变更为登录失败后,要提示失败信息,而不是再跳转至登录页面。所以就需要按照业务场景1的方式把失败信息通过Filter Response返回至前台。代码如下:
public class MyAuthenticationFilter extends FormAuthenticationFilter
{
    /**
     * 认证成功后,也不再跳转至主页
     * <p>
     * (因为请求改成了异步请求,无法跳转)
     *
     * @param token
     * @param subject
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
        ServletResponse response) throws Exception
    {
        //设置响应格式为UTF-8,否则会乱码
        response.setContentType(ContentType.APPLICATION_JSON.toString());
        Writer writer = response.getWriter();
        String json = JsonUtils.toJson(Result.ok());
        writer.write(json);
        writer.flush();

        return false;
    }

    /**
     * 覆写认证失败的接口
     * <p>
     * 返回认证失败的提示信息,不让再返回认证失败页面
     *
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
        ServletResponse response)
    {
        LOGGER.info("start login failed flow.");
        try
        {
            //设置响应格式为UTF-8,否则会乱码
            response.setContentType(ContentType.APPLICATION_JSON.toString());
            Writer writer = response.getWriter();
            String failedMsg = I18nUtils.get(LOGIN_FAILED_KEY);
            String failedJson = JsonUtils.toJson(Result.error(failedMsg));
            writer.write(failedJson);
            writer.flush();
        }
        catch (IOException ex)
        {
            LOGGER.error("login failed.", ex);
        }
        return false;
    }
}
  1. PC Web登录使用的Controller是@Controller,而不是@RestController注解的,没有办法向前台返回JSON,所以也只能在Filter中响应JSON。但是响应JSON到前台后,需要有接收JSON的Ajax请求才行。
    原来前端Vue提交登录请求的表单代码如下:
    <form class="imgBox" method="post" action="/login">
      <img src="assets/img.png" alt="">
      <div class="frame">
        <p class="logoStyle"><img src="assets/logo.png" alt=""></p>
        <p class="terrace">xxx</p>
        <div class="nameInput">
          <input type="text" placeholder="请输入用户名" name="name" class="userName">
        </div>
        <img src="assets/icon_yonghuming1.svg" alt="" class="namePicture1">
        <img src="assets/icon_mima1.svg" alt="" class="namePicture2">
        <div class="nameInput">
          <input type="password" placeholder="请输密码" name="password" class="passWord">
        </div>   
        
        <button type="submit" class="loginBox">登录</button>
        <p  style="width:300px;height:50px;color:#fff;font-size:14px;margin-left:130px;opacity:0.4;" v-show="showView" >{{msg}}</p>
      </div>
    </form>

变更至ajax请求代码如下(前端框架Vue,对应的ajax组件一般是axios):

    <form class="imgBox" @submit.prevent="login($event)">
    ...
    </form>
    <a href="/index" style="display:none" ref="xxx"></a>
  </div>
login(event) {
        console.log("start login.");
        let count = event.target.length;

        let formData = new FormData();
        if (count && count > 2) {
          for (let i = 0; i < count; i++) {
            let element = event.target[i];
            if (element.nodeName === 'INPUT') {
              console.log("name=" + element.name + ",value=" + element.value);
              formData.append(element.name, element.value);
            }
          };
          let self = this;
          axios.post("/login", formData)
            .then((res) => {
              console.log("result:" + JSON.stringify(res));
              if (res && res.data && res.data.code < 0) {
                console.log("login failed");
                this.showView = true
                this.msg = error.msg

              } else {
                console.log("login successfully.");
                self.$refs["xxx"].click();
               
              }
            })
            .catch((error) => {
              console.log("error:" + error);
             
            });
        }
      }
    }
  1. 经过上述变更后,无论登录成功还是失败,都能从请求的Response中拿到结果JSON,但是改成了ajax请求后,也无法做登录成功后的跳转,而且后台Filter也不会跳转,所以在上述第6步中需要额外加1个隐藏的<a>标签,并在登录成功后,模拟点击<a>标签跳转的操作。关键代码就是如下:
<a href="/index" style="display:none" ref="xxx"></a>
self.$refs["xxx"].click();

至此场景2的问题也解决了。这里没有使用vue的vue-router的原因是暂时只有一个登录页面和主页。

四、总结

  1. Controller本质是Servlet,Controller的2种注解形式@RestController和@Controller一个是做ajax请求,一个是做url跳转请求的,但如果通过Filter来过滤处理的话,就没有明确的界线了,上面2个业务场景里面,第1个本来是@RestController类型的Controller,使用Filter后就变成了:成功后响应JSON,失败后,Url跳转;第2个是@Controller类型的Controller,使用Filter后就变成了:成功和失败都响应JSON,然后由前端控制成功后跳转;
  2. 熟练掌握Controller和Filter是根本之道,灵活运用解决业务问题才更有意义;

五、参考

[1]Servlet和Filter区别
[2]微信小程序登录接口

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值