web開發安全之請求及返回流資料加解密實踐

彭于晏發表於2018-03-30

web開發過程中對post請求過來的整個請求流資料,怎樣保證post在傳輸過程中被擷取後無法獲取到使用者提交請求實際資料,保證請求安全,在實踐過程中我們採用過濾器(Filter)來實現流擷取完成這個程式碼post請求流資料及返回資料加解密

一、請求資料流解密

1 請求資料提交filter進行資料過濾,過濾器類主要用於建立HttpServletRequest,實現解密,將解密後的request返回到下層

    public class CharacterFilter implements Filter{
    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                      FilterChain chain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest)req;
                 TsRequest wrapRequest= new TsRequest(request,request.getParameterMap());
                chain.doFilter(wrapRequest, res);
             }
   }

2、提交資料解密:前端或者app端對提交資料流資料解密,繼承HttpServletRequestWrapper方法實現自己的HttpServletRequest,拷貝原始資料流,getParameterValues寫入解密方法,Spring框架進行自動解包是會通過getParameterValues讀取資料流,進行自動解包,將資料set到物件,完成資料解密過程(copy資料流主要解決流只能讀取一次問題)

    public class TsRequest extends HttpServletRequestWrapper{
    private Map params;
    public static final int BUFFER_SIZE = 4096;
    private byte[] requestBody = null;

    public TsRequest(HttpServletRequest request, Map newParams) {
        super(request);
        this.params = newParams;
        // 快取請求body 讀取Post資料
        String method = request.getMethod();
        if (("POST").equals(request.getMethod())) {
            try {
                ByteArrayOutputStream out = copy(request.getInputStream());
                requestBody = out.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static ByteArrayOutputStream copy(InputStream in) throws IOException {
        String data = convertStreamToString(in);
        if (data != null && data.indexOf("time") == -1) {
            try {
                data = AESDataUtils.decrypt(data);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        byte bytes[] = data.getBytes("UTF-8");
        ByteArrayOutputStream out = new ByteArrayOutputStream(bytes.length);
        out.write(bytes, 0, bytes.length);
        out.flush();
        return out;
    }

    public static String convertStreamToString(InputStream is)
            throws UnsupportedEncodingException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is,
                "UTF-8"));
        StringBuilder sb = new StringBuilder();

        String line = null;
        try {
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return sb.toString();
    }

    /**
     * 重寫 getInputStream()
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (requestBody == null) {
            requestBody = new byte[0];
        }
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }

    /**
     * 重寫 getReader()
     */
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    public Map getParameterMap() {
        return params;
    }

    public Enumeration getParameterNames() {
        Vector l = new Vector(params.keySet());
        return l.elements();
    }

    public String[] getParameterValues(String name) {
        Object v = params.get(name);
        if (v == null) {
            return null;
        } else if (v instanceof String[]) {
            String[] value = (String[]) v;
            for (int i = 0; i < value.length; i++) {
                // AES解密資料
            value[i]=AESDataUtils.decrypt(value[i]);
                if (StringUtils.isNoneEmpty(value[i])) {
                    value[i] = value[i].replaceAll("<", "&lt;");
                    value[i] = value[i].replaceAll(">", "&gt;");
                }
            }
            return (String[]) value;
        } else if (v instanceof String) {
            String value = (String) v;
         value=AESDataUtils.decrypt(value);
            // AES解密資料
            if (StringUtils.isNoneBlank(value)) {
                value = value.replaceAll("<", "&lt;");
                value = value.replaceAll(">", "&gt;");
            }
            return new String[] { (String) value };
        } else {
            return new String[] { v.toString() };
        }
    }

    public String getParameter(String name) {
        Object v = params.get(name);
        if (v == null) {
            return null;
        } else if (v instanceof String) {
            String value = (String) v;
            value = AESDataUtils.decrypt(value);
            // AES解密資料
            if (StringUtils.isNoneBlank(value)) {
                value = value.replaceAll("<", "&lt;");
                value = value.replaceAll(">", "&gt;");
            }
            return (String) value;
        } else if (v instanceof String[]) {
            String[] strArr = (String[]) v;
            if (strArr.length > 0) {
                String value = strArr[0];
                String aesdata=AESDataUtils.decrypt(value);
                if(StringUtils.isNoneEmpty(aesdata)){
                    value =aesdata ;
                }
                // AES解密資料
                if (StringUtils.isNoneBlank(value)) {
                    value = value.replaceAll("<", "&lt;");
                    value = value.replaceAll(">", "&gt;");
                }
                return value;
            } else {
                return null;
            }
        } else {
            return v.toString();
        }
    }

}

二、返回資料流加密

1、繼承過濾器介面實現返回資料過濾攔截,將返回資料轉換為代理類,通過代理類改變處理返回資料流進行加密並將加密後資料重新寫入到流中返回(使用代理類主要是為了解決流只能讀取一次問題)

    public class ResponseFilter implements Filter{
     @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain filterChain) throws IOException, ServletException {
            filterChain.doFilter(request, response);
            ResponseWrapper wrapperResponse = new ResponseWrapper(
                    (HttpServletResponse) response);// 轉換成代理類
            // 這裡只攔截返回,直接讓請求過去,如果在請求前有處理,可以在這裡處理
            filterChain.doFilter(request, wrapperResponse);
            byte[] content = wrapperResponse.getContent();// 獲取返回值
            if (content.length > 0) {

                String str = new String(content, "UTF-8");
                String ciphertext = null;

                try {
                    ciphertext = AESDataUtils.encrypt(str);
                    // ......根據需要處理返回值
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //將加密後資料重新寫如後重新整理資料流
                ServletOutputStream out = response.getOutputStream();
                out.write(ciphertext.getBytes());
                out.flush();
            }
    }
}

2.返回值輸出代理,通過代理獲取返回值資訊內容

public class ResponseWrapper extends HttpServletResponseWrapper  
{  
  
    private ByteArrayOutputStream buffer;  
  
    private ServletOutputStream out;  
  
    public ResponseWrapper(HttpServletResponse httpServletResponse)  
    {  
        super(httpServletResponse);  
        buffer = new ByteArrayOutputStream();  
        out = new WrapperOutputStream(buffer);  
    }  
  
    @Override  
    public ServletOutputStream getOutputStream()  
        throws IOException  
    {  
        return out;  
    }  
  
    @Override  
    public void flushBuffer()  
        throws IOException  
    {  
        if (out != null)  
        {  
            out.flush();  
        }  
    }  
  
    public byte[] getContent()  
        throws IOException  
    {  
        flushBuffer();  
        return buffer.toByteArray();  
    }  
  
    class WrapperOutputStream extends ServletOutputStream  
    {  
        private ByteArrayOutputStream bos;  
  
        public WrapperOutputStream(ByteArrayOutputStream bos)  
        {  
            this.bos = bos;  
        }  
  
        @Override  
        public void write(int b)  
            throws IOException  
        {  
            bos.write(b);  
        }  
    }  
  
}  

三、使用工具類程式碼

AES加密工具類加解密程式碼模組,主要http請求加密過程會進行URL編碼傳入,需要進行URLDecoder.decode進行解碼後才能進行解密操作

     public class AESDataUtils {
     /**
     * 解密
     * @param value
     * @return
     */
            public static String decrypt(String value){
                if(StringUtils.isNoneEmpty(value)){
                    if(value.indexOf("%")>-1){
                        value=URLDecoder.decode(value);
                    }
                }
                try { 
                    return AESUtils.decrypt(value);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                return null;
            }
            /**
             * 加密
             * @param value
             * @return
             */
            public static String encrypt(String value){
                value=AESUtils.encrypt(value);
                value=URLEncoder.encode(value);
                return value;
            }
}

實際AES加解密程式碼塊,本文件採用aes進行加密,也可以自行可以換其它加密方式進行加密

    public class AESUtils {
    private static String secretKey = "祕鑰";
    private static String ivParameter = "";
    
    /**
     * aes 解密
     * 
     * @param sSrc
     * @return
     * @throws Exception
     */
    public static String decrypt(String sSrc) throws Exception {
        try {
            byte[] raw = secretKey.getBytes("ASCII");
            SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
            Cipher cipher = Cipher.getInstance("AES / CBC / PKCS5Padding");
            IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
            byte[] encrypted1 = new BASE64Decoder().decodeBuffer(sSrc);// 先用base64解密
            byte[] original = cipher.doFinal(encrypted1);
            String originalString = new String(original, "UTF-8");
            return originalString;
        } catch (Exception ex) {
            return null;
        }
    }
    
     //加密
    public static String encrypt(String sSrc) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            byte[] raw = secretKey.getBytes();
            SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
            //使用CBC模式,需要一個向量iv,可增加加密演算法的強度
            IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
            //此處使用BASE64做轉碼。
            return new BASE64Encoder().encode(encrypted);
        } catch (Exception ex) {
            return null;
        }
    }

}

以上為加解密在實際開發過程中程式碼,程式碼提交是對於>和<等符號進行過濾,防止sql注入,在開發過程中可以參考此程式碼進行適當修改進行使用

相關文章