問題背景
接口項目本身約定數據都是json
格式,使用@RequestBody
接收
@RestController
@RequestMapping("/app")
@Transactional(rollbackFor=Exception.class)
public class AppAction {
@PostMapping("/login")
public LoginResp login(@RequestBody @Valid LoginReq req)throws Exception{
throw new LogicException(Code.APP_VERSION_LOWER.getCode(),Code
.APP_VERSION_LOWER.getMessage());
}
}
但是請求安全需要,請求和響應數據加密解密處理,也就是在http
發送post
請求的時候,服務端收到的本身是密文,需要解密就纔是 json
。
ShO7s5vmTis0Yp4OmnqHZL+pIP9YkHZ4hw/7XfSK2QAKIY0kWsGMlNG92RVQLf079/D7iFsCNf5yilklH+BhJQo6J7iuEaxvZFQfp+vPW3A=
因此使用RequestBodyAdvice
和ResponseBodyAdvice
裏面做了解密 和 加密處理,
最近在接釘釘小程序出現了問題,因爲釘釘在發送請求的是時候,會校驗content-type
,因爲本身發的密文,不是json 格式,所以content-type=application/json;
的時候,發送密文傳是失敗的,因此,只能按照content-type=application/x-www-form-urlencoded
發送,
報錯如下:
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
服務端肯定失敗,因爲只會處理 json
解決思路:
既然,發送的請求類型不對,那麼我在服務端處理之前包裝下requet,讓它變成json格式,也就是問題變成,如果包裝requst ,把content-type
變成application/json
-
攔截器
-
Aop
-
過濾器
經過測試,最後選擇使用過濾器,用
HttpServletRequestWrapper
包裝request
後往後傳遞處理。
新建過濾器
@Component
@WebFilter(urlPatterns = "/app/**")
public class MyRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest wrapperRequest = null;
if (request instanceof HttpServletRequest) {
wrapperRequest = new MyRequestWrapper((HttpServletRequest) request);
}
chain.doFilter(wrapperRequest, response);
}
class MyRequestWrapper extends HttpServletRequestWrapper {
public MyRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public Enumeration<String> getHeaders(String name) {
if (null != name && name.equals("content-type")) {
return new Enumeration<String>() {
private boolean hasGetted = false;
@Override
public String nextElement() {
if (hasGetted) {
throw new NoSuchElementException();
} else {
hasGetted = true;
return MediaType.APPLICATION_JSON_VALUE;
}
}
@Override
public boolean hasMoreElements() {
return !hasGetted;
}
};
}
return super.getHeaders(name);
}
}
}
調試發現,還是不行
調試分析
調用鏈
RequestMappingHandlerAdapter#invokeHandlerMethod
ServletInvocableHandlerMethod#invokeAndHandle
InvocableHandlerMethod#invokeForRequest
InvocableHandlerMethod#getMethodArgumentValues
HandlerMethodArgumentResolverComposite#resolveArgument
RequestResponseBodyMethodProcessor#resolveArgument
RequestResponseBodyMethodProcessor#readWithMessageConverters
AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
調試分析
@SuppressWarnings("unchecked")
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType)
throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
try {
// 這裏的確是從 header 裏面獲取的,我們包裝類在就是在這裏修改了content-type
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
// 沒有conent-type, 默認是 字節流處理 application/octet-stream
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
......
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message;
try {
// 這裏是關鍵,待會分析
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
// 下面就是 各種 HttpMessageConverter 調用,什麼json string 都是在這裏處理
// json ---MappingJackson2HttpMessageConverter
// 自定義的 faston ---- FastonHttpMessageConverter
// string ----StringHttpMessageConverter
// form ----FormHttpMessageConverter
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
// 處理各種 RequestBodyAdvice
if (message.hasBody()) {
// 前置處理 beforeBodyRead
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
// 後置處理 afterBodyRead
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
// body == null 經常報錯 Request body is missing 就是在這裏了
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && !message.hasBody())) {
return null;
}
// 開始的那個報錯 Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported 就是這裏拋出的
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}
// 以上步驟都對
MediaType selectedContentType = contentType;
Object theBody = body;
// 日誌輸出 請求封裝結果
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
//Read "application/json;charset=UTF-8" to [LoginReq{userName='dura-sale-1', password='96e79218965eb72c92a549dd5a330112', lon=null, lat=null, ci (truncated)...]
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
});
return body;
}
繼續分析EmptyBodyCheckingHttpInputMessage
// 把HttpInputMessage 包裝爲 EmptyBodyCheckingHttpInputMessage
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
所以,其實,各種請求的信息,還是從HttpInputMessage
裏面獲取的
查看構造函數
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
// 這裏很關鍵,直接去獲取請求的 inputStream了
InputStream inputStream = inputMessage.getBody();
......
}
繼續查看getBody
//ServletServerHttpRequest#getBody
@Override
public InputStream getBody() throws IOException {
// 判斷是不是 form 表單,
//也就是content-type=application/x-www-form-urlencoded;charset=UTF-8
if (isFormPost(this.servletRequest)) {
// 是的話,獲取的 request.getParameterMaps()
return getBodyFromServletRequestParameters(this.servletRequest);
}
else {
// 不是的話,獲取 request.getInputStream();
return this.servletRequest.getInputStream();
}
}
但是很坑的是 isFormPost
的判斷
//ServletServerHttpRequest#isFormPost
private static boolean isFormPost(HttpServletRequest request) {
// 直接獲取的是 request.getContentType(),而不是 request.getHeader("content-type")
String contentType = request.getContentType();
return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
HttpMethod.POST.matches(request.getMethod()));
}
這就很坑了啊,說明,我們在HttpServletRequestWrapper
修改的header
裏面的conent-type
沒起到作用啊 ,所以肯定走到邏輯request.getInputStream();
去處理了
這裏的處理,後續也可以,但是仍然有個很坑的問題,參數處理中直接把特殊字符轉義了,導致解密邏輯失敗
比如密文是:
ShO7s5vmTis0Yp4OmnqHZL+pIP9YkHZ4hw/7XfSK2QAKIY0kWsGMlNG92RVQLf079/D7iFsCNf5yilklH+BhJQo6J7iuEaxvZFQfp+vPW3A=
//被轉義的密文
ShO7s5vmTis0Yp4OmnqHZL+pIP9YkHZ4hw%2F7XfSK2QAKIY0kWsGMlNG92RVQLf079%2FD7iFsCNf5yilklH+BhJQo6J7iuEaxvZFQfp+vPW3A=
這還是隻是/
被轉義了,其他沒有用到的,還不知道多少坑呢,所以換個角度處理
上面分析,既然判斷邏輯是request.getContentType()
,所以,覆蓋一下嘛,修改MyRequestWrapper
,添加如下:
public String getContentType() {
return MediaType.APPLICATION_JSON_UTF8_VALUE;
}
繼續調試,報錯如下:
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing:
爲什麼呢,因爲剛纔判斷isFormPost=false
,是從inputStream
裏面獲取輸入流,所以,也要覆寫一下,繼續修改,添加如下:
@Override
public ServletInputStream getInputStream() throws IOException {
return getRequest().getInputStream();
}
但是,還是提示 Required request body is missing
,繼續調試分析
奇怪,getBody()
返回的是inputStream
,但是上面已經添加了getInputStream()
,到底幾個意思啊,也就是說,我們getRequest().getInputStream();
返回的輸入流不對?
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
this.headers = inputMessage.getHeaders();
InputStream inputStream = inputMessage.getBody();
// 輸入流,是否支持?
if (inputStream.markSupported()) {
inputStream.mark(1);
this.body = (inputStream.read() != -1 ? inputStream : null);
inputStream.reset();
}
else {
// 走到這個邏輯了
PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
int b = pushbackInputStream.read();
if (b == -1) {
// 所以 報錯 Required request body is missing
this.body = null;
}
else {
this.body = pushbackInputStream;
pushbackInputStream.unread(b);
}
}
}
最開始的的cotent-type=application/x-www-form-urlencoded;charset=UTF-8
,屬於表單提交數據,所以是用request.getParamMaps()
去獲取參數處理,所以request.getInputStream.markSupported()=false
,而我們只是強制修改了content-type=application-json;
,可以去獲取request.getInputStream()
,但是確是不支持的,也就是不可read
的
思考,在包裝類MyRequestWrapper
裏面,自己獲取一下內容,然後轉換爲inputStream
返回,繼續修改,完整代碼如下:
class MyRequestWrapper extends HttpServletRequestWrapper {
private String body;
public MyRequestWrapper(HttpServletRequest request) {
super(request);
try {
String encrypt = IOUtils.toString(request.getInputStream());
log.debug("請求密文:" + encrypt);
this.body = encrypt;
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Enumeration<String> getHeaders(String name) {
if (null != name && name.equals("content-type")) {
return new Enumeration<String>() {
private boolean hasGetted = false;
@Override
public String nextElement() {
if (hasGetted) {
throw new NoSuchElementException();
} else {
hasGetted = true;
return MediaType.APPLICATION_JSON_VALUE;
}
}
@Override
public boolean hasMoreElements() {
return !hasGetted;
}
};
}
return super.getHeaders(name);
}
public String getContentType() {
return MediaType.APPLICATION_JSON_UTF8_VALUE;
}
@Override
public ServletInputStream getInputStream() throws IOException {
InputStream inputStream = IOUtils.toInputStream(this.getBody());
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 inputStream.read();
}
};
return servletInputStream;
}
public String getBody() {
return this.body;
}
}
再次調試,果然可以了,
另外,雖然判斷isFormPost
的邏輯是使用request.getContentType
來判斷,但是判斷支持的content-ype
類型,卻的確是從header
裏面獲取判斷的,略坑,所以我們最初包裝類裏面設置header
的代碼還必須保留,不然報錯