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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章