Make simple servlet filter work with #ControllerAdvice - spring

I've a simple filter just to check if a request contains a special header with static key - no user auth - just to protect endpoints. The idea is to throw an AccessForbiddenException if the key does not match which then will be mapped to response with a class annotated with #ControllerAdvice. However I can't make it work. My #ExceptionHandler isn't called.
ClientKeyFilter
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Controller
import javax.servlet.*
import javax.servlet.http.HttpServletRequest
#Controller //I know that #Component might be here
public class ClientKeyFilter implements Filter {
#Value('${CLIENT_KEY}')
String clientKey
public void init(FilterConfig filterConfig) {}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
req = (HttpServletRequest) req
def reqClientKey = req.getHeader('Client-Key')
if (!clientKey.equals(reqClientKey)) {
throw new AccessForbiddenException('Invalid API key')
}
chain.doFilter(req, res)
}
public void destroy() {}
}
AccessForbiddenException
public class AccessForbiddenException extends RuntimeException {
AccessForbiddenException(String message) {
super(message)
}
}
ExceptionController
#ControllerAdvice
class ExceptionController {
static final Logger logger = LoggerFactory.getLogger(ExceptionController)
#ExceptionHandler(AccessForbiddenException)
public ResponseEntity handleException(HttpServletRequest request, AccessForbiddenException e) {
logger.error('Caught exception.', e)
return new ResponseEntity<>(e.getMessage(), I_AM_A_TEAPOT)
}
}
Where I'm wrong? Can simple servlet filter work with spring-boot's exception mapping?

As specified by the java servlet specification Filters execute always before a Servlet is invoked. Now a #ControllerAdvice is only useful for controller which are executed inside the DispatcherServlet. So using a Filter and expecting a #ControllerAdvice or in this case the #ExceptionHandler, to be invoked isn't going to happen.
You need to either put the same logic in the filter (for writing a JSON response) or instead of a filter use a HandlerInterceptor which does this check. The easiest way is to extend the HandlerInterceptorAdapter and just override and implement the preHandle method and put the logic from the filter into that method.
public class ClientKeyInterceptor extends HandlerInterceptorAdapter {
#Value('${CLIENT_KEY}')
String clientKey
#Override
public boolean preHandle(ServletRequest req, ServletResponse res, Object handler) {
String reqClientKey = req.getHeader('Client-Key')
if (!clientKey.equals(reqClientKey)) {
throw new AccessForbiddenException('Invalid API key')
}
return true;
}
}

You can't use #ControllerAdvice, because it gets called in case of an exception in some controller, but your ClientKeyFilter is not a #Controller.
You should replace the #Controller annotation with the #Component and just set response body and status like this:
#Component
public class ClientKeyFilter implements Filter {
#Value('${CLIENT_KEY}')
String clientKey
public void init(FilterConfig filterConfig) {
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String reqClientKey = request.getHeader("Client-Key");
if (!clientKey.equals(reqClientKey)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid API key");
return;
}
chain.doFilter(req, res);
}
public void destroy() {
}
}

Servlet Filters in Java classes are used for the following purposes:
To check requests from client before they access resources at backend.
To check responses from server before sent back to the client.
Exception throw from Filter may not be catch by #ControllerAdvice because in may not reach DispatcherServlet. I am handling in my project as below:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
String token = null;
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && (bearerToken.contains("Bearer "))) {
if (bearerToken.startsWith("Bearer "))
token = bearerToken.substring(7, bearerToken.length());
try {
AuthenticationInfo authInfo = TokenHandler.validateToken(token);
logger.debug("Found id:{}", authInfo.getId());
authInfo.uri = request.getRequestURI();
AuthPersistenceBean persistentBean = new AuthPersistenceBean(authInfo);
SecurityContextHolder.getContext().setAuthentication(persistentBean);
logger.debug("Found id:'{}', added into SecurityContextHolder", authInfo.getId());
} catch (AuthenticationException authException) {
logger.error("User Unauthorized: Invalid token provided");
raiseException(request, response);
return;
} catch (Exception e) {
raiseException(request, response);
return;
}
// Wrapping the error response
private void raiseException(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiError apiError = new ApiError(HttpStatus.UNAUTHORIZED);
apiError.setMessage("User Unauthorized: Invalid token provided");
apiError.setPath(request.getRequestURI());
byte[] body = new ObjectMapper().writeValueAsBytes(apiError);
response.getOutputStream().write(body);
}
// ApiError class
public class ApiError {
// 4xx and 5xx
private HttpStatus status;
// holds a user-friendly message about the error.
private String message;
// holds a system message describing the error in more detail.
private String debugMessage;
// returns the part of this request's URL
private String path;
public ApiError(HttpStatus status) {
this();
this.status = status;
}
//setter and getters

Related

How to validated rest url in spring boot?

validate Rest URL in spring boot.
Requirement: If I hit the wrong URL then it should throw a custom exception.
ex. Correct URL is "/fulfillment/600747l/send_to_hub" If I hit "/api/600747l/send_to_hub_1" then it should return exception like
"404:- URL not Found.".
Right now it returning "500 : -
{
"timestamp": 1531995246549,
"status": 500,
"error": "Internal Server Error",
"message": "Invalid Request URL.",
"path": "/api/600747l/send_to_hub_1"
}"
you need to write NewClass with annotation #ControllerAdvice which will redirect all exceptions to this NewClass.
example
Your Custom Exception Class:
#Data
#AllArgsConstructor
#EqualsAndHashCode(callSuper = false)
public class IOApiException extends IOException {
private ErrorReason errorReason;
public IOApiException(String message, ErrorReason errorReason) {
super(message);
this.errorReason = errorReason;
}
}
Now the CustomExceptionHandler Class -
#ControllerAdvice
#RestController
public class GlobalExceptionHandler {
Logger logger = LoggerFactory.getLogger(this.getClass());
#ResponseStatus(HttpStatus.UNAUTHORIZED)
#ExceptionHandler(value = IOApiException.class)
public GlobalErrorResponse handleException(IOApiException e) {
logger.error("UNAUTHORIZED: ", e);
return new GlobalErrorResponse("URL Not Found", HttpStatus.UNAUTHORIZED.value(), e.getErrorReason());
}
//this to handle customErrorResponseClasses
public GlobalErrorResponse getErrorResponseFromGenericException(Exception ex) {
if (ex == null) {
return handleException(new Exception("INTERNAL_SERVER_ERROR"));
}
else if (ex instanceof IOApiException) {
return handleException((IOApiException) ex);
}
}
Now Your error response class:
public class GlobalErrorResponse {
private String message;
#JsonIgnore
private int statusCode;
private ErrorReason reason;
}
ErrorReason Class
public enum ErrorReason {
INTERNAL_SERVER_ERROR,
INVALID_REQUEST_PARAMETER,
INVALID_URL
}
add and register one filter who calls the GlobalExceptionHandler in exception case like this
public class ExceptionHandlerFilter implements Filter {
private final GlobalExceptionHandler globalExceptionHandler;
public ExceptionHandlerFilter(GlobalExceptionHandler globalExceptionHandler) {
this.globalExceptionHandler = globalExceptionHandler;
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (Exception exception) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
GlobalErrorResponse errorResponse = globalExceptionHandler.getErrorResponseFromGenericException(exception);
httpResponse.setStatus(errorResponse.getStatusCode());
response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
}
}
#Override
public void destroy() {
}
}
Like this you can add as many exceptions you want.. and can handle it manually.
As per your question first of all you need to define a base url(e.g.-/api) so that any url must be handled through your controller.Now after base url as shown /api/600747l/send_to_hub_1 #PathVariable int id. This circumstance is important, because Spring documentation said that if method argument annotated with #PathVariable can’t be casted to specified type (in our case to int), it will be exposed as String. Hence it can cause a TypeMismatchException.
To handle this I will use #ExceptionHandler annotation on #Controller level. Such approach suits for this situation as no one else. I just need to make 2 changes in the Controller:
1.Add MessageSource field
2.Add exception handler method
#Autowired
private MessageSource messageSource;
...
#ExceptionHandler(TypeMismatchException.class)
#ResponseStatus(value=HttpStatus.NOT_FOUND)
#ResponseBody
public ErrorInfo handleTypeMismatchException(HttpServletRequest req, TypeMismatchException ex) {
Locale locale = LocaleContextHolder.getLocale();
String errorMessage = messageSource.getMessage("error.bad.smartphone.id", null, locale);
errorMessage += ex.getValue();
String errorURL = req.getRequestURL().toString();
return new ErrorInfo(errorURL, errorMessage);
}
...

OncePerRequestFilter - handling exceptions annotated with #ResponseStatus

I'm looking for a way to log all my requests and responses in the database (1 record = 1 request + 1 response).
My use case in details:
Log record in database with request URL, params, IP, start date etc.
Update database record (when request finish) and save response,
exceptions, end date etc.
I'm trying to do with custom OncePerRequestFilter and it work's almost OK. But I have problem with handling exceptions annotated with annotation #ResponseStatus. This kind of exceptions (thrown in controllers) I can't catch in my custom doFilter method. Do you know any way to capture these exceptions in filter? Unless I should do this in some other way?
AuditFilter:
#Component
public class AuditFilter extends OncePerRequestFilter {
private Logger logger = Logger.getLogger(AuditFilter.class.getName());
private RequestAuditRepository repository;
AuditFilter(RequestAuditRepository repository) {
this.repository = repository;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
}
private void doFilterWrapped(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain)
throws ServletException, IOException {
RequestAuditLog requestAuditLog = new RequestAuditLog();
String catchedExceptionMsg = null;
try {
beforeRequest(requestAuditLog, request);
filterChain.doFilter(request, response);
}
catch (Exception e) {
// Not called when exception with #ResponStatus annotation throwed
catchedExceptionMsg = e.getMessage();
throw e;
}
finally {
afterRequest(requestAuditLog, catchedExceptionMsg, request, response);
response.copyBodyToResponse();
}
}
...
}
BadRequestException:
#ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}
I think the BadRequestException is handled even before your custom filter gets triggered and therefore you can't catch this exception in your filter.
What you could do is that you write your own ExceptionHandler additionally to your filter and log your stuff there.
#ControllerAdvice
public class MyExceptionHandler {
#ExceptionHandler(BadRequestException.class)
public void handleError(BadRequestException ex) {
// do your stuff here
}
}

Spring Rest filter Data Chaining

I have a spring Rest web app. And created auth filter. on this layer, I am getting User which is needed at RestController methods. I want to avoid DB request duplication. Is it possible to pass some objects from Filter to RestController Methods as a param?
#Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException,
ServletException {
String authorization = ((HttpServletRequest) req).getHeader("Authorization");
DeviceEntity deviceEntity = mDeviceService.byToken(authorization);
if (deviceEntity != null) {
if (new Date().before(deviceEntity.getExpireOn())) {
chain.doFilter(req, res);
}
} else {
((HttpServletResponse) res).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
I want to pass deviceEntity to the next class
#RestController
#RequestMapping("/general")
public class RestService {
#RequestMapping(value = "/ping", method = RequestMethod.GET)
#ResponseStatus(HttpStatus.OK)
public void ping(DeviceEntity de) {
// ^^^^^^^^^^
//i want to have access to this device here
LOG.info("Ping called");
}
}
I need some way to pass this Device entity to RequestMapped method

How to read httpServletResponse in the interceptor?

I have a spring boot application. And now I need to read request and response in interceptor.I use a HttpServletRequestWrapper replace the request in DispatcherServlet
#Component("dispatcherServlet")
public class FofDisPatcherServlet extends DispatcherServlet {
#Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
MultiReadHttpServletRequest requestWrapper = null;
try {
requestWrapper = new MultiReadHttpServletRequest(request);
super.doDispatch(requestWrapper, response);
} catch (Exception e) {
super.doDispatch(request,response);
}
}
}
And in my interceptor , I can read the request body. But when I want to read the response body, it doesn't works.when I replace the response in the CustomerDispatcherServlet I got nothing response.I have tried ContentCachingResponseWrapper , but I got the payload with "".
It's a old question.and I have search some questions but didn't find a suitable solution.
I know I can solve the problem with AOP.But I want to know how can I do it in the interceptor?
here is my interceptor code
public class CustomerInterceptor extends HandlerInterceptorAdapter{
#Override
public void postHandle(...){
MultiReadHttpServletRequest req = (MultiReadHttpServletRequest) request;
ContentCachingResponseWrapper res = new ContentCachingResponseWrapper(response);
Byte[] body = res. getContentAsByteArray();
...
}
}
the body I got is [].
After few days .I find the answer.In the CustomerDispatcherServlet I should add responseWrapper.copyBodyToResponse()
the CustomerDIspatcherServlet like this:
#Component("dispatcherServlet")
public class FofDisPatcherServlet extends DispatcherServlet {
#Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
MultiReadHttpServletRequest requestWrapper = null;
try {
requestWrapper = new MultiReadHttpServletRequest(request);
if (!(response instanceof ContentCachingResponseWrapper)) {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
super.doDispatch(requestWrapper, responseWrapper);
responseWrapper.copyBodyToResponse();
}else {
super.doDispatch(requestWrapper, response);
}
} catch (Exception e) {
super.doDispatch(request, response);
}
}
}
Try this:
#Component("dispatcherServlet")
public class FofDisPatcherServlet extends DispatcherServlet {
#Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
MultiReadHttpServletRequest requestWrapper = null;
try {
requestWrapper = new MultiReadHttpServletRequest(request);
super.doDispatch(requestWrapper, new ContentCachingResponseWrapper(request));
} catch (Exception e) {
super.doDispatch(request,response);
}
}
}
.
public class CustomerInterceptor extends HandlerInterceptorAdapter{
#Override
public void postHandle(..., HttpServletResponse response){
if (response instanceof ContentCachingResponseWrapper) {
Byte[] body = ((ContentCachingResponseWrapper)response). getContentAsByteArray();
}
...
}
}
The error is in your code
public class CustomerInterceptor extends HandlerInterceptorAdapter{
#Override
public void postHandle((HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView){
MultiReadHttpServletRequest req = (MultiReadHttpServletRequest) request;
ContentCachingResponseWrapper res = new ContentCachingResponseWrapper(response);
Byte[] body = res. getContentAsByteArray();
...
}
}
You are passing request in ContentCachingResponseWrapper.
See this question very similar problem .

Java spring get body of post request

I have the following problem: I try to get body of a POST request before it is handled by a spring controller. For that I am using the HandlerInterceptorAdapter's preHandle() method.
As stated in this discussion Spring REST service: retrieving JSON from Request I also use the HttpServletRequestWrapper. With this wrapper I managed to print the body of the first POST request, but the second POST throws an IOException: StreamClosed.
Do you have any ideas on how I can get the body of all POST requests?
Here is the preHandle() method from the interceptor:
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println(request.getMethod());
MyRequestWrapper w = new MyRequestWrapper(request);
BufferedReader r = w.getReader();
System.out.println(r.readLine());
return super.preHandle(request, response, handler);
}
The HttpServletRequestWrapper:
public class MyRequestWrapper extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;
private HttpServletRequest request;
public MyRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
}
#Override
public ServletInputStream getInputStream() throws IOException {
cachedBytes = new ByteArrayOutputStream();
if (request.getMethod().equals("POST"))
cacheInputStream();
return new CachedServletInputStream();
}
#Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private void cacheInputStream() throws IOException {
/*
* Cache the inputstream in order to read it multiple times. For
* convenience, I use apache.commons IOUtils
*/
ServletInputStream inputStream = super.getInputStream();
if (inputStream == null) {
return;
}
IOUtils.copy(inputStream, cachedBytes);
}
/* An inputstream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
#Override
public int read() throws IOException {
return input.read();
}
}
}
The console output:
2014-10-15 12:13:00 INFO [http-nio-8080-exec-1] org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'dispatcherServlet': initialization completed in 9 ms
GET
null
GET
null
POST
{"long":null,"owner":{"__type":"Owner","id":20,"version":1,"md5Password":""},"string":"ws","tool":{"__type":"Tool","id":33,"version":1}}
POST
2014-10-15 12:13:00 ERROR [http-nio-8080-exec-3] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet dispatcherServlet threw exception
java.io.IOException: Stream closed
You're attempting to read from the original request in your Wrapper, but after this, the original request is still being read - hence the request input stream has been consumed and cannot be read from again.
Instead of using an Interceptor, consider using a javax.servlet.Filter. In the doFilter method, you can pass the wrapped request on down the chain.
I've used filter that implements Filter & interceptor that extends HandlerInterceptorAdapter (because in the filter all fields are nullable and I can't save anything to DB. see Autowired Null Pointer Exception) to retreive request and response body and save them to DB. If your filter works fine then use only filter.
filter. Here I wrap a request and a response to read from them not only once. You can use ContentCachingRequestWrapper and ContentCachingResponseWrapper for that.
#Component
public class RequestLogFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger(RequestLogFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
logger.info("======================> FILTER <======================");
HttpServletRequest requestToCache = new ContentCachingRequestWrapper((HttpServletRequest) request);
HttpServletResponse responseToCache = new ContentCachingResponseWrapper((HttpServletResponse) response);
// before method
chain.doFilter(requestToCache, responseToCache);
// after method
// your logic(save to DB, logging...)
getRequestData(request);
getResponseData(response);
}
#Override
public void destroy() {
}
}
-
#Component
public class RequestLogInterceptor extends HandlerInterceptorAdapter {
private final Logger logger = LoggerFactory.getLogger(RequestLogInterceptor.class);
#Autowired
private InboundRequestLogStore inboundRequestLogStore;
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
logger.info("====================> INTERCEPTOR <========================");
try {
if (request.getAttribute(InboundRequestAspect.INBOUND_LOG_MARKER) != null) {
InboundRequestLogRecord logRecord = new InboundRequestLogRecord();
logRecord.setIpAddress(request.getRemoteAddr());
// getting request and response body
logRecord.setRequestBody(getRequestData(request));
logRecord.setResponseBody(getResponseData(response));
logRecord.setResponseCode(((HttpServletResponse) response).getStatus());
String uri = request.getScheme() + "://" + request.getServerName()
+ ("http".equals(request.getScheme()) && request.getServerPort() == 80
|| "https".equals(request.getScheme()) && request.getServerPort() == 443 ? ""
: ":" + request.getServerPort())
+ request.getRequestURI()
+ (request.getQueryString() != null ? "?" + request.getQueryString() : "");
logRecord.setUrl(uri);
inboundRequestLogStore.add(logRecord); // save to DB
} else {
((ContentCachingResponseWrapper) response).copyBodyToResponse(); // in other case you send null to the response
}
} catch (Exception e) {
logger.error("error ", e);
try {
((ContentCachingResponseWrapper) response).copyBodyToResponse(); // in other case you send null to the response
} catch (Exception e2) {
// TODO Auto-generated catch block
logger.error("error ", e2);
}
}
}
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;
}
public static String getResponseData(final HttpServletResponse response) throws UnsupportedEncodingException, 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(); // in other case you send null to the response
}
return payload;
}
}
add to servlet-context.xml
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<beans:bean class="path.to.RequestLogInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
namespaces:
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
ContentCachingRequestWrapper - http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingRequestWrapper.html
ContentCachingResponseWrapper - http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingResponseWrapper.html

Resources