json - origin - Smartgwt RestDataSource con SpringMVC y cliente cruzado
spring boot cors allow all origins (1)
Después de mucho trabajo, tengo una aplicación de servicios web back-end que funciona con Spring-RS, Spring MVC, controladores de Spring, y estos controladores usan al usuario Jackson dentro del marco Spring para convertir las respuestas a JSON.
Aquí se encuentra parte de WEB-INF / myproject-servlet.xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg type="java.lang.String" value="yyyy-MM-dd"></constructor-arg>
</bean>
</property>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="jsonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes" value="application/json"/>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jsonHttpMessageConverter" />
</list>
</property>
</bean>
¡Esta aplicación de servicios web funciona genial! Puedo implementar WAR en mi tomcat local y se implementa correctamente. Puedo probar el controlador unitario para asegurarme de que la URL sea correcta y que la aplicación web esté configurada correctamente en Spring. Puedo presionar la URL y recuperar los datos JSON exactamente como esperaba. La url es:
http://mylocalhost/myproject/invoices/invoiceId/1
devuelve 1 factura.
Ahora, estoy ejecutando una aplicación web SmartGWT, la versión gratuita, y tengo un controlador RestDataScource. He escrito muchas aplicaciones web SmartGWT anteriormente, y estas aplicaciones fueron todo incluido: entidades, dao''s, capa de servicio, controladores y fuentes de datos. Con esto, no hubo problemas de clientes cruzados, siempre que los controladores y las fuentes de datos estuvieran dentro de la misma aplicación. Y no estoy en contra de hacer eso de nuevo, pero quiero tratar de separarlos.
¡Hace poco vi que esto no funciona! Con mi aplicación web SmartGWT ejecutándose localmente en Jetty para el modo de desarrollo. La URL de inicio es:
http://mylocalhost:8888/myapp
Y cuando esto intenta llamar al back-end en
http://mylocalhost:8080/my-ws, then my listgrid gives me a warning message.
Si solo puedo agregar una línea: RPCManager.setAllowCrossDomainCalls (true); ¿Lo agrego dentro de mi RESTDataSource? ¿A dónde agrego esto? ¿Y realmente hará que todo funcione? ¿Hay algo más que deba agregar?
Entonces, estaba buscando en XJSONDataSource y vi que necesitaba hacer algunos cambios en mi RestDataSource para convertirlo a XJsonDataSource. Aquí hay buena información con otra publicación y sugirieron agregar:
// Where do I put this line? the controller or the datasource
String callbackString = request.getParameter("callback");
// Where do I put this code? the controller or the datasource
response.setContentType("text/X-JSON");
response.getWriter().write( callbackString + " ( " + JSONstring + " ) " );
response.setStatus(HttpServletResponse.SC_OK);
No estoy seguro de dónde va este código, entonces necesito ayuda adicional allí. En cuanto al controlador, esta es parte de lo que parece:
@RequestMapping(value = "/invoiceId", method = RequestMethod.GET, headers = "Accept=application/json")
public @ResponseBody
InvoiceDetailDTO getContactTypeByUserId(@RequestBody String invoiceNumber)
{
InvoiceDetailDTO invoiceDetailDto = invoiceService.getInvoiceDetail(invoiceNumber);
// invoiceDetailDto is automatically converted to JSON by Spring
return invoiceDetailDto;
}
En el código de arriba con "solicitud" y "respuesta" tiene que ir al controlador, ¿cómo hago eso?
En última instancia, me encantaría usar mi RestDataSource y modificarla para que funcione de la manera que quiero, e ignorar cualquiera de estos problemas entre sitios. Si necesito usar XJSONDataSource, solo necesito algunos buenos ejemplos reales, y un ejemplo sobre cómo ajustar mis controladores si es necesario.
¡Gracias!
RPCManager.setAllowCrossDomainCalls(true);
debe llamarse durante las primeras etapas de inicialización (por ejemplo, onModuleLoad()
).
getContactTypeByUserId
podría tener que agregar Access-Control-Allow-Origin
como encabezado de respuesta con el valor adecuado.
Verifique http://en.wikipedia.org/wiki/Cross-origin_resource_sharing .
Basado en http://forums.smartclient.com/showthread.php?t=15487 , SmartGWT debe manejar solicitudes de dominios cruzados por sí mismo.
En el peor de los casos, es posible que deba enviar una respuesta de estilo JSONP junto con los encabezados necesarios para que funcione.
En ese caso, probablemente sea mejor tener un método separado, similar al siguiente, para atender solicitudes de SmartGWT.
No he trabajado con XJSONDataSource, así que seguirlo es solo una guía.
// use a slightly different URI to distinguish from other controller method
@RequestMapping(value = "/invoiceId/sgwt", method = RequestMethod.GET, headers = "Accept=application/json")
public @ResponseBody String getContactTypeByUserIdForSgwt(@RequestBody String invoiceNumber,
HttpServletRequest request, HttpServletResponse response) {
// can reuse normal controller method
InvoiceDetailDTO invoiceDetailDto = getContactTypeByUserId(invoiceNumber);
// use jackson or other tool to convert invoiceDetailDto to a JSON string
String JSONstring = convertToJson(invoiceDetailDto);
// will have to check SmartGWT request to make sure actual parameter name that send the callback name
String callbackString = request.getParameter("callback");
response.setContentType("text/X-JSON");
return callbackString + " ( " + JSONstring + " ) " ;
}
Actualizar
Probablemente sea una buena idea limpiar el código (o comenzar desde cero / mínimo) debido a sobras de esfuerzos previos.
Hay tres fases para resolver esto:
1. Haz que SmartGWT funcione correctamente, sin usar el servicio
2. Haga que el servicio funcione correctamente con las solicitudes CORS
3. Cambie SmartGWT para usar el servicio
La fase 1 se debe utilizar para resolver cualquier problema del lado del cliente.
Pase a la fase 2, si el cliente está trabajando con el servicio cuando se implementa en el mismo host / dominio.
Fase 1
Para esto, es posible utilizar una URL de datos que proporcione una respuesta estática, como se explica en las respuestas de RestDataSource JSON .
Coloque la respuesta de muestra en un archivo similar a test.json
y haga que esté accesible desde la aplicación web del cliente.
Mantenga el código de DataSource al mínimo y use setDataURL();
con la ubicación test.json
.
test.json
- cambia (y agrega si es necesario) los nombres y valores de los campos
{
response:{
status:0,
startRow:0,
endRow:3,
totalRows:3,
data:[
{field1:"value", field2:"value"},
{field1:"value", field2:"value"},
{field1:"value", field2:"value"},
]
}
}
Fuente de datos
public class TestDS extends RestDataSource {
private static TestDS instance = new TestDS();
public static TestDS getInstance() {
return instance;
}
private TestDS() {
setDataURL("data/test.json"); // => http://<client-app-host:port>/<context>/data/test.json
setDataFormat(DSDataFormat.JSON);
// setClientOnly(true);
DataSourceTextField field1 = new DataSourceTextField("field1", "Field 1");
DataSourceTextField field2 = new DataSourceTextField("field2", "Field 2");
setFields(field1, field2);
}
}
Fase 2
Verifique las referencias para detalles adicionales.
Encabezados de una solicitud CORS de verificación previa fallida realizada desde una página alojada en localhost:8118
, y servicio alojado en localhost:7117
.
Falló debido a un puerto diferente. Fallará en un esquema diferente (https / ftp / file / etc.) O en otro host / dominio también.
Host: localhost:7117
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://localhost:8118 <= indicates origin to which access should be granted
Access-Control-Request-Method: GET <= indicates the method that will be used in actual request
Access-Control-Request-Headers: content-type <= indicates the headers that will be used in actual request
Server: Apache-Coyote/1.1
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS
Content-Length: 0
Los pares de encabezado de solicitud / respuesta de una solicitud exitosa.
Host: localhost:7117
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://localhost:8118
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: http://localhost:8118
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Content-Type
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS
Content-Length: 0
Host: localhost:7117
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Referer: http://localhost:8118/cors-test.html
Origin: http://localhost:8118
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Content-Type: application/json
Transfer-Encoding: chunked
Para soportar las solicitudes CORS, el backend de servicio debe responder correctamente a la solicitud de OPCIONES de verificación previa, no solo a la llamada de servicio.
Esto se puede hacer usando un ServletFilter.
<filter>
<filter-name>corsfilter</filter-name>
<filter-class>test.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>corsfilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
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())) {
response.addHeader("Access-Control-Allow-Origin", "http://localhost:8118");
// list of allowed methods, Access-Control-Request-Method must be a subset of this
response.addHeader("Access-Control-Allow-Methods", "GET");
// list of allowed headers, Access-Control-Request-Headers must be a subset of this
response.addHeader("Access-Control-Allow-Headers", "Content-Type, If-Modified-Since");
// pre-flight request cache timeout
// response.addHeader("Access-Control-Max-Age", "60");
}
filterChain.doFilter(request, response);
}
}
@RequestMapping(method = RequestMethod.GET, value = "/values", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map> getValues() {
List<Map<String, Object>> values = getValues(); // handle actual data processing and return a list suitable for response
SgwtResponse sgwtResponse = new SgwtResponse(); // A POJO with basic (public) attributes
sgwtResponse.status = 0L;
sgwtResponse.startRow = 0L;
sgwtResponse.endRow = Long.valueOf(values.size());
sgwtResponse.totalRows = sgwtResponse.startRow + sgwtResponse.endRow;
sgwtResponse.data = values; // java.util.List
Map<String, SgwtResponse> jsonData = new HashMap<String, SgwtResponse>();
jsonData.put("response", sgwtResponse);
HttpHeaders headers = new HttpHeaders();
headers.add("Access-Control-Allow-Origin", "*"); // required
return new ResponseEntity<Map>(jsonData, headers, HttpStatus.OK);
}
Una página de prueba simple que usa jQuery para recuperar una respuesta JSON usando XHR.
Cambie la URL y despliegue en la aplicación web del cliente para probar el servicio directamente, sin usar SmartGWT.
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
$(document).ready(function () {
$("#retrieve").click(function () {
$.ajax({
type: "GET",
contentType: "application/json",
url: "<URL-of-service>",
dataType: "json",
success: function (data, status, xhr) {
$("#content").text(JSON.stringify(data, null, 2));
},
error: function (xhr, status, error) {
$("#content").text("Unable to retrieve data");
}
});
});
});
</script>
</head>
<body>
<input type="button" id="retrieve" value="Retrieve"/>
<div id="content"/>
</body>
</html>
If-Modified-Since
header fue requerido en Access-Control-Allow-Headers
para SmartGWT.
Use RPCManager.setAllowCrossDomainCalls(true);
durante la inicialización SmartGWT para evitar la advertencia.
La mayoría de los navegadores modernos (compatibilidad con el navegador 1 ) y SmartGWT RestDataSource son compatibles con las solicitudes CORS.
Utilice XJSONDataSource solo cuando tenga que confiar en JSONP, debido a la incompatibilidad del navegador con las solicitudes CORS.
El envío de Access-Control-Allow-Origin: *
para la solicitud previa al vuelo permitirá a cualquier sitio realizar llamadas de dominio cruzado al servicio, lo que podría suponer un problema de seguridad, además *
no se puede utilizar en determinadas solicitudes CORS.
Un mejor enfoque es especificar el sitio exacto al que se permiten las solicitudes de dominios cruzados: Access-Control-Allow-Origin: http://www.foo.com
.
Probablemente no sea necesario en este caso, pero compruebe los dominios de origen múltiple de Access-Control-Allow-Origin. si es necesario, para encontrar maneras de permitir que múltiples sitios hagan solicitudes de CORS.
Referencias
[1] https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
[2] http://java-success.blogspot.com/2012/11/cors-and-jquery-with-spring-mvc-restful.html