Google

Nov 29, 2012

CORS and jQuery with Spring MVC, RESTful Web Service and Maven

In the post JSONP and jQuery with Spring MVC, RESTful Web Service and Maven we looked at cross domain example with JSONP. JSONP has a number of limitations like, it supports only GET requests and not PUT, POST, DELETE, etc and it does not also send headers across. CORS stands for Cross Origin Resource Sharing, which allows you to share GET, POST, PUT, and DELETE requests and CORS is supported by the modern browsers.The CORS make use of 2 requests.

Request 1: "OPTIONS" request as part of the handshake to determine if cross domain is allowed by the server.

Request 2: GET, POST, PUT, or DELETE request that performs the actual operation on the server.




This example basically extends the JSONP example with only the changes shown here.


Step 1: The JSP file with jQuery, JavaScript, and ajax code snippets. This code snippet also shows extraction of the request headers and populating back the response.


<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  
 <script type="text/javascript" src="/secure/aes-gui/resources/js/jquery/jquery-1.7.2.js"></script>
 
 <title>Spring MVC - CORS and jQuery Tutorial</title>
</head>
<body>
 
<h3>Spring MVC - CORS and jQuery Tutorial</h3>

<div style="border: 1px solid #ccc; width: 600px;">
 Add Two Numbers:
 
 <input id="inputNumber1" name="inputNumber1" type="text" size="5" > +
 <input id="inputNumber2" name="inputNumber2" type="text" size="5">
 <input type="submit" value="Add" onclick="add()" />
 
 Sum: <span id="sum"></span>
 
 
 <input type="submit" value="Show Headers" onclick="displayHeaders()" />
 
 
</div>
 
 
<script type="text/javascript">

 function add() {
  var url = 'http://DEV:8080/aes-gui/simple/poc/main/add';
  
  console.log("logging...............");
  
  $.ajax({
   type : 'GET',
   url : url,
   data : {
    inputNumber1 : $("#inputNumber1").val(),
    inputNumber2 : $("#inputNumber2").val()
   },
   async : false,
   contentType : "application/json",
   crossDomain : true,
   success : function(response, textStatus, jqXHR) {
    console.log("reached here");
    // data contains the result
    // Assign result to the sum id
    $("#sum").replaceWith('<span id="sum">' + response + '</span>');
    console.log(response);

   },
   headers: headerData,
    
   error : function(jqXHR, textStatus, errorThrown) {
    console.log(errorThrown);
   }
  });
 };
 
 function displayHeaders() {
  var req = new XMLHttpRequest();
  req.open('GET', document.location, false);
  req.send(null);
  var headers = req.getAllResponseHeaders().toLowerCase();
  console.log(headers);
     
  var splitHeaders = headers.split(/\r\n|\n|\r/g);
  console.log("split headers:\n"  + splitHeaders);
  for (var i in splitHeaders){
   var currVal = splitHeaders[i];
   var entry = currVal.split(":");
   var headername = entry[0];
   var val = entry [1];
   console.log(headername + "=" + val);
  }
  
  
 }
 
 var headerData = {};
 
  $(document).ready(function() {
     getHeaders();
     console.log("document is ready=" + headerData);
  });
 
 function getHeaders() {
  var req = new XMLHttpRequest();
  req.open('GET', document.location, false);
  req.send(null);
  var headers = req.getAllResponseHeaders().toLowerCase();
 
  var headersArray = {};
  
  var splitHeaders = headers.split(/\r\n|\n|\r/g);
  console.log("split headers:\n"  + splitHeaders);
  for (var i in splitHeaders){
   var currVal = splitHeaders[i];
   var entry = currVal.split(":");
   var headername = entry[0];
   var val = entry [1];
   console.log(headername + "=" + val);
   if(!(typeof headername === undefined) ) {
    if(headername.toUpperCase() === "SID" || headername.toUpperCase() === "MYCUSTOM" || headername.toUpperCase() === "SMUSER"  ){
          headersArray[headername]=val;
    }
      }
   
  }
  
  /* headersArray["firstName"]="arul" ;
  headersArray["lastName"]="kumaran" ; */
             
  console.log(headersArray);
  
  console.log(JSON.stringify(headersArray));
  headerData = headersArray;
  
 }
 
</script>

 
</body>
</html> 

Step 2: The server needs to have request filter that intercepts the "OPTIONS" request and add the header information required for the CORS to work. The headers to note are Access-Control-Allow-Origin and Access-Control-Allow-Headers.


package com.myapp.accounting.aes.poc.controller;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;


public class CorsFilter extends OncePerRequestFilter {

 @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) {
            // CORS "pre-flight" request
            response.addHeader("Access-Control-Allow-Origin", "*");
            response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
            response.addHeader("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with, sid, mycustom, smuser");
            response.addHeader("Access-Control-Max-Age", "1800");//30 min
        }
        filterChain.doFilter(request, response);
    }
}

Step 3: Define the filter on web.xml file. The key is the last two declarations for the filter.

 
<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

 <!-- Processes application requests for Securities component related requests -->
 <servlet>
  <servlet-name>mvc-dispatcher</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>

 <servlet-mapping>
  <servlet-name>mvc-dispatcher</servlet-name>
  <url-pattern>/simple/*</url-pattern>
 </servlet-mapping>

 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
 </context-param>

 <!-- Creates the Spring Container shared by all Servlets and Filters -->
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>

 <filter>
  <filter-name>cors</filter-name>
  <filter-class>com.myapp.accounting.aes.poc.controller.CorsFilter</filter-class>
 </filter>

 <filter-mapping>
  <filter-name>cors</filter-name>
  <url-pattern>/simple/*</url-pattern>
 </filter-mapping>

</web-app>


The filter will intercept the cross domain OPTIONS request add the relevant headers to allow cross domain.


Step 4: The controller that is actually serving the subsequent RESTful GET, POST, PUT or DELETE requests need to send the relevant headers back. So, in your controller you need to populate the relevant headers as well.


 

package com.myapp.accounting.aes.poc.controller;

import java.util.Enumeration;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/poc/main")
public class POCController {

 @RequestMapping(value = "/sum", method = RequestMethod.GET)
 public String getNonAjaxPage(HttpServletRequest request, HttpServletResponse response) {
  Enumeration headerNames = request.getHeaderNames();
  while(headerNames.hasMoreElements()){
   String nextElement = (String)headerNames.nextElement();
   System.out.println(nextElement + "=" + request.getHeaders(nextElement));
   response.addHeader(nextElement, request.getHeader(nextElement));
  }
  
  //adding som custom headers
  response.addHeader("SID", "I573558");
  response.addHeader("MYCUSTOM", "CUSTOM");
  
  return "poc-page/ajax-sum";
 }
 
 @RequestMapping(value = "/add", method = RequestMethod.GET)
 public ResponseEntity<String> add(
   @RequestParam(value = "inputNumber1", required = true) Integer inputNumber1,
   @RequestParam(value = "inputNumber2", required = true) Integer inputNumber2,
   Model model, HttpServletRequest request) {

  // Delegate to service to do the actual adding
  Integer sum = inputNumber1 + inputNumber2;

  String result = null;

  // @ResponseBody will automatically convert the returned value into JSON
  // format. you must have Jackson in your classpath

  
  
  result =  sum.toString();
  
  
  HttpHeaders responseHeaders = new HttpHeaders();
  
  Enumeration headerNames = request.getHeaderNames();
  while(headerNames.hasMoreElements()){
   String nextElement = (String)headerNames.nextElement();
   System.out.println(nextElement + "=" + request.getHeaders(nextElement));
   responseHeaders.set(nextElement, request.getHeader(nextElement));
  }
  
  
  //populating the header required for CORS
  responseHeaders.set("Access-Control-Allow-Origin", "*"); 
  
  
  return new ResponseEntity<String>(result, responseHeaders, HttpStatus.OK);
 }
}


That's all to it. The rest will remain same as the JSONP tutorial.


Labels: ,

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home