实际项目开发中,我们经常需要对项目进行重构,进行一些系统架构升级,或者一些功能增强,例如我们如果有以下需求:
现有项目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、动态的为一个对象增加功能,而且还能动态撤销,继承不能做掉动态增删。
优点也是如上,缺点会产生过多相似的对象,不容易排错,所以要控制装饰者不要滥用。




