目录
- 概述
- 方案思路
- 封装HttpServletRequest请求
- 把可重复读请求体通过过滤器往下传
- 记录入参日志
- 实现入参记录拦截器
- 注册拦截器
- 记录返参日志
概述
请求日志几乎是所有大型企业级项目的必要的模块,请求日志对于我们来说后期在项目运行上线一段时间用于排除异常、请求分流处理、限制流量等。
请求日志一般都会记录请求参数、请求地址、请求状态(Status Code)、SessionId、请求方法方式(Method)、请求时间、客户端IP地址、请求返回内容、耗时等等。如果你得系统还有其他个性化的配置,也可以完成记录。
记录请求参数时,由于servlet.getInputStream的数据只能读取一次,因此需要先把数据缓存下来,构造返回流,保证之后的Controller可以正常读取到请求体的数据。
方案思路
- 封装HttpServletRequest请求类,改类在构造方法中将请求的输入流中的数据缓存了起来,保证之后的处理可以重复读取输入流中的数据。
- 实现过滤器,把上步封装的请求类传下去,保证Controller可以正常读取输入流中的数据。
- 添加拦截器,读取输入流中的数据。
- 读取返回参数。
封装HttpServletRequest请求
package com.example.demo.intercept;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* @author
* @date 封装HttpServletRequest请求
*/
public class RequestWrapper extends HttpServletRequestWrapper {
private final String body;
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesBody = -1;
while ((bytesBody = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesBody);
}
} else {
stringBuilder.append("");
}
} catch (IOException e) {
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
body = stringBuilder.toString();
}
@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 readListener) {
}
@Override
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;
}
}
把可重复读请求体通过过滤器往下传
防止请求流读取一次后就没有了,之后的不管是过滤器、拦截器、处理器都是读的已经缓存好的数据,实现可重复读。
package com.example.demo.intercept;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @author
* @date 防止请求流读取一次后就没有了
*/
@Component
@WebFilter(urlPatterns = "/**")
public class RecordChannelFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest request = null;
if (servletRequest instanceof HttpServletRequest) {
request = new RequestWrapper((HttpServletRequest) servletRequest);
}
if (request ==null){
//防止流读取一次就没有了,将流传递下去
filterChain.doFilter(servletRequest,servletResponse);
}else {
filterChain.doFilter(request,servletResponse);
}
}
@Override
public void destroy() {
}
}
记录入参日志
实现入参记录拦截器
通过拦截器的方式实现用户入参记录。
package com.example.demo.intercept;
import com.alibaba.fastjson.JSONObject;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import io.jsonwebtoken.Claims;
import javax.annotation.Resource;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author
* @date 记录用户操作记录入参
*/
@Component
public class OperationLogInterceptor implements HandlerInterceptor {
/**
* Jwt secert串,需要与加密token的秘钥一致
*/
public static final String JWT_SECERT = "23142d7a9s7d66970ad07d8sa";
/**
* 需要记录的接口URL
*/
private static List<String> pathList = new ArrayList<>();
static {
pathList.add("/mdms/model");
}
@Resource
private FunctionDOMapper functionDOMapper;//菜单动能sql
@Resource
private UserOperationHistoryDOMapper historyDOMapper;//操作日志记录表
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String servletPath = "" + request.getServletPath();
String method = request.getMethod();
pathList.forEach(path -> {
if (servletPath.contains(path)){
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
//获取token在请求中
if (cookie.getName().equals("_qjt_ac_")) {
String token = cookie.getValue();
/**解密token**/
byte[] encodeKey = Base64.decode(JWT_SECERT);
Claims claims = null;
try {
SecretKeySpec keySpec = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
claims = Jwts.parser().setSigningKey(keySpec).parseClaimsJws(token).getBody();
} catch (Exception e) {
return;
}
//用户账号
String account = claims.getSubject();
//查询URL在功能表中的功能
functionDOMapper.selectOne(servletPath, method);
//获取入参
RequestWrapper requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper(request);
}
Map<String,Object> map = new HashMap<>();
map.put("parameter", JSONObject.parse(requestWrapper.getBody()));
historyDOMapper.insert(map);//将操作信息入库
}
}
}
}
});
return true;
}
}
注册拦截器
package com.example.demo.config;
import com.example.demo.intercept.OperationLogInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author
* @date 注册拦截器
*/
public class WebConfig implements WebMvcConfigurer {
@Bean
public HandlerInterceptor getOperationLogInterceptor() {
return new OperationLogInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(getOperationLogInterceptor()).addPathPatterns("/**").excludePathPatterns();
}
}
记录返参日志
package com.example.demo.intercept;
import com.alibaba.fastjson.JSONObject;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.ws.Response;
import java.util.*;
/**
* @author
* @date 记录用户操作日志返参
*/
@ControllerAdvice(basePackages = "项目包")
public class GetResponseBody implements ResponseBodyAdvice<Object> {
/**
* Jwt secert串,需要与加密token的秘钥一致
*/
public static final String JWT_SECERT = "23142d7a9s7d66970ad07d8sa";
/**
* 需要记录的接口URL
*/
private static List<String> pathList = new ArrayList<>();
static {
pathList.add("/mdms/model");
}
@Resource
private FunctionDOMapper functionDOMapper;//菜单动能sql
@Resource
private UserOperationHistoryDOMapper historyDOMapper;//操作日志记录表
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return false;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
String path = serverHttpRequest.getURI().getPath();
String methodValue = serverHttpRequest.getMethodValue();
pathList.forEach(serverPath -> {
if (path.contains(serverPath)) {
HashMap<String, String> cookieMap = new HashMap<>();
HttpHeaders headers = serverHttpRequest.getHeaders();
List<String> cookieList = headers.get("cookie");
if (CollectionUtils.isEmpty(cookieList)) {
return;
}
String replaceAll = cookieList.get(0).replaceAll(";", "").replaceAll(";", "");
String[] split = replaceAll.split(";");
for (String cookie : split) {
String[] param = cookie.split("=");
cookieMap.put(param[0], param[1]);
}
String token = cookieMap.get("_qjt_ac_");
byte[] encodeKey = Base64.decode(JWT_SECERT);
Claims claims = null;
try {
SecretKeySpec keySpec = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
claims = Jwts.parser().setSigningKey(keySpec).parseClaimsJws(token).getBody();
} catch (Exception e) {
return;
}
//用户账号
String account = claims.getSubject();
//查询URL在功能表中的功能
functionDOMapper.selectOne(servletPath, method);
//获取返参
List<Object> list = historyDOMapper.select("功能表参数", account);
list.sort((Object1,Object2)->Object2.getTime().compareTo(Object1.getTime()));//将查询到的操作记录按时间降序排列
Object history = list.get(0);
if (body instanceof Response) {
Response response = (Response) body;
JSONObject jsonObject = JSONObject.parseObject(history.getParam());
jsonObject.put("message",response.getMessage());
jsonObject.put("body",response.getData());
history.setParam(jsonObject.toString());
history.setDes(response.getMessage());
}
historyDOMapper.updateByPrimaryKeySelective(history);//将操作信息更新
}
});
return body;
}
}