欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Controller与Filter如何交替使用

程序员文章站 2024-03-24 11:21:04
...

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

  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]微信小程序登录接口