Enabling Cross Origin Requests for a Sprint MVC RESTful Web Service
Skip the definition of CORS, I will test 4 implementations which from Spring official document.
1. Preparation:
1-1. Create a resource controller
The service was deployed at domain api.novastartupclassic.com
1 2 3 4 5 6 7 8 9 10 11 |
@RestController @RequestMapping( "/v1/authc" ) public class GreetingController { @GetMapping("/member/register") public Object register( @RequestBody Member member) { ...... return Re.ok( "Registered successfully." ).callback(); } } |
1-2.
1-3. Create an Ajax frontend Request
The web page was deployed at domain shop.novastartupclassic.com
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$.ajax({ type: "POST", url: 'http://api.novastartupclassic.com/v1/authc/member/register', contentType: 'application/json;charset=UTF-8', data: JSON.stringify(member), dataType: 'json', cache: false, success: function (data) { if (data.code == ur.statusCode.SUCCESS) { } else { ur.api.pages.utils.gritterError(data.message); } } }); |
1-4. Spring MVC
1 |
Version 5.0.2.RELEASE |
1-5. Virtual Host
Und windows: C:\Windows\System32\drivers\etc\hosts
1 2 |
127.0.0.1 api.novastartupclassic.com 127.0.0.1 shop.novastartupclassic.com |
1-6. Server Reverse Proxy
Here Apache is using.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<VirtualHost *:80> ServerName api.novastartupclassic.com ProxyPreserveHost On <Proxy> Order deny,allow Allow from all </Proxy> //These settings are optional, if you have problem, please enable them. #Header always set Access-Control-Allow-Origin "*" #Header always set Access-Control-Max-Age "3600" #Header always set Access-Control-Allow-Headers "x-requested-with, Content-Type, origin, Authorization, refresh-token, accept, client-security-token" ProxyPass / http://127.0.0.1:8080/nova-api/ ProxyPassReverse / http://127.0.0.1:8080/nova-api/ </VirtualHost> |
2. Implementation
2-1. 1. Global CORS configuration with JavaConfig
Create one global CORS Configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Configuration @EnableWebMvc public class CorsConfigurerAdapter extends WebMvcConfigurerAdapter { @Override public void addCorsMappings( CorsRegistry registry ) { registry.addMapping( "/v1/**" ) .allowedOrigins( "http://shop.novastartupclassic.com" ) .allowedMethods( "GET", "POST", "PUT", "DELETE", "OPTIONS" ) .allowedHeaders( "x-requested-with", "Authorization", "refresh-token" ) .exposedHeaders( "header1", "header2" ) .allowCredentials( false ) //allow cookie .maxAge( 3600 ); } } |
and inject it into the container:
1 |
<bean class="net.mobabel.nova.api.config.CorsConfigurerAdapter"></bean> |
2-1-1. Test Result:
Failed. 403 Forbidden as the response
Invalid CORS request
WebMvcConfigurerAdapter is deprecated, and I also tried to update the CorsConfigurerAdapter, but the same problem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Configuration @EnableWebMvc public class CorsConfigurerAdapter implements WebMvcConfigurer { @Override public void addCorsMappings( CorsRegistry registry ) { registry.addMapping( "/v1/**" ) .allowedOrigins( "http://shop.novastartupclassic.com" ) .allowedMethods( "GET", "POST", "PUT", "DELETE", "OPTIONS" ) .allowedHeaders( "x-requested-with", "Authorization", "refresh-token" ) .exposedHeaders( "header1", "header2" ) .allowCredentials( false ) //allow cookie .maxAge( 3600 ); } } |
2-2.
2-3. 2. Global CORS configuration with XML Config
Add the following XML configuration in Spring context XML.
1 2 3 4 5 6 7 8 9 |
<mvc:cors> <mvc:mapping path="/v1/**" allowed-origins="http://shop.novastartupclassic.com" allowed-methods="GET, POST, PUT, DELETE, OPTIONS" allowed-headers="x-requested-with, Authorization, refresh-token" exposed-headers="header1, header2" allow-credentials="false" max-age="3600" /> </mvc:cors> |
2-3-1. Test Result:
Failed. 403 Forbidden as the response
Invalid CORS request
2-4. 3. Global CORS configuration with Filter
Create one Filter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@Component public class CORSFilter implements Filter { @Override public void init( FilterConfig filterConfig ) throws ServletException { } @Override public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; String origin = (String) servletRequest.getRemoteHost() + ":" + servletRequest.getRemotePort(); response.setHeader( "Access-Control-Allow-Origin", "http://shop.novastartupclassic.com" ); response.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" ); response.setHeader( "Access-Control-Max-Age", "3600" ); response.setHeader( "Access-Control-Allow-Headers", "x-requested-with, Authorization, refresh-token" ); response.setHeader( "Access-Control-Allow-Credentials", "false" ); filterChain.doFilter( servletRequest, servletResponse ); } @Override public void destroy() { } } |
Then modify web.xml to make the filter work.
1 2 3 4 5 6 7 8 |
<filter> <filter-name>cors</filter-name> <filter-class>net.mobabel.nova.api.filter.CORSFilter</filter-class> </filter> <filter-mapping> <filter-name>cors</filter-name> <url-pattern>/v1/*</url-pattern> </filter-mapping> |
2-4-1. Test Result:
200 response But Failed.
Why? Because there is only one preflighted request which in this snapshot. There is no following our actual POST request.
And frontend detected the XHR.onerror
The reason is clear, Content-Type is not allowed in the header, we can add it. Modify the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Component public class CORSFilter implements Filter { @Override public void init( FilterConfig filterConfig ) throws ServletException { } @Override public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; String origin = (String) servletRequest.getRemoteHost() + ":" + servletRequest.getRemotePort(); response.setHeader( "Access-Control-Allow-Origin", "http://shop.novastartupclassic.com" ); response.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" ); response.setHeader( "Access-Control-Max-Age", "3600" ); //you can also use * here to allow all types header response.setHeader( "Access-Control-Allow-Headers", "Content-Type, x-requested-with, Authorization, refresh-token" ); response.setHeader( "Access-Control-Allow-Credentials", "false" ); filterChain.doFilter( servletRequest, servletResponse ); } @Override public void destroy() { } } |
2-4-2. Test again
Successfully.
This is preflight request.
This is actual POST request with Payload Json data.
2-5. What is Preflighted Request?
“preflighted” requests first send an HTTP request by the OPTIONS
method to the resource on the other domain, in order to determine whether the actual request is safe to send. Cross-site requests are preflighted like this since they may have implications to user data.
This is the CORS request flow chart.
2-6. 4. Controller method CORS configuration
The CORS annotation was added for my special request method. It is also possible to add this annotation at the controller class level as well, in order to enable CORS on all handler methods of this class.
1 2 3 4 5 6 |
@CrossOrigin(origins = "http://shop.novastartupclassic.com") @GetMapping("/member/register") public Object register( @RequestBody Member member) { ...... return Re.ok( "Registered successfully." ).callback(); } |
2-6-1. Test Result:
Successfully.
Anyway, adding annotations for Rest controllers or methods is not convenient for development.
3. Conclusion
Only the implementation 3 and 4 have been tested as correct methods for Spring MVC. You can use annotation for controllers or global filter for CORS.
4. Reference
从零开始学 Java – Spring MVC 实现跨域资源 CORS 请求