java filter修改request内容和response内容
背景
最近在做一个开放平台,因为是对外开放的api,所以针对api做了签名和加密机制
请求阶段:原始request body 加密 -> encrypt body -> 签名,放到header
响应阶段:原始response body 加密 -> encrypt body -> 签名,放到header
方案
对于这种通用的http请求处理,最容易想到的就是通过java servlet filter或者spring mvc interceptor来做,具体原理可以参见下图
对于spring mvc interceptor,可以看下接口定义
因为需要针对request做签名验证和解密操作,因此需要对request和response做修改,springmvc中,request body和response body都是基于流(stream)来做的,但是stream只能读取一次;因此我们需要对request和response做一些处理
事实上,springmvc interceptor并不适合此场景,具体可以参考HandlerInterceptor接口定义
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
此处修改request或者response并不会对后续的请求生效,因为HttpServletRequest,HttpServletResponse都未提供对流的修改操作
此时只能选择java servlet filter,我们看下filter接口定义
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
default void destroy() {
}
}
通常我们实现一个filter是流程如下
public class XxxFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!condition) {
// 错误处理
// ...
} else {
// 继续往下,交给下一个
chain.doFilter(request, response)
}
}
}
事实上,我们依然无法修改request和reposne的stream对象,但是chain.doFilter时,我们可以传递一个和原始request, response不一样的对象
那么此处的filter流程如下
1、从header读取签名
2、从request读取request body,解密
3、校验签名,签名不通过,直接返回错误
4、构造新的request,request body内容是request body解密后的内容
5、构造新的response
6、根据新的request, resepond,通过chain.doFilter()继续往下执行
7、读取response body部分,并加密
8、生成签名,放入header
9、通过原始reponse,写入response body加密后的内容
整体框架代码如下
public class ApiSignFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 读取header内容,app_key,签名等内容
String appKey = httpServletRequest.getHeader(APP_KEY);
String sign = httpServletRequest.getHeader(AUTHORIZATION_KEY);
String nonceStr = httpServletRequest.getHeader(NONCE_STR_KEY);
String timestamp = httpServletRequest.getHeader(TIMESTAMP_KEY);
// 缺少header,直接返回错误
if (StringUtils.isBlank(appKey) || StringUtils.isBlank(sign)) {
noOkResp((HttpServletResponse) response, HttpStatus.UNAUTHORIZED);
return;
}
// 读取body
String body = null;
if (httpServletRequest.getContentLength() > 0) {
body = new String(IOUtils.toByteArray(httpServletRequest.getInputStream()), request.getCharacterEncoding());
}
// 根据规则生成签名
String verifySign = generateSign(httpServletRequest.getRequestURL().toString(), body, nonceStr, timestamp);
// 校验签名,校验失败直接返回
if (!StringUtils.equals(sign, verifySign)) {
noOkResp((HttpServletResponse) response, HttpStatus.UNAUTHORIZED);
return;
}
// 解密request body
String postRequestBody = null;
if (StringUtils.isNotBlank(body)) {
JSONObject bodyMap = JSON.parseObject(body);
postRequestBody = clientHelper.decryptBody((String) bodyMap.get(ENCRYPT_BODY_KEY));
}
// 构造新的request和response,继续执行请求
AuthHttpServletRequestWrapper requestWrapper = new AuthHttpServletRequestWrapper(httpServletRequest, postRequestBody);
AuthHttpServletResponseWrapper responseWrapper = new AuthHttpServletResponseWrapper((HttpServletResponse) response);
chain.doFilter(requestWrapper, responseWrapper);
// 请求失败处理
if (responseWrapper.getStatus() != HttpStatus.OK.value()) {
response.getOutputStream().write(responseWrapper.getResponseData());
response.getOutputStream().flush();
return;
}
// 读取body并加密
String respBody = new String(responseWrapper.getResponseData(), responseWrapper.getCharacterEncoding());
String encryptRespBody = clientHelper.encryptBody(respBody);
Map<String, String> encryptBodyMap = new HashMap<>();
encryptBodyMap.put(ENCRYPT_BODY_KEY, encryptRespBody);
String postBody = JSON.toJSONString(encryptBodyMap);
// 生成签名并写入header
nonceStr = UUID.randomUUID().toString();
long respTs = System.currentTimeMillis();
String responseSign = generateSign(requestWrapper.getRequestURL().toString(), postBody, nonceStr, respTs);
((HttpServletResponse) response).addHeader(NONCE_STR_KEY, nonceStr);
((HttpServletResponse) response).addHeader(TIMESTAMP_KEY, String.valueOf(respTs));
((HttpServletResponse) response).addHeader(AUTHORIZATION_KEY, responseSign);
// 写入加密后的reseponse body
response.getWriter().write(postBody);
response.getWriter().flush();
}
具体的RequestWrapper,ResponseWrapper代码如下
public class AuthHttpServletRequestWrapper extends HttpServletRequestWrapper {
/// 这里缓存了body
private byte[] body;
public AuthHttpServletRequestWrapper(HttpServletRequest request, String data) {
super(request);
body = data == null ? new byte[0] : data.getBytes(StandardCharsets.UTF_8);
}
// 这里保证了stream可以多次读取
@Override
public ServletInputStream getInputStream() {
return new RRServletInputStreamWrapper(body);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
public class RRServletInputStreamWrapper extends ServletInputStream {
private InputStream inputStream;
public RRServletInputStreamWrapper(byte[] data) {
super();
this.inputStream = new ByteArrayInputStream(data);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return this.inputStream.read();
}
}
public class AuthHttpServletResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer;
private ServletOutputStream outputStream;
private PrintWriter writer;
public AuthHttpServletResponseWrapper(HttpServletResponse response) throws UnsupportedEncodingException {
super(response);
this.buffer = new ByteArrayOutputStream();
// 这里不是原始response可以拿到的outputStream,这里做了缓存,因此写入不会影响到response
this.outputStream = new BufferedServletOutputStreamWrapper(buffer);
this.writer = new PrintWriter(new OutputStreamWriter(buffer, this.getCharacterEncoding()));
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return this.outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
return this.writer;
}
@Override
public void flushBuffer() throws IOException {
if (outputStream != null) {
outputStream.flush();
}
if (writer != null) {
writer.flush();
}
}
@Override
public void reset() {
buffer.reset();
}
public byte[] getResponseData() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
}
public class BufferedServletOutputStreamWrapper extends ServletOutputStream {
private OutputStream outputStream;
public BufferedServletOutputStreamWrapper(OutputStream outputStream) {
super();
this.outputStream = outputStream;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
this.outputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
super.write(b, 0, b.length);
}
}