这周再搞一个比较奇葩的事,老项目中做的接口验签是放到httpRequestBody中的,这就给第三方对接我们时产生了很大的误区。请求的Content-Type是application/json;charset=UTF-8
接受的controller:
@PostMapping(value = "/test")
public String register(@RequestBody @Valid ComParam<User>){
return "success";
}
ComParam:
public class ComParam<T> {
@ApiModelProperty(value = "请求头参数", required = true)
@NotNull(message = "请求头参数不能为空")
@Valid
private Header header;
@ApiModelProperty(value = "业务参数", required = true)
@NotNull(message = "业务参数不能为空")
@Valid
private T body;
}
public class Header {
@NotNull(message = "appKey不能为空")
private String appKey;
@NotNull(message = "timestamp不能为空")
private String timestamp;
@NotNull(message = "签名不能为空")
private String signature;
}
从上面就可以看出,业务参数是放到一个泛型对象中的,那对应的请求参数就要成为下面的这种:
{
"body": {
"idNo": "111111111",
"mobile": "13000000001",
"name": "张三"
},
"header": {
"appKey": "testKey",
"timestamp": "2023-04-12 00:11:22",
"signature": "eb5bfcb311c320bede5f7cb403fe1687d6aefca1"
}
}
是不是很奇怪。
更奇怪的是,我们的验签是放到切面中的,根据切面参数是否是实现ComParam来做。
这时就有一个需求:把请求体中的header参数全部放到httpHeader中,又不动controller的代码,这就难搞,需要把HttpBody的json自己转化成ComParam类型的。
请求的httpBody格式是类似这样的
{
"idNo": "111111111",
"mobile": "13000000001",
"name": "张三"
}
那就开搞:
1. 因为验签是放到切面中的,切面中能拿到的参数已经是ComParam,切属性都是null。所以需要重新从request中获取请求内容,但是request中的流是一次性的,读完就没有了,考虑暂存下这个流。需要两个类
import org.springframework.core.annotation.Order;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @author :
* @date: 2024/8/23 13:05
* @Description:过滤器缓存内容
*/
@WebFilter
@Order(1)
public class BodyReaderFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
BodyReaderRequestWrapper requestWrapper = new BodyReaderRequestWrapper(req);
filterChain.doFilter(requestWrapper, servletResponse);
}
@Override
public void destroy() {
}
}
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author :
* @date: 2024/8/23 11:45
* @Description:继承HttpServletRequestWrapper,缓存内容
*/
public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {
private final String body;
public BodyReaderRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
StringBuilder sb = new StringBuilder();
ServletInputStream ins = request.getInputStream();
BufferedReader isr = null;
try {
if (ins != null) {
isr = new BufferedReader(new InputStreamReader(ins));
char[] charBuffer = new char[128];
int readCount = 0;
while ((readCount = isr.read(charBuffer)) != -1) {
sb.append(charBuffer,0,readCount);
}
} else {
sb.append("");
}
} catch (IOException e) {
throw e;
} finally {
if (isr != null) {
isr.close();
}
}
body = sb.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());
return 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 byteArrayIns.read();
}
};
}
}
2.在切面中重新实例化ComParam,然后验证
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.Set;
@Component
@Aspect
@Slf4j
public class SignatureAspectConfig {
@Around("execution(* com.xx.controller..*.*(..))")
public Object around(ProceedingJoinPoint jp) throws Throwable {
Object[] args = jp.getArgs();
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
if (args[0] instanceof RequestParam) {
RequestParam param = (RequestParam) args[0];
// header为空,从请求头中获取
if (Objects.isNull(param.getHeader()) && StringUtils.isNotBlank(request.getHeader("appKey"))) {
param.setHeader(getHeaderByRequest(request));
MethodSignature methodSignature = (MethodSignature)((MethodInvocationProceedingJoinPoint) jp).getSignature();
// 这里获取第0个参数,和外围的args[0]对应
ParameterizedType parameterizedType = (ParameterizedType) methodSignature.getMethod().getParameters()[0].getParameterizedType();
// 这里只获取了第0个泛型的类型
String typeName = parameterizedType.getActualTypeArguments()[0].getTypeName();
// 获取泛型类型
Class<?> TT = Class.forName(typeName);
String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
Object bo = JSONObject.parseObject(body, TT);
param.setBody(bo);
}
Header header = param.getHeader();
// 手动校验器校验header
if (Objects.isNull(header)) {
throw new ParamException("缺少请求header");
}
Set<ConstraintViolation<Object>> validateHeader = validator.validate(header);
String headerErrorMsg = getErrorMsg(validateHeader);
if (StringUtils.isNotBlank(headerErrorMsg)) {
throw new ParamException(headerErrorMsg);
}
if (Objects.isNull(param.getBody())) {
throw new ParamException("业务参数不能为空");
}
// 手动校验body
Set<ConstraintViolation<Object>> validateBody = validator.validate(param.getBody());
String bodyErrorMsg = getErrorMsg(validateBody);
if (StringUtils.isNotBlank(bodyErrorMsg)) {
throw new ParamException(bodyErrorMsg);
}
}
主要点是缓存httpRequest的内容+获取类泛型属性的真实运行类