Spring AOP with RequestDispatcher causes recursive calls - spring

Spring-servlet.xml:
<aop:config>
<aop:advisor advice-ref="interceptor" pointcut="#annotation(Validator)"/>
</aop:config>
<bean id="interceptor" class="org.aopalliance.intercept.MethodInterceptor" />
MethodInterceptor invoke():
if (!valid){
RequestDispatcher rd = request.getRequestDispatcher(errorView);
rd.forward(request, response);
}
Working flow of control:
My interceptor is called before any Spring controller method that is annotated with the Validator annotation. The intention is to validate the request, if validation fails, forward the request to a different view. This is usually working. If there is an error (!valid), the RequestDispatcher.forward is called. This causes another Spring controller method to be called which ultimately shows the error view. This normally works.
Issue:
For some Spring controllers, my RequestDispatcher's errorView causes the request to be forwarded back to the same method causing an infinite loop (invoke()gets called over and over). I think this is because of how the Spring controller's request mappings (see below) are set up.
Error view: #RequestMapping(value = URL, params="error")
Normal view: #RequestMapping(value = URL, params="proceed")
So when the first request is routed it's got 'proceed' in the request params. Then when there's an error and the RequestDispatcher forwards to the view with the 'error' param in the query string, it should forward to the "Error view" method above, but it doesn't. It always forwards to the 'proceed' method causing an infinite loop on the MethodInterceptor invoke(). This seems to be because the 'proceed' parameter is still in the HttpServletRequest. However this isn't easy to fix because the whole point of the interceptor is that it has no knowledge of the Spring controller itself - it only knows if an error occurred, and that it should forward to the error view if an error occurred.
Workaround:
Using the request mappings below, it fixes the issue. This is probably because the HttpServletRequest parameter is overwritten when using the key=value notation.
Error view: #RequestMapping(value = URL, params="view=error")
Normal view: #RequestMapping(value = URL, params="view=proceed")
Question
How can I "properly" fix the issue without resorting to the workaround shown above? Is there a more standard way to forward to the correct spring controller?

Solution#1:
Having configured as following:
Error view: #RequestMapping(value = URL, params="error")
Normal view: #RequestMapping(value = URL, params="proceed")
You could try for redirect as follows:
MethodInterceptor invoke():
if (!valid){
// RequestDispatcher rd = request.getRequestDispatcher(errorView);
// rd.forward(request, response);
response.sendRedirect(errorView);
}
Drawback: the browser would make a second request, therefore the old method parameters are no longer in the httpservletrequest.
WorkArround: To Avoid drawback, You could use Spring MVC Flash Attribute. You could follow this tutorial to know how Flash Attribute works.
Refs:FlashAttributesExample
Solution#2:
How can I "properly" fix the issue without resorting to the workaround
shown above? Is there a more standard way to forward to the correct
spring controller?
You could incorporate by implementing you own RequestMappingHandlerAdapter.
Solution#3:
Here is the code for the aspect:
public class RequestBodyValidatorAspect {
private Validator validator;
#Pointcut("#annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void controllerInvocation() {
}
#Around("controllerInvocation()")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Annotation[][] argAnnotations = method.getParameterAnnotations();
String[] argNames = methodSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (hasRequestBodyAndValidAnnotations(argAnnotations[i])) {
validateArg(args[i], argNames[i]);
}
}
return joinPoint.proceed(args);
}
private boolean hasRequestBodyAndValidAnnotations(Annotation[] annotations) {
if (annotations.length < 2)
return false;
boolean hasValid = false;
boolean hasRequestBody = false;
for (Annotation annotation : annotations) {
if (Valid.class.isInstance(annotation))
hasValid = true;
else if (RequestBody.class.isInstance(annotation))
hasRequestBody = true;
if (hasValid && hasRequestBody)
return true;
}
return false;
}
#SuppressWarnings({"ThrowableInstanceNeverThrown"})
private void validateArg(Object arg, String argName) {
BindingResult result = getBindingResult(arg, argName);
validator.validate(arg, result);
if (result.hasErrors()) {
throw new HttpMessageConversionException("Validation of controller input parameter failed",
new BindException(result));
}
}
private BindingResult getBindingResult(Object target, String targetName) {
return new BeanPropertyBindingResult(target, targetName);
}
#Required
public void setValidator(Validator validator) {
this.validator = validator;
}
}
One limitation with this work-around is that it can only apply a single validator to all controllers. You can also avoid it.
public class TypeMatchingValidator implements Validator, InitializingBean, ApplicationContextAware {
private ApplicationContext context;
private Collection<Validator> validators;
public void afterPropertiesSet() throws Exception {
findAllValidatorBeans();
}
public boolean supports(Class clazz) {
for (Validator validator : validators) {
if (validator.supports(clazz)) {
return true;
}
}
return false;
}
public void validate(Object target, Errors errors) {
for (Validator validator : validators) {
if (validator.supports(target.getClass())) {
validator.validate(target, errors);
}
}
}
private void findAllValidatorBeans() {
Map<String, Validator> validatorBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, Validator.class, true, false);
validators = validatorBeans.values();
validators.remove(this);
}
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
}
Spring XML configuration file using the validator aspect and the meta-validator together:
<!-- enable Spring AOP support -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- declare the validator aspect and inject the validator into it -->
<bean id="validatorAspect" class="com.something.RequestBodyValidatorAspect">
<property name="validator" ref="validator"/>
</bean>
<!-- inject the validator into the DataBinder framework -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="webBindingInitializer">
<bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer" p:validator-ref="validator"/>
</property>
</bean>
<!-- declare the meta-validator bean -->
<bean id="validator" class="com.something.TypeMatchingValidator"/>
<!-- declare all Validator beans, these will be discovered by TypeMatchingValidator -->
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
<bean class="com.something.PersonValidator"/>
<bean class="com.something.AccountValidator"/>
Resources Refs:scottfrederick:pring-3-Validation-Aspect
Solution#4:
Yet another solution for form validation using aop , you can check the blog: form-validation-using-aspect-oriented-programming-aop-in-spring-framework

Related

spring boot override default REST exception handler

I am not able to override default spring boot error response in REST api. I have following code
#ControllerAdvice
#Controller
class ExceptionHandlerCtrl {
#ResponseStatus(value=HttpStatus.UNPROCESSABLE_ENTITY, reason="Invalid data")
#ExceptionHandler(BusinessValidationException.class)
#ResponseBody
public ResponseEntity<BusinessValidationErrorVO> handleBusinessValidationException(BusinessValidationException exception){
BusinessValidationErrorVO vo = new BusinessValidationErrorVO()
vo.errors = exception.validationException
vo.msg = exception.message
def result = new ResponseEntity<>(vo, HttpStatus.UNPROCESSABLE_ENTITY);
result
}
Then in my REST api I am throwing this BusinessValidationException. This handler is called (I can see it in debugger) however I still got default spring boot REST error message. Is there a way to override and use default only as fallback? Spring Boot version 1.3.2 with groovy. Best Regards
Remove #ResponseStatus from your method. It creates an undesirable side effect and you don't need it, since you are setting HttpStatus.UNPROCESSABLE_ENTITY in your ResponseEntity.
From the JavaDoc on ResponseStatus:
Warning: when using this annotation on an exception class, or when setting the reason attribute of this annotation, the HttpServletResponse.sendError method will be used.
With HttpServletResponse.sendError, the response is considered complete and should not be written to any further. Furthermore, the Servlet container will typically write an HTML error page therefore making the use of a reason unsuitable for REST APIs. For such cases it is preferable to use a ResponseEntity as a return type and avoid the use of #ResponseStatus altogether.
I suggest you to read this question: Spring Boot REST service exception handling
There you can find some examples that explain how to combine ErrorController/ ControllerAdvice in order to catch any exception.
In particular check this answer:
https://stackoverflow.com/a/28903217/379906
You should probably remove the annotation #ResponseStatus from the method handleBusinessValidationException.
Another way that you have to rewrite the default error message is using a controller with the annotation #RequestMapping("/error"). The controller must implement the ErrorController interface.
This is the error controller that I use in my app.
#RestController
#RequestMapping("/error")
public class RestErrorController implements ErrorController
{
private final ErrorAttributes errorAttributes;
#Autowired
public MatemoErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
#Override
public String getErrorPath() {
return "/error";
}
#RequestMapping
public Map<String, Object> error(HttpServletRequest aRequest) {
return getErrorAttributes(aRequest, getTraceParameter(aRequest));
}
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
private Map<String, Object> getErrorAttributes(HttpServletRequest aRequest, boolean includeStackTrace)
{
RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
return errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace);
} }

How to return JSON response for unauthorized AJAX calls instead of login page as AJAX response?

I have implemented Spring Security in my application. Whenever someone tries to access any url if authentication is required for these urls user will be redirected to login page. Now, if AJAX call is made for any such url I would like to return JSON response instead of login page's HTML as AJAX response. How can I do that ?
You have to create json for this i am doing here with .net
var url="url";
$.ajax({
type: "get",
dataType: "json",
data:url,
async: true,
url: "testCall",//this can be your api or any server side call
success: function (data) {
},
failure: function () {
alert(textStatus);
}
});
//here is server side code for creating json
[WebMethod(EnableSession = true)]
[ScriptMethod(UseHttpGet = true)]
public void testCall(string url)
{
Context.Response.Write()//here your will just hard code the json data
//it will receive by ajax success method.
}
Faced the same thing not long ago, came out with this solution.
You'll have to redefine the authentication entry point to handle the exception and returning a proper JSON response.
First create a class for your response. Needs to be a POJO.
public class MyErrorResponse {
// your stuff here, and getters / setters
}
Then go define the authentication entry point
public class MyBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
private List<HttpMessageConverter<Object>> messageConverters = new ArrayList<>();
private MediaType retrieveRequestMediaType(HttpServletRequest request) {
String accept = request.getHeader("accept");
if(Strings.isNullOrEmpty(accept))
accept = MediaType.APPLICATION_JSON_VALUE;
MediaType requestMediaType = MediaType.valueOf(accept);
return requestMediaType;
}
private HttpMessageConverter<Object> retrieveMessageConverter(List<HttpMessageConverter<Object>> messageConverters, Class<?> clazz, MediaType mediaType) {
for (HttpMessageConverter<Object> httpMessageConverter : messageConverters) {
if(httpMessageConverter.canWrite(clazz, mediaType)) {
return httpMessageConverter;
}
}
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.warn(String.format("Unauthorized access with session id '%s'", request.getSession().getId()));
MyErrorResponse esponse = new MyErrorResponse();
// populate your response object with all the info you need
MediaType mediaType = MediaType.APPLICATION_JSON;
try{
mediaType = retrieveRequestMediaType(request);
} catch(InvalidMediaTypeException imte) {
// log, do nothing
}
// getting the best fitting message converter, according to the "accept" header of the request
HttpMessageConverter<Object> httpMessageConverter = retrieveMessageConverter(messageConverters, MyErrorResponse.class, mediaType);
if(httpMessageConverter == null) {
log.info("Could not find specific handler. Using JSON.");
httpMessageConverter = retrieveMessageConverter(messageConverters, MyErrorResponse.class, MediaType.APPLICATION_JSON);
}
response.setStatus(HttpStatus.UNAUTHORIZED.value());
ServletServerHttpResponse serverHttpResponse = new ServletServerHttpResponse(errorResponse);
httpMessageConverter.write(response, mediaType, serverHttpResponse);
}
}
Once you got your bean set up, time to wire it up in the security context:
<beans:bean class="[fully qualified name of the entry point class]" id="myBasicAuthenticationEntryPoint">
<beans:property name="messageConverters">
<beans:list>
<!-- add message converters here -->
<!-- Spring provide lots of them, google it -->
</beans:list>
</beans:property>
</beans:bean>
<http use-expressions="true">
<http-basic entry-point-ref="myBasicAuthenticationEntryPoint" />
<!-- add other stuff here, if needed -->
</http>
Hope it helps

#InitBinder with #RequestBody escaping XSS in Spring 3.2.4

I am having a #RequestBody annotated argument in my method like this:
#RequestMapping(value = "/courses/{courseId}/{name}/comment", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.OK)
public #ResponseBody CommentContainer addComment(#PathVariable Long courseId,
#ActiveAccount Account currentUser,
#Valid #RequestBody AddCommentForm form,
BindingResult formBinding,
HttpServletRequest request) throws RequestValidationException {
.....
}
Then I have a #InitBinder annotated method in the same controller:
#InitBinder
public void initBinder(WebDataBinder dataBinder) {
dataBinder.registerCustomEditor(AddCommentForm.class, new StringEscapeEditor());
}
My StringEscapeEditor is not running. But my initBinder method is. So it does not mapping my form to the escape editor. This seems right after reading this thread (Where it seems like #RequestMapping is not supported by #InitBinder):
spring mvc #InitBinder is not called when processing ajax request
And i tested to map a #PathVariable string and then my editor is working.
This is a big deal in my application since most of my bindings is done with #RequestBody and it would be great if i could apply some custom bindings to it.
What is the most common way to solve this problem? and to escape my input data for script attacks.
To escape XSS I suggest that escaping is done while outputting the data, because correct escaping depends on the output document.
If JSON response generated by #ResponseBody is consumed directly by the client and there is no opportunity to XSS escape the content, then JacksonMessageConverter can be customised to perform XSS escaping on strings.
One can customise JacksonMessageConverter like this:
1) First we create ObjectMapper factory that will create our custom object mapper:
public class HtmlEscapingObjectMapperFactory implements FactoryBean<ObjectMapper> {
private final ObjectMapper objectMapper;
public HtmlEscapingObjectMapperFactory() {
objectMapper = new ObjectMapper();
objectMapper.getJsonFactory().setCharacterEscapes(new HTMLCharacterEscapes());
}
#Override
public ObjectMapper getObject() throws Exception {
return objectMapper;
}
#Override
public Class<?> getObjectType() {
return ObjectMapper.class;
}
#Override
public boolean isSingleton() {
return true;
}
public static class HTMLCharacterEscapes extends CharacterEscapes {
private final int[] asciiEscapes;
public HTMLCharacterEscapes() {
// start with set of characters known to require escaping (double-quote, backslash etc)
asciiEscapes = CharacterEscapes.standardAsciiEscapesForJSON();
// and force escaping of a few others:
asciiEscapes['<'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['>'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['&'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['"'] = CharacterEscapes.ESCAPE_CUSTOM;
asciiEscapes['\''] = CharacterEscapes.ESCAPE_CUSTOM;
}
#Override
public int[] getEscapeCodesForAscii() {
return asciiEscapes;
}
// and this for others; we don't need anything special here
#Override
public SerializableString getEscapeSequence(int ch) {
return new SerializedString(StringEscapeUtils.escapeHtml4(Character.toString((char) ch)));
}
}
}
(inspiration for HtmlCharacterEscapes came from this question: HTML escape with Spring MVC and Jackson Mapper)
2) Then we register the message converter that uses our custom object mapper (example in xml config):
<bean id="htmlEscapingObjectMapper" class="com.example.HtmlEscapingObjectMapperFactory" />
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" p:objectMapper-ref="htmlEscapingObjectMapper" />
</mvc:message-converters>
</mvc:annotation-driven>
Now all the JSON messages created by #ResponseBody should have strings escaped as specified in HTMLCharacterEscapes.
Alternative solutions to the problem:
XSS escape what you need in the controller body after the objects have been deserialised
maybe XSS escape in javascript on the client before outputting the content
In addition to doing output escaping, it may be useful to also do some input validation (using standard Spring validation methods) to block some of the content that you don't want to be entered into the system / database.
EDIT: JavaConfig
I haven't tried this out but in Java config it should work like this (you won't need Factory Bean from above because you can set up everything in config in this case):
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
converters.add(buildHtmlEscapingJsonConverter());
}
private MappingJacksonHttpMessageConverter buildHtmlEscapingJsonConverter() {
MappingJacksonHttpMessageConverter htmlEscapingConverter = new MappingJacksonHttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.getJsonFactory().setCharacterEscapes(new HTMLCharacterEscapes());
htmlEscapingConverter.setObjectMapper(objectMapper);
return htmlEscapingConverter;
}
Please be aware that any other non-json default message converters that would normally be configured will now be lost (e.g. XML converters etc..) and if you need them, you will need to add them manually (you can see what's active by default here in section 2.2: http://www.baeldung.com/spring-httpmessageconverter-rest)

Spring MVC redirect with variables

I have following Spring MVC 3.2.4 method:
#RequestMapping(value = "/products/{product}", method = RequestMethod.POST)
public String update(Product product, #Valid #ModelAttribute("productForm") ProductForm productForm, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
return "products/view";
}
mapper.map(productForm, product);
productService.saveProduct(product);
return "redirect:/products/{product}";
}
After success it should redirect back user to detail of product. Problem is that instead of redirecting to page "/products/1" I am redirected to page "/products/Product [code=1234567890, name=Nejaky]". It looks like placeholder {product} is replaced by product.toString() instead of original ID from URL.
I am using built-in Spring Data converter:
<mvc:annotation-driven conversion-service="conversionService">
<mvc:argument-resolvers>
<bean class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
</mvc:argument-resolvers>
</mvc:annotation-driven>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean" />
<bean class="org.springframework.data.repository.support.DomainClassConverter">
<constructor-arg ref="conversionService" />
</bean>
What should I do to make it work correctly and redirect me back to "/products/1" without doing things like "redirect:/product" + product.getId()?
Our story starts in RedirectView source code, in the method replaceUriTemplateVariables.
protected StringBuilder replaceUriTemplateVariables(
String targetUrl, Map<String, Object> model, Map<String, String> currentUriVariables, String encodingScheme)
throws UnsupportedEncodingException {
StringBuilder result = new StringBuilder();
Matcher m = URI_TEMPLATE_VARIABLE_PATTERN.matcher(targetUrl);
int endLastMatch = 0;
while (m.find()) {
String name = m.group(1);
Object value = model.containsKey(name) ? model.remove(name) : currentUriVariables.get(name);
Assert.notNull(value, "Model has no value for '" + name + "'");
result.append(targetUrl.substring(endLastMatch, m.start()));
result.append(UriUtils.encodePathSegment(value.toString(), encodingScheme));
endLastMatch = m.end();
}
result.append(targetUrl.substring(endLastMatch, targetUrl.length()));
return result;
}
As you had predicted, the method uses value.toString() where value is your product object in the Model. No other component like a conversion system is involved here. Your options are as follows:
Use
"redirect:/product" + product.getId()
Add a model attribute called "productId" and use that in your view name
model.addAttribute("productId", product.getId());
"redirect:/product/{productId}"
Or use uri variables. I don't have information on those yet.
Ok, finally found reason for this. I had to annotate product param with #PathVariable. Wondering that it worked without it.
I know it's an old question but for anyone facing the same problem here is the answer
Just inject RedirectAttributes to your controller and use redirectAtrr.addAttribute([attrbuteName],[attributeValue])
#RequestMapping(value = "/products/{product}", method = RequestMethod.POST)
public String update(Product product,#Valid,#ModelAttribute("productForm") ProductForm productForm,BindingResult bindingResult,Model model,RedirectAttributes redirectAttr) {
if (bindingResult.hasErrors()) {
return "products/view";
}
mapper.map(productForm, product);
productService.saveProduct(product);
redirectAttr.addAttributte("productId",product.getId());
return "redirect:/products/{productId}";
}
Read documentation for more understanding.

GWT does not work with Spring REST service

I wrote a Spring REST application. I tested it with curl command and it worked truly.
In another GWT Ajax application I have an RequestBuilder object that it does not work with my Spring Rest: after calling sendRequest method, the event onResponseReceived is fired but the getText method returns an empty string.
this is a part of my spring servlet.xml configuration file
<bean id="jsonmembertemplate"
class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" >
<property name="contentType" value="text/javascript;charset=UTF-8"/>
<property name="disableCaching" value="false"/>
</bean>
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" />
and a part of my controller class
#RequestMapping(method = RequestMethod.GET, value="/member/get/{memberid}")
public String getMember(Model model, #PathVariable("memberid") int id, HttpServletResponse response) {
model.addAttribute("member", memberDao.get(id));
return "jsonmembertemplate";
}
and gwt code
private RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, "http://localhost:8080/depna-nat-server/member/get/1?");
try {
rb.setHeader("Content-Type", "text/javascript;charset=UTF-8");
rb.sendRequest(null, new RequestCallback() {
#Override
public void onResponseReceived(Request request, Response response) {
Window.alert(Integer.toString(response.getStatusCode()));
Window.alert(response.getStatusText());
Window.alert(Integer.toString(response.getText().length()));
area.setText(response.getText());
}
#Override
public void onError(Request request, Throwable exception) {
Window.alert("fail");
}
});
} catch (RequestException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
I have a couple of gwt applications working with json and xml rest services offered by spring, we use spring instead of gwt (rpc or rf) because these services are offered to 3party apps as well.
I started my first project with RequestBuilder and we dind't have any problem, so maybe you have some issue in your code, could you inspect the traffic and post the errors you have?
If you are running a cross-domain issue (statusCode=0 normally means it) , add a filter to your servlet container, take a look to this document.
I finally opted to use gwtquery-ajax and gquery-data-binding because it made really easy to consume these services and to map them to java objects.

Resources