一次正常的請求
最近別人需要調(diào)用我們系統(tǒng)的某一個(gè)功能,對方希望提供一個(gè)api讓其能夠更新數(shù)據(jù)。由于該同學(xué)是客戶端開發(fā),于是有了類似以下代碼。
1
2
3
4
5
6
7
8
|
@RequestMapping (method = RequestMethod.POST, value = "/update.json" , produces = MediaType.APPLICATION_JSON_VALUE) public @ResponseBody Contacter update( @RequestBody Contacter contacterRO) { logger.debug( "get update request {}" , contacterRO.toString()); if (contacterRO.getUserId() == 123 ) { contacterRO.setUserName( "adminUpdate-wangdachui" ); } return contacterRO; } |
客戶端通過代碼發(fā)起http請求來調(diào)用。接著,該同學(xué)又提出:希望通過瀏覽器使用js調(diào)用,于是便有跨域問題。
為何跨域
簡單的說即為瀏覽器限制訪問A站點(diǎn)下的js代碼對B站點(diǎn)下的url進(jìn)行ajax請求。假如當(dāng)前域名是www.abc.com,那么在當(dāng)前環(huán)境中運(yùn)行的js代碼,出于安全考慮,正常情況下不能訪問www.zzz.com域名下的資源。
例如:以下代碼再本域名下可以通過js代碼正常調(diào)用接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
(function() { var url = "http://localhost:8080/api/Home/update.json" ; var data = { "userId" : 123 , "userName" : "wangdachui" }; $.ajax({ url: url, type: 'POST' , dataType: 'json' , data: $.toJSON(data), contentType: 'application/json' } ).done(function(result) { console.log( "success" ); console.log(result); } ).fail(function() { console.log( "error" ); } ) } )() |
輸出為:
1
|
Object {userId: 123 , userName: "adminUpdate-wangdachui" } |
但是在其他域名下訪問則出錯:
1
2
|
OPTIONS http: //localhost:8080/api/Home/update.json XMLHttpRequest cannot load http: //localhost:8080/api/Home/update.json. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 403. |
解決方案
JSONP
使用jsonp來進(jìn)行跨域是一種比較常見的方式,但是在接口已經(jīng)寫好的情況下,無論是服務(wù)端還是調(diào)用端都需要進(jìn)行改造且要兼容原來的接口,工作量偏大,于是我們考慮其他方法。
CORS協(xié)議
按照參考資料的說法:每一個(gè)頁面需要返回一個(gè)名為‘Access-Control-Allow-Origin'的HTTP頭來允許外域的站點(diǎn)訪問。你可以僅僅暴露有限的資源和有限的外域站點(diǎn)訪問。在COR模式中,訪問控制的職責(zé)可以放到頁面開發(fā)者的手中,而不是服務(wù)器管理員。當(dāng)然頁面開發(fā)者需要寫專門的處理代碼來允許被外域訪問。 我們可以理解為:如果一個(gè)請求需要允許跨域訪問,則需要在http頭中設(shè)置Access-Control-Allow-Origin來決定需要允許哪些站點(diǎn)來訪問。如假設(shè)需要允許www.foo.com這個(gè)站點(diǎn)的請求跨域,則可以設(shè)置:Access-Control-Allow-Origin:http://www.foo.com?;蛘逜ccess-Control-Allow-Origin: * 。 CORS作為HTML5的一部分,在大部分現(xiàn)代瀏覽器中有所支持。
CORS具有以下常見的header
1
2
3
4
5
6
7
8
|
Access-Control-Allow-Origin: http: //foo.org Access-Control-Max-Age: 3628800 Access-Control-Allow-Methods: GET,PUT, DELETE Access-Control-Allow-Headers: content-type "Access-Control-Max-Age" 表明在 3628800 秒內(nèi),不需要再發(fā)送預(yù)檢驗(yàn)請求,可以緩存該結(jié)果 "Access-Control-Allow-Methods" 表明它允許GET、PUT、DELETE的外域請求 "Access-Control-Allow-Headers" 表明它允許跨域請求包含content-type頭 |
CORS基本流程
首先發(fā)出預(yù)檢驗(yàn)(Preflight)請求,它先向資源服務(wù)器發(fā)出一個(gè)OPTIONS方法、包含“Origin”頭的請求。該回復(fù)可以控制COR請求的方法,HTTP頭以及驗(yàn)證等信息。只有該請求獲得允許以后,才會發(fā)起真實(shí)的外域請求。
Spring MVC支持CORS
1
|
Response to preflight request doesn 't pass access control check: No ' Access-Control-Allow-Origin ' header is present on the requested resource. Origin ' null ' is therefore not allowed access. The response had HTTP status code 403 . |
從以上這段錯誤信息中我們可以看到,直接原因是因?yàn)檎埱箢^中沒有Access-Control-Allow-Origin這個(gè)頭。于是我們直接想法便是在請求頭中加上這個(gè)header。服務(wù)器能夠返回403,表明服務(wù)器確實(shí)對請求進(jìn)行了處理。
MVC 攔截器
首先我們配置一個(gè)攔截器來攔截請求,將請求的頭信息打日志。
1
2
3
4
5
6
7
8
9
10
11
12
|
DEBUG requestURL:/api/Home/update.json DEBUG method:OPTIONS DEBUG header host:localhost: 8080 DEBUG header connection:keep-alive DEBUG header cache-control:max-age= 0 DEBUG header access-control-request-method:POST DEBUG header origin: null DEBUG header user-agent:Mozilla/ 5.0 (Windows NT 6.1 ; WOW64) AppleWebKit/ 537.36 (KHTML, like Gecko) Chrome/ 49.0 . 2623.87 Safari/ 537.36 DEBUG header access-control-request-headers:accept, content-type DEBUG header accept:*/* DEBUG header accept-encoding:gzip, deflate, sdch DEBUG header accept-language:zh-CN,zh;q= 0.8 ,en;q= 0.6 |
在postHandle里打印日志發(fā)現(xiàn),此時(shí)response的status為403。跟蹤SpringMVC代碼發(fā)現(xiàn),在org.springframework.web.servlet.DispatcherServlet.doDispatch中會根據(jù)根據(jù)request來獲取HandlerExecutionChain,SpringMVC在獲取常規(guī)的處理器后會檢查是否為跨域請求,如果是則替換原有的實(shí)例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Override public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); if (handler == null ) { handler = getDefaultHandler(); } if (handler == null ) { return null ; } // Bean name or resolved handler? if (handler instanceof String) { String handlerName = (String) handler; handler = getApplicationContext().getBean(handlerName); } HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (CorsUtils.isCorsRequest(request)) { CorsConfiguration globalConfig = this .corsConfigSource.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; } |
檢查的方法也很簡單,即檢查請求頭中是否有origin字段
1
2
3
|
public static boolean isCorsRequest(HttpServletRequest request) { return (request.getHeader(HttpHeaders.ORIGIN) != null ); } |
請求接著會交由 HttpRequestHandlerAdapter.handle來處理,根據(jù)handle不同,處理不同的邏輯。前面根據(jù)請求頭判斷是一個(gè)跨域請求,獲取到的Handler為PreFlightHandler,其實(shí)現(xiàn)為:
1
2
3
4
|
@Override public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { corsProcessor.processRequest( this .config, request, response); } |
繼續(xù)跟進(jìn)
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
|
@Override public Boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { if (!CorsUtils.isCorsRequest(request)) { return true ; } ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response); ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request); if (WebUtils.isSameOrigin(serverRequest)) { logger.debug( "Skip CORS processing, request is a same-origin one" ); return true ; } if (responseHasCors(serverResponse)) { logger.debug( "Skip CORS processing, response already contains \"Access-Control-Allow-Origin\" header" ); return true ; } Boolean preFlightRequest = CorsUtils.isPreFlightRequest(request); if (config == null ) { if (preFlightRequest) { rejectRequest(serverResponse); return false ; } else { return true ; } } return handleInternal(serverRequest, serverResponse, config, preFlightRequest); } |
此方法首先會檢查是否為跨域請求,如果不是則直接返回,接著檢查是否同一個(gè)域下,或者response頭里是否具有Access-Control-Allow-Origin字段或者request里是否具有Access-Control-Request-Method。如果滿足判斷條件,則拒絕這個(gè)請求。
由此我們知道,可以通過在檢查之前設(shè)置response的Access-Control-Allow-Origin頭來通過檢查。我們在攔截器的preHandle的處理。加入如下代碼:
1
|
response.setHeader( "Access-Control-Allow-Origin" , "*" ); |
此時(shí)瀏覽器中OPTIONS請求返回200。但是依然報(bào)錯:
1
|
Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response. |
我們注意到:在request的請求頭里有Access-Control-Request-Headers:accept, content-type,但是這個(gè)請求頭的中沒有,此時(shí)瀏覽器沒有據(jù)需發(fā)送請求。嘗試在response中加入:
1
|
response.setHeader( "Access-Control-Allow-Headers" , "Origin, X-Requested-With, Content-Type, Accept" ); |
執(zhí)行成功:Object {userId: 123, userName: “adminUpdate-wangdachui”}。
至此:我們通過分析原理使SpringMVC實(shí)現(xiàn)跨域,原有實(shí)現(xiàn)以及客戶端代碼不需要任何改動。
SpringMVC 4
此外,在參考資料2中,SpringMVC4提供了非常方便的實(shí)現(xiàn)跨域的方法。
在requestMapping中使用注解。 @CrossOrigin(origins = “http://localhost:9000”)
全局實(shí)現(xiàn) .定義類繼承WebMvcConfigurerAdapter
1
2
3
4
5
6
|
public class CorsConfigurerAdapter extends WebMvcConfigurerAdapter{ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping( "/api/*" ).allowedOrigins( "*" ); } } |
將該類注入到容器中:
1
|
< bean class = "com.tmall.wireless.angel.web.config.CorsConfigurerAdapter" ></ bean > |
總結(jié)
以上就是本文關(guān)于Spring實(shí)現(xiàn)處理跨域請求代碼詳解的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題。如有不足之處,歡迎留言指出。感謝朋友們對本站的支持!
原文鏈接:https://www.cnblogs.com/softidea/p/6108066.html