請求包裹器


永遠不要相信來自客戶端的資料,例如,基於安全考量,輸入中若有 HTML 標籤應該過濾掉,或者將 <> 角括號置換為 HTML 替代字元 &lt;&gt;

你可以使用過濾器的方式,將使用者請求參數中的角括號字元進行替換。但問題在於,雖然你可以使用 HttpServletRequestgetParameter() 取得請求參數值,但就是沒有一個像 setParameter() 的方法,可以讓你將處理過後的請求參數重新設定給 HttpServletRequest

你也許會想要親自實作 HttpServletRequest 介面,讓 getParameter() 傳回過濾後的請求參數值,但這麼作的話,HttpServletRequest 介面上所有定義的方法都要實作,實作所有的方法是件很麻煩的事。所幸,有個 HttpServletRequestWrapper 幫你實作了 HttpServletRequest 介面,你只要繼承 HttpServletRequestWrapper 類別,並撰寫你想要重新定義的方法即可。相對應於 ServletRequest 介面,也有個 ServletRequestWrapper 類別可以使用。

請求包裹器

以下的範例透過繼承 HttpServletRequestWrapper 實作了一個請求包裹器,可以將請求參數中的角括號替換為替代字元。

package cc.openhome;

import java.util.*;
import javax.servlet.http.*;

public class CharacterRequestWrapper extends HttpServletRequestWrapper {
    private Map<String, String> escapes;

    public CharacterRequestWrapper(HttpServletRequest request, Map<String, String> escapes) {
        super(request);
        this.escapes = escapes;
    }

    @Override
    public String getParameter(String name) {
        return Optional.ofNullable(getRequest().getParameter(name))
                       .map(this::escape)
                       .orElse(null);
    }

    private String escape(String value) {
        String result = value;
        for(String origin : escapes.keySet()) {
            result = result.replaceAll(origin, escapes.get(origin));
        }
        return result;
    }
}

在繼承 HttpServletRequestWrapper 之後,必須定義建構式,透過 super() 來呼叫父類別建構式,並傳入想要包裹的原請求物件,之後若想取得被包裹的原請求物件,則可以透過 getRequest() 方法來取得。建構式中也傳入了一個 Map 物件,這個物件中「鍵」的部份為想要替換的原字元,「值」的部份則是對應的替代字元。

如果程式中想要從包裹器請求物件的 getParameter() 取得請求參數,則先從原請求物件的 getParameter() 取得值,然後進行字元替換。字元替換的方法撰寫在 escape() 方法之中。

可以使用這個請求包裹器類別搭配過濾器,以進行字元過濾的服務。例如:

package cc.openhome;

import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebFilter(
    urlPatterns={"/guestbook"},
    initParams={
        @WebInitParam(
            name="ESCAPE_LIST",
            value="/WEB-INF/escapes.txt"
        )
    }
)
public class CharacterFilter extends HttpFilter {
    private Map<String, String> escapes = new HashMap<>();

    @Override
    public void init() throws ServletException {
        try(BufferedReader reader = new BufferedReader(
                    new InputStreamReader(
                        getServletContext()
                            .getResourceAsStream(getInitParameter("ESCAPE_LIST"))))) {

            String input = null;
            while ((input = reader.readLine()) != null) {
                String[] tokens = input.split("\t");
                escapes.put(tokens[0], tokens[1]);
            }
        } catch (IOException ex) {
            Logger.getLogger(CharacterFilter.class.getName())
                    .log(Level.SEVERE, null, ex);
        }
    }

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response,
          FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new CharacterRequestWrapper(request, escapes), response);
    }
}

這個過濾器過濾字元的依據是個字元表檔案,檔案名稱可以透過過濾器初始參數來設定,並透過 getInitParameter() 來取得檔案名稱。讀入的字元對應將分別填入 Map 物件的「鍵」與「值」。

接下來在 doFilter() 之中,建立 CharacterRequestWrapper 實例,並將原請求物件傳入建構式進行包裹。然後將 CharacterRequestWrapper 實例傳入 FilterChaindoFilter() 中作為請求物件。

之後的 Filter 或 Servlet 實例,不需要也不會知道請求物件已經被包裹,在必須取得請求參數時,一樣呼叫 getParameter() 即可。

字元過濾表檔案必須放在 WEB-INF 之中,檔名為 escapes.txt,你可以自行新增要過濾的字元到檔案中,字元之間以跳位字元(\t)分隔。例如檔案內容如下:

<   &lt;
>   &gt; 

當你將這個過濾器掛上去之後,如果有使用者試圖輸入 HTML 標籤,由於角括號都被替換為替代字元。