I want to implement Rest logging for API using Spring. I tried this:
public static String readPayload(final HttpServletRequest request) throws IOException {
String payloadData = null;
ContentCachingRequestWrapper contentCachingRequestWrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (null != contentCachingRequestWrapper) {
byte[] buf = contentCachingRequestWrapper.getContentAsByteArray();
if (buf.length > 0) {
payloadData = new String(buf, 0, buf.length, contentCachingRequestWrapper.getCharacterEncoding());
}
}
return payloadData;
}
public static String getResponseData(final HttpServletResponse response) throws IOException {
String payload = null;
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
wrapper.copyBodyToResponse();
}
}
return payload;
}
#PostMapping(value = "/v1", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest requestCacheWrapperObject = new ContentCachingRequestWrapper(request);
requestCacheWrapperObject.getParameterMap();
.raw_request(readPayload(requestCacheWrapperObject))
.raw_response(getResponseData(response))
}
But I get NULL for request and response.
Do you know what is the proper way to get the payload from the request and the response?
So you just need to have your own interceptor.
#Component
public class HttpRequestResponseLoggingInterceptorAdapter extends HandlerInterceptorAdapter {
#Autowired
private LoggingUtils loggingutils;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
loggingutils.preHandle(request, response);
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
#Nullable ModelAndView modelAndView) {
try {
loggingutils.postHandle(request, response);
} catch(Exception e) {
System.out.println("Exception while logging outgoing response");
}
}
}
Once that is done, you need to bind your new interceptor to existing interceptors.
#Configuration
public class InterceptorConfig implements WebMvcConfigurer {
#Autowired
private HttpRequestResponseLoggingInterceptorAdapter httpRequestResponseLoggingInterceptorAdapter;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(httpRequestResponseLoggingInterceptorAdapter);
}
}
Once that is done, your incoming requests for handlemessage method will be intercepted, and can do whatever pre/post processing you want to have.
Logging in this case.
Let me know if this helps.
Sounds like your usecase would be best suited with a class extending spring's org.springframework.web.servlet.handler.HandlerInterceptorAdapter.
Custom interceptors can override preHandle and postHandle - both of which it sounds like you are inclined to use.
EDIT:
// add to wherevere your source code is
public class CustomInterceptor extends HandlerInterceptorAdapter {
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO: use 'request' from param above and log whatever details you want
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO: use 'response' from param above and log whatever details you want
}
}
// add to your context
<mvc:interceptors>
<bean id="customInterceptor" class="your.package.CustomInterceptor"/>
</mvc:interceptors>
Related
I want to use HandlerInterceptor to achive idempotent check, and if the request pass the intercepter, cache the response body which retrun from controller, and the key for cache is a token which from request header. but now, i can get request header(it contains the key for cache) in 'postHandle', but can't get response body(the value for cache) in this method. is there any way to achieve it?
here is my code:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface Idempotent {
}
public class IdempotentInterceptor implements HandlerInterceptor {
#Autowired
private TokenService tokenService;
#Autowired
private RedisCache redisCache;
#Value("${idempotent.tokenName}")
private String TOKEN_NAME;
#Value("${idempotent.respCachePrefix}")
private String prefix;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
if (methodAnnotation != null) {
if (!tokenService.checkIdempotent(request)) {
String token = request.getHeader(TOKEN_NAME);
log.info("duplicate request, IdempotentToken: {}", token);
Object result = redisCache.get(prefix + token);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
if (result != null) {
log.info("{} exist result, return the cached result: {}", token, result);
response.getWriter().print(JSON.toJSONString(result));
} else {
response.getWriter().print(JSON.toJSONString(new ResultDTO<>(ResultDTO.FAIL_CODE, "invalid request, please check idempotent token.")));
}
response.getWriter().flush();
return false;
}
return true;
}
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
if (methodAnnotation != null) {
// I want to cache the response body here, but can't get infos...
redisCache.set(prefix + request.getHeader(TOKEN_NAME), "", 600L);
}
}
}
#GetMapping("/test/{data}")
#Idempotent
public ResultDTO test(#PathVariable("data") String data) {
return new ResultDTO(data);
}
in a word, i want to get both request headers and response body in one method
I have a filter for JWT which can throw a JWTDecodeException
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
final JwtService jwtService;
public JwtAuthenticationTokenFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtService.extractToken(request);
if (StringUtils.hasLength(token)) {
DecodedJWT decodedJWT = jwtService.validateToken(token);
JwtAuthenticationToken authentication = new JwtAuthenticationToken(decodedJWT);
authentication.setAuthenticated(true);
SecurityContextUtils.setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
Also I have a single error handler which handles all exceptions
#RequestMapping("/error")
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> test(Exception ex, WebRequest request) {
if (ex instanceof AccessDeniedException || ex instanceof JWTDecodeException) {
System.out.println(123);
}
return handleExceptionInternal(ex, "test", new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
But I can't catch JWTDecodeException because for some reason the 'ex' param store simple Exception. I trace it with debug and saw when my JWTDecodeException becomes Exception, but I can't understand how to avoid it.
Moreover I can use JWTDecodeExceptiont = (JWTDecodeException)request.getAttribute("javax.servlet.error.exception",0); in handler and it will return JWTDecodeException.
Is there a way to get JWTDecodeException not from 'request' but from 'ex'?
I want to Authenticate one of the post request body key-value pair, but I want to do the same with the help of a Interceptor/Filter. How can I do that?
You can create a custom request filter that will check the request:
public class MyFilter implements OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
var user = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// do stuff you need to do here
filterChain.doFilter(request, response);
}
}
and then in your WebSecurityConfiguration class register the filter like this
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(new MyFilter(), BasicAuthenticationFilter.class);
}
}
You can extend HandlerInterceptorAdapter and perform your custom operations/filters on top of request by overriding preHandle() method.
Pseudocode is here:
#Component
public class SimpleInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// Handle your request here. In your case, authentication check should go here.
return true;
}
}
Add the SimpleInterceptor to the registry to intercept the requests.
#Configuration
#EnableWebMvc
public class SimpleMvnConfigurer implements WebMvcConfigurer {
#Autowired
SimpleInterceptor simpleInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(simpleInterceptor);
}
}
That's all!
EDIT 1:
To send the response from preHandle method, follow below pseudocode:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// Handle your request here. AIn your case, authentication check should go here.
if (!isValidAuth()) {
// Populate the response here.
try {
response.setStatus(401);
response.getWriter().write("Authentication failed.");
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
return true;
} ```
You can try this with Filter.
public class SimpleFilter implements Filter {
private void throwUnauthorized(ServletResponse res) throws IOException {
HttpServletResponse response = (HttpServletResponse) res;
response.reset();
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (!isValidAuth(request)) {
throwUnauthorized(res);
}
chain.doFilter(req, res);
}
private boolean isValidAuth(HttpServletRequest request) {
// YOUR LOGIC GOES HERE.
return false;
}
#Override
public void destroy() {
}
#Override
public void init(FilterConfig arg0) {
}
}
Register the filter using FilterRegistrationBean
#Bean
public FilterRegistrationBean<SimpleFilter> simpleFilter() {
FilterRegistrationBean<SimpleFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SimpleFilter());
return registrationBean;
}
Let me know if this works.
I have custom Filter and I want to log body from request.
But when I use ContentCachingRequestWrapper and try to call getContentAsByteArray() I always get an empty array.
#Component
public class CustomFilter implements Filter {
private final Logger log = LoggerFactory.getLogger(CustomFilter.class);
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest requestToCache = new ContentCachingRequestWrapper(request);
chain.doFilter(req, res);
log.info(getRequestData(requestToCache));
}
#Override
public void init(FilterConfig filterConfig) {
}
#Override
public void destroy() {
}
public static String getRequestData(final HttpServletRequest request) throws UnsupportedEncodingException {
String payload = null;
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
}
}
return payload;
}
}
I also tried create Interceptor, but had the same problem.
What am I doing wrong?
Thanks for help.
You can use the existing spring implementation by just registering this bean in a #Configuration annotated class:
#Bean
public static Filter requestLoggingFilter() {
final CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludePayload(true);
loggingFilter.setMaxPayloadLength(512);
return loggingFilter;
}
Whilst I'd recommend going with NiVer's answer, I've been looking into why this issue occurs and I can finally give you an answer.
When you create a new ContentCachingRequestWrapper, the internal ByteArrayOutputStream is initialized but no data is copied to it. The body is only written to the ByteArrayOutputStream when you call getParameter, getParameterMap(), getParameterNames() or getParameterValues(String name) methods, and even then the data is only copied if the content type contains application/x-www-form-urlencoded.
I have a need where certain HTTP requests must be redirected to a Spring Boot web app/service, but that on the request-side, the Spring app does nothing and acts as a passthrough between the HTTP client (another service) and the request's true destination. But when the response comes back to the Spring app (from that destination), I need the Spring app to be able to inspect the response and possibly take action on it if need be. So:
HTTP client makes a request to, say, http://someapi.example.com
Network magic routes the request to my Spring app at, say, http://myproxy.example.com
On the request, this app/proxy does nothing, and so the request is forwarded on http://someapi.example.com
The service endpoint at http://someapi.example.com returns an HTTP response back to the proxy
The proxy at http://myproxy.example.com inspects this response, and possibly sends an alert before returning the response back to the original client
So essentially, a filter that acts as a pass-through on the request, and only really does anything after the remote service has executed and returned a response.
My best attempt thus far has been to setup a servlet filter:
#Override
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response)
// How and where do I put my code?
if(responseContainsFizz(response)) {
// Send an alert (don't worry about this code)
}
}
Is this possible to do? If so, where do I put the code that inspects and acts upon the response? With my code the way it is I get exceptions thrown when trying to hit a controller from a browser:
java.lang.IllegalStateException: STREAM
at org.eclipse.jetty.server.Response.getWriter(Response.java:910) ~[jetty-server-9.2.16.v20160414.jar:9.2.16.v20160414]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_92]
rest of stack trace omitted for brevity
Any ideas?
Per the Servlet API documentation, the reason you are getting the IllegalStateException is because you are attempting to call ServletResponse.getWriter after ServletResponse.getOutputStream has already been called on the response. So it appears that the method you need to call is ServletResponse.getOutputStream().
However, if you are trying to access the body of the response, the best solution is to wrap the response in a ServletResponseWrapper so that you can capture the data:
public class MyFilter implements Filter
{
#Override
public void init(FilterConfig filterConfig) throws ServletException
{
}
#Override
public void destroy()
{
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
MyServletResponseWrapper responseWrapper = new MyServletResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, responseWrapper);
if (evaluateResponse(responseWrapper)) {
// Send an alert
}
}
private boolean evaluateResponse(MyServletResponseWrapper responseWrapper) throws IOException
{
String body = responseWrapper.getResponseBodyAsText();
// Perform business logic on the body text
return true;
}
private static class MyServletResponseWrapper extends HttpServletResponseWrapper
{
private ByteArrayOutputStream copyOutputStream;
private ServletOutputStream wrappedOutputStream;
public MyServletResponseWrapper(HttpServletResponse response)
{
super(response);
}
public String getResponseBodyAsText() throws IOException
{
String encoding = getResponse().getCharacterEncoding();
return copyOutputStream.toString(encoding);
}
#Override
public ServletOutputStream getOutputStream() throws IOException
{
if (wrappedOutputStream == null) {
wrappedOutputStream = getResponse().getOutputStream();
copyOutputStream = new ByteArrayOutputStream();
}
return new ServletOutputStream()
{
#Override
public boolean isReady()
{
return wrappedOutputStream.isReady();
}
#Override
public void setWriteListener(WriteListener listener)
{
wrappedOutputStream.setWriteListener(listener);
}
#Override
public void write(int b) throws IOException
{
wrappedOutputStream.write(b);
copyOutputStream.write(b);
}
#Override
public void close() throws IOException
{
wrappedOutputStream.close();
copyOutputStream.close();
}
};
}
}
}
The response can be easy manipulated/replaced/extended e with a filter and a response wrapper.
In the filter before the call chain.doFilter(request, wrapper) you prepare a PrintWriter for the new response content and the wrapper object.
After the call chain.doFilter(request, wrapper) is the actuall response manipulation.
The wrapper is used to get access to the response as String.
The Filter:
#WebFilter(filterName = "ResponseAnalysisFilter", urlPatterns = { "/ResponseFilterTest/*" })
public class ResponseFilter implements Filter {
public ResponseFilter() {}
#Override
public void init(FilterConfig filterConfig) throws ServletException {}
#Override
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
PrintWriter out = response.getWriter();
CharResponseWrapper wrapper = new CharResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, wrapper);
String oldResponseString = wrapper.toString();
if (oldResponseString.contains("Fizz")) {
// replace something
String newResponseString = oldResponseString.replaceAll("Fizz", "Cheers");
// show alert with a javascript appended in the head tag
newResponseString = newResponseString.replace("</head>",
"<script>alert('Found Fizz, replaced with Cheers');</script></head>");
out.write(newResponseString);
response.setContentLength(newResponseString.length());
}
else { //not changed
out.write(oldResponseString);
}
// the above if-else block could be replaced with the code you need.
// for example: sending notification, writing log, etc.
out.close();
}
}
The Response Wrapper:
public class CharResponseWrapper extends HttpServletResponseWrapper {
private CharArrayWriter output;
public String toString() {
return output.toString();
}
public CharResponseWrapper(HttpServletResponse response) {
super(response);
output = new CharArrayWriter();
}
public PrintWriter getWriter() {
return new PrintWriter(output);
}
}
The Test Servlet:
#WebServlet("/ResponseFilterTest/*")
public class ResponseFilterTest extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
response.getWriter().append(
"<html><head><title>replaceResponse filter</title></head><body>");
if (request.getRequestURI().contains("Fizz")) {
response.getWriter().append("Fizz");
}
else {
response.getWriter().append("Limo");
}
response.getWriter().append("</body></html>");
}
}
Test Urls:
https://yourHost:8181/contextPath/ResponseFilterTest/Fizz (Trigger response Replacement)
https://yourHost:8181/contextPath/ResponseFilterTest/ (response unchanged)
More Info and examples about filters:
http://www.oracle.com/technetwork/java/filters-137243.html#72674
http://www.leveluplunch.com/java/tutorials/034-modify-html-response-using-filter/
https://punekaramit.wordpress.com/2010/03/16/intercepting-http-response-using-servlet-filter/