暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

浅谈设计模式-装饰者模式之HTTP请求加密解密

仑哥讲JAVA 2019-01-02
472

    实际项目开发中,我们经常需要对项目进行重构,进行一些系统架构升级,或者一些功能增强,例如我们如果有以下需求:

    现有项目A对接外围系统B,通过Http+Post方式交互,交互报文采用json格式,要求双方对交互报文正文全部进行对称加密,以防信息被拦截破解。

    项目中一般有大量的接口交互,在每一个类中去做这个加密解密工作显然是非常不现实的。如果是Tomcat部署的SpringMVC项目,或者SpringBoot项目,我们很容易想到需要在接口交互中增加一个过滤器,接受请求,对HttpServletRequest进行解密,然后在送出请求时对HttpServletResponse进行加密。

    简易UML结构如下:

    HttpServletRequest有一个实现类(RequestFacade)和一个包装类(HttpServletRequestWrapper),这便是装饰者模式,动态的给对象增加一些新的功能,或者修改原有的一些的功能,要求装饰者和被装饰的类实现同一个接口,装饰对象持有被装饰者对象的实例。

    实现类我们不需太多关注,不同的web容器可能有不同实现,我们要做的是对包装类进行修改,使其满足我们的需求。

    我们已经知道(有兴趣可以自己看下SpringMvc的源码实现)在HttpServletRequest的默认实现中,读取post请求的正文body是通过getInputStream()方法实现的,如下:

    那么问题来了,返回的是一个InputStream的子类,我们知道,流只能被读取一次,如果我们读取了加密了body,如何放回去呢?

    我们需要继承并修改包装类的相关方法,使得可以读取InputStream后再“放回去”,针对request如下:

package com.allen.design.Wrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

private final String body;
   public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
       StringBuilder stringBuilder = new StringBuilder();
       BufferedReader bufferedReader = null;
       try {
InputStream inputStream = request.getInputStream();
           if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
               char[] charBuffer = new char[128];
               int bytesRead = -1;
               while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
               }
} else {
stringBuilder.append("");
           }
} catch (IOException ex) {
throw ex;
       } finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
               } catch (IOException ex) {
throw ex;
               }
}
}
body = stringBuilder.toString();
   }
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request,String requestBody){
super(request);
       this.body = requestBody;
   }

@Override
   public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
       ServletInputStream servletInputStream = new ServletInputStream() {
@Override
           public boolean isFinished() {
return false;
           }

@Override
           public boolean isReady() {
return false;
           }

@Override
           public void setReadListener(ReadListener listener) {

}

public int read() throws IOException {
return byteArrayInputStream.read();
           }
};
       return servletInputStream;
   }

@Override
   public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
   }

public String getBody() {
return this.body;
   }
}

    通过定义一个String类型的body变量,达到读取到InputStream后,再通过特殊的构造器装饰HttpServletRequest。

    同样类似继承并修改Response的包装类:

package com.allen.design.Wrapper;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
public class ResponseWrapper extends HttpServletResponseWrapper {

private ByteArrayOutputStream bytes = new ByteArrayOutputStream();
   private HttpServletResponse response;
   private PrintWriter pwrite;
   public ResponseWrapper(HttpServletResponse response) {
super(response);
       this.response = response;
   }

@Override
   public ServletOutputStream getOutputStream() throws IOException {
return new MyServletOutputStream(bytes); // 将数据写到 byte 中
   }

/**
    * 重写父类的 getWriter() 方法,将响应数据缓存在 PrintWriter 中
    */
   @Override
   public PrintWriter getWriter() throws IOException {
try{
pwrite = new PrintWriter(new OutputStreamWriter(bytes, "utf-8"));
       } catch(UnsupportedEncodingException e) {
e.printStackTrace();
       }
return pwrite;
   }

/**
    * 获取缓存在 PrintWriter 中的响应数据
    * @return
    */
   public byte[] getBytes() {
if(null != pwrite) {
pwrite.close();
           return bytes.toByteArray();
       }

if(null != bytes) {
try {
bytes.flush();
           } catch(IOException e) {
e.printStackTrace();
           }
}
return bytes.toByteArray();
   }

class MyServletOutputStream extends ServletOutputStream {
private ByteArrayOutputStream ostream ;
       public MyServletOutputStream(ByteArrayOutputStream ostream) {
this.ostream = ostream;
       }

@Override
       public void write(int b) throws IOException {
ostream.write(b); // 将数据写到 stream 中
       }

@Override
       public boolean isReady() {
return false;
       }

@Override
       public void setWriteListener(WriteListener listener) {

}
}

}

    HttpServletResponse的包装类比较简单,因为我们可以直接通过OutputStream的write方法把我们需要的内容写入。

    使用上述两个包装类,我们写出如下过滤器:

package com.allen.design.Wrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.netty.handler.codec.base64.Base64Decoder;
import org.jboss.netty.handler.codec.base64.Base64Encoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.*;
@Component
@WebFilter(filterName = "colationFilter", urlPatterns = "/*")
public class HTTPBasicAuthorizeAttribute implements Filter {

private static final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(
Arrays.asList("/hello")));
   private Logger logger = LoggerFactory.getLogger(HTTPBasicAuthorizeAttribute.class);
   @Override
   public void destroy() {
logger.debug("后台token过滤器,溜了溜了溜了溜了");
       //可以日志管理添加
   }

@Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("后台token过滤器检测");
       HttpServletRequest req = (HttpServletRequest) request;
       HttpServletResponse res = (HttpServletResponse) response;
       String path = req.getRequestURI().substring(req.getContextPath().length()).replaceAll("[/]+$", "");
       boolean allowedPath = ALLOWED_PATHS.contains(path);
       String resultStatusCode = "PERMISSION_DENIED";
       if (allowedPath) {
//System.out.println("这里是不需要处理的url进入的方法");
           chain.doFilter(request, response);
           return;
       } else {
//System.out.println("这里是需要处理的url进入的方法");
           resultStatusCode = checkHTTPBasicAuthorize(request);
           if (resultStatusCode.equals("SINGTIMEOUT")) {//超时
               toResponse((HttpServletResponse) response, 2, (HttpServletRequest) request);
               return;
           } else if (resultStatusCode.equals("PERMISSION_DENIED")) {//权限不够
               toResponse((HttpServletResponse) response, 0, (HttpServletRequest) request);
               return;
           }
logger.info("后台token过滤器检测通过");
       }
/**
        * 以下是全局包装reques和response,实现记录日志的效果
        * 实现数据加密和解密
        * @param buffer 字节流的请求体
        * @param respBody 返回的body
        */
       ServletRequest requestWrapper = null;
       requestWrapper = new BodyReaderHttpServletRequestWrapper(req);
       if (requestWrapper == null) {
chain.doFilter(request, response);
       } else {
long startTime = System.currentTimeMillis();
           int len = request.getContentLength();
           ServletInputStream iii = requestWrapper.getInputStream();
           byte[] buffer = new byte[len];
           iii.read(buffer, 0, len);
           String host = request.getRemoteHost();
           /**
            * 若存储则需要替换换行符,空格等
            * */
           String encodeReqBody = new String(buffer);
           String decodeReqBody = String.valueOf(Base64Utils.decodeFromString(encodeReqBody));
           SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
           logger.info(format.format(new Date()) + " " + host + " " + resultStatusCode + "请求\"" + path + "\",内容为:" + decodeReqBody);
           ServletRequestWrapper requestWrapperDecode = new BodyReaderHttpServletRequestWrapper(req, decodeReqBody);
           ResponseWrapper responseWrapper = new ResponseWrapper(res);
           chain.doFilter(requestWrapperDecode, responseWrapper);
           byte[] respBody = responseWrapper.getBytes();
           /**
            * 这里可以压缩返回内容,例如gzip
            */
/*                ByteArrayOutputStream bout = new ByteArrayOutputStream();
               GZIPOutputStream gzipOut = new GZIPOutputStream(bout); // 创建 GZIPOutputStream 对象
               gzipOut.write(respBody); // 将响应的数据写到 Gzip 压缩流中
               gzipOut.close(); // 将数据刷新到  bout 字节流数组
               byte[] bts = bout.toByteArray();
               res.setHeader("Content-Encoding", "gzip"); // 设置响应头信息
               //写入bts即可*/
           String decodeRespBody = Base64Utils.encodeToString(respBody);
           logger.info("响应:" + new String(respBody));
           logger.info("耗时:" + (System.currentTimeMillis() - startTime) + "ms");
           res.setContentLength(-1);
           res.getOutputStream().write(decodeRespBody.getBytes());
       }
}

/**
    * 响应
    *
    * @param response
    * @param i        类型
    * @throws IOException
    */
   private void toResponse(HttpServletResponse response, int i, HttpServletRequest request) throws IOException {
HttpServletResponse httpResponse = response;
       httpResponse.setCharacterEncoding("UTF-8");
       httpResponse.setContentType("application/json; charset=utf-8");
       httpResponse.setHeader("Access-Control-Allow-Origin", "*");
       httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
       httpResponse.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PATCH,PUT");
       httpResponse.setHeader("Access-Control-Max-Age", "3600");
       httpResponse.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,x-requested-with,X-Custom-Header," +
"Content-Type,Accept,Authorization");
       String method = request.getMethod();
       if ("OPTIONS".equalsIgnoreCase(method)) {
logger.info("OPTIONS请求");
           httpResponse.setStatus(HttpServletResponse.SC_ACCEPTED);
       }

ObjectMapper mapper = new ObjectMapper();
       PrintWriter writer = httpResponse.getWriter();
       writer.write("校验失败");
       writer.close();
       if (writer != null)
writer = null;
   }

@Override
   public void init(FilterConfig arg0) throws ServletException {
logger.info("后台token过滤器启动");
   }

/**
    * 检测请求同token信息
    *
    * @param request
    * @return
    */
   private String checkHTTPBasicAuthorize(ServletRequest request) {
return "PASS";
   }
}

    为了方便演示,我们只使用了base64的加密解密(具体可以用其他实现),我们排除了/hello的url请求,创建一个boot入口类,并注解为RestController测试:

package com.allen.design;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class DesignApplication {
private final static Logger logger = LoggerFactory.getLogger(DesignApplication.class);
   public static void main(String[] args) {
SpringApplication.run(DesignApplication.class, args);
   }
@RequestMapping("/hello")
String hello(@RequestBody String body){
logger.info(body);
       return "我没有加密";
   }
@RequestMapping("/world")
String world(@RequestBody String body){
logger.info(body);
       return "我被加密了";
   }

}

    使用postman进行测试,首先使用正文{"content":"你好"}请求

http://localhost:8080/hello,返回预期结果:我没有加密。

    如果请求/world,就不能用普通的正文了,需要用base64加密的,我们使用https://www.base64decode.org/在线加密上述字符串,结果为:

    eyJjb250ZW50Ijoi5L2g5aW9In0=。

    然后去请求http://localhost:8080/world,结果如下:

    5oiR6KKr5Yqg5a+G5LqG

    按照预期来讲,应该是"我被加密了",怎么是一串英文字母呢?因为这是被base64加密后传输的,使用刚才网站进行解密,得到结果:

    我被加密了

    如上便通过实例解释了装饰者模式(注意本文为了方便,直接通过继承的方式继承了wrapper类,也可以直接通过wrapper类似的方式创建新的装饰者),同时解决了本文开始提到的需求,实例中包含对请求的校验和加密解密,以及URL过滤,具体可以自行拓展。

    装饰者模式的应用场景一般有以下场合:

    1、动态扩展一个类的功能,通常可以替代继承;

    2、动态的为一个对象增加功能,而且还能动态撤销,继承不能做掉动态增删。

    优点也是如上,缺点会产生过多相似的对象,不容易排错,所以要控制装饰者不要滥用。


        

    

文章转载自仑哥讲JAVA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论