徹底搞懂跨域問(wèn)題SpringBoot助你暢通無(wú)阻
環(huán)境:SpringBoot2.7.16
1. 簡(jiǎn)介
跨源資源共享(CORS,或通俗地譯為跨域資源共享)是一種基于 HTTP 頭的機(jī)制,該機(jī)制通過(guò)允許服務(wù)器標(biāo)示除了它自己以外的其他源(域、協(xié)議或端口),使得瀏覽器允許這些源訪問(wèn)加載自己的資源??缭促Y源共享還通過(guò)一種機(jī)制來(lái)檢查服務(wù)器是否會(huì)允許要發(fā)送的真實(shí)請(qǐng)求,該機(jī)制通過(guò)瀏覽器發(fā)起一個(gè)到服務(wù)器托管的跨源資源的“預(yù)檢”請(qǐng)求。在預(yù)檢中,瀏覽器發(fā)送的頭中標(biāo)示有 HTTP 方法和真實(shí)請(qǐng)求中會(huì)用到的頭。
跨源 HTTP 請(qǐng)求例子:運(yùn)行在https://www.a.com 的 JavaScript 代碼使用 XMLHttpRequest 來(lái)發(fā)起一個(gè)到 https://www.b.com/data.json 的請(qǐng)求。
出于安全性,瀏覽器限制腳本內(nèi)發(fā)起的跨源 HTTP 請(qǐng)求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。這意味著使用這些 API 的 Web 應(yīng)用程序只能從加載應(yīng)用程序的同一個(gè)域請(qǐng)求 HTTP 資源,除非響應(yīng)報(bào)文包含了正確 CORS 響應(yīng)頭。
圖片
什么情況下需要 CORS?
這份跨源共享標(biāo)準(zhǔn)允許在下列場(chǎng)景中使用跨站點(diǎn) HTTP 請(qǐng)求:
- XMLHttpRequest 或 Fetch API 發(fā)起的跨源 HTTP 請(qǐng)求。
- Web 字體(CSS 中通過(guò) @font-face 使用跨源字體資源),因此,網(wǎng)站就可以發(fā)布 TrueType 字體資源,并只允許已授權(quán)網(wǎng)站進(jìn)行跨站調(diào)用。
- WebGL 貼圖。
- 使用 drawImage() 將圖片或視頻畫(huà)面繪制到 canvas。
- 來(lái)自圖像的 CSS 圖形 (en-US)。
跨源資源共享標(biāo)準(zhǔn)新增了一組 HTTP 標(biāo)頭字段,允許服務(wù)器聲明哪些源站通過(guò)瀏覽器有權(quán)限訪問(wèn)哪些資源。另外,規(guī)范要求,對(duì)那些可能對(duì)服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請(qǐng)求方法(特別是 GET 以外的 HTTP 請(qǐng)求,或者搭配某些 MIME 類型的 POST 請(qǐng)求),瀏覽器必須首先使用 OPTIONS 方法發(fā)起一個(gè)預(yù)檢請(qǐng)求(preflight request),從而獲知服務(wù)端是否允許該跨源請(qǐng)求。服務(wù)器確認(rèn)允許之后,才發(fā)起實(shí)際的 HTTP 請(qǐng)求。在預(yù)檢請(qǐng)求的返回中,服務(wù)器端也可以通知客戶端,是否需要攜帶身份憑證(例如 Cookie 和 HTTP 認(rèn)證相關(guān)數(shù)據(jù))。
CORS 請(qǐng)求失敗會(huì)產(chǎn)生錯(cuò)誤,但是為了安全,在 JavaScript 代碼層面無(wú)法獲知到底具體是哪里出了問(wèn)題。你只能查看瀏覽器的控制臺(tái)以得知具體是哪里出現(xiàn)了錯(cuò)誤。
什么是預(yù)檢請(qǐng)求?
一個(gè) CORS 預(yù)檢請(qǐng)求是用于檢查服務(wù)器是否支持 CORS 即跨域資源共享。它一般是用了以下幾個(gè) HTTP 請(qǐng)求首部的 OPTIONS 請(qǐng)求:Access-Control-Request-Method 和 Access-Control-Request-Headers,以及一個(gè) Origin 首部。當(dāng)有必要的時(shí)候,瀏覽器會(huì)自動(dòng)發(fā)出一個(gè)預(yù)檢請(qǐng)求;所以在正常情況下,前端開(kāi)發(fā)者不需要自己去發(fā)這樣的請(qǐng)求。
舉個(gè)例子,一個(gè)客戶端可能會(huì)在實(shí)際發(fā)送一個(gè) DELETE 請(qǐng)求之前,先向服務(wù)器發(fā)起一個(gè)預(yù)檢請(qǐng)求,用于詢問(wèn)是否可以向服務(wù)器發(fā)起一個(gè) DELETE 請(qǐng)求:
OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org
如果服務(wù)器允許,那么服務(wù)器就會(huì)響應(yīng)這個(gè)預(yù)檢請(qǐng)求。并且其響應(yīng)頭 Access-Control-Allow-Methods 會(huì)將 DELETE 包含在其中:
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400
針對(duì)CORS的HTTP Headers:
- Access-Control-Allow-Origin指示響應(yīng)的資源是否可以被給定的來(lái)源共享。
- Access-Control-Allow-Credentials指示當(dāng)請(qǐng)求的憑證標(biāo)記為 true 時(shí),是否可以公開(kāi)對(duì)該請(qǐng)求響應(yīng)。
- Access-Control-Allow-Headers用在對(duì)預(yù)檢請(qǐng)求的響應(yīng)中,指示實(shí)際的請(qǐng)求中可以使用哪些 HTTP 標(biāo)頭。
- Access-Control-Allow-Methods指定對(duì)預(yù)檢請(qǐng)求的響應(yīng)中,哪些 HTTP 方法允許訪問(wèn)請(qǐng)求的資源。
- Access-Control-Expose-Headers通過(guò)列出標(biāo)頭的名稱,指示哪些標(biāo)頭可以作為響應(yīng)的一部分公開(kāi)。
- Access-Control-Max-Age指示預(yù)檢請(qǐng)求的結(jié)果能被緩存多久。
- Access-Control-Request-Headers用于發(fā)起一個(gè)預(yù)檢請(qǐng)求,告知服務(wù)器正式請(qǐng)求會(huì)使用哪些 HTTP 標(biāo)頭。
- Access-Control-Request-Method用于發(fā)起一個(gè)預(yù)檢請(qǐng)求,告知服務(wù)器正式請(qǐng)求會(huì)使用哪一種 HTTP 請(qǐng)求方法。
- Origin指示獲取資源的請(qǐng)求是從什么源發(fā)起的。
- Timing-Allow-Origin指定特定的源,以允許其訪問(wèn) Resource Timing API 功能提供的屬性值,否則由于跨源限制,這些值將被報(bào)告為零。
大致了解了CORS后,接下來(lái)介紹在SpringBoot中如何解決跨域問(wèn)題
2. 實(shí)戰(zhàn)案例
Spring MVC HandlerMapping實(shí)現(xiàn)提供了對(duì)CORS的內(nèi)置支持。在成功地將請(qǐng)求映射到處理程序之后,HandlerMapping實(shí)現(xiàn)檢查給定請(qǐng)求和處理程序的CORS配置,并采取進(jìn)一步的操作。Preflight requests(預(yù)檢請(qǐng)求)被直接處理,而簡(jiǎn)單和實(shí)際的CORS請(qǐng)求被攔截、驗(yàn)證,并設(shè)置了所需的CORS響應(yīng)頭。
2.1 @CrossOrigin
@CrossOrigin注釋允許對(duì)帶注釋的控制器方法進(jìn)行跨域請(qǐng)求,如下例所示:
@RestController
@RequestMapping("/accounts")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
默認(rèn)情況下,@CrossOrigin允許:
- 所有請(qǐng)求來(lái)源origins。
- 所有請(qǐng)求headers。
- 控制器方法映射到的所有HTTP方法。
注意:默認(rèn)情況下不啟用allowCredentials,因?yàn)樗⒘艘粋€(gè)公開(kāi)敏感的用戶特定信息(如Cookie和CSRF令牌)的信任級(jí)別,并且只應(yīng)在適當(dāng)?shù)牡胤绞褂?。啟用時(shí),必須將allowOrigins設(shè)置為一個(gè)或多個(gè)特定域(但不是特殊值“*”),或者可以使用allowOringPatterns屬性來(lái)匹配一組動(dòng)態(tài)原點(diǎn)。
maxAge 默認(rèn)30分鐘。
@CrossOrigin在類級(jí)別也受支持,并由所有方法繼承,如下例所示:
@CrossOrigin(origins = "https://www.pack.com", maxAge = 3600)
@RestController
@RequestMapping("/accounts")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
}
你可以在類級(jí)別和方法級(jí)別使用@CrossOrigin,如下例所示:
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/accounts")
public class AccountController {
@CrossOrigin("http://www.pack.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
}
2.2 全局配置
除了細(xì)粒度的控制器方法級(jí)配置外,你還可以全局CORS配置??梢栽谌魏蜨andlerMapping上單獨(dú)設(shè)置基于URL的CorsConfiguration映射。
默認(rèn)情況下,全局配置啟用以下功能:
- 所有請(qǐng)求來(lái)源origins.
- 所有請(qǐng)求headers.
- GET, HEAD, and POST methods.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://www.pack.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600) ;
}
}
2.3 CORS過(guò)濾器
可以通過(guò)內(nèi)置的CorsFilter應(yīng)用CORS支持。
注意:如果你嘗試將CorsFilter與Spring Security一起使用,請(qǐng)記住Spring Security內(nèi)置了對(duì)CORS的支持。
要配置篩選器,請(qǐng)將CorsConfigurationSource傳遞給其構(gòu)造函數(shù),如下例所示:
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration() ;
config.setAllowCredentials(true) ;
config.addAllowedOrigin("http://www.pack.com") ;
config.addAllowedHeader("*") ;
config.addAllowedMethod("*") ;
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource() ;
source.registerCorsConfiguration("/**", config) ;
CorsFilter filter = new CorsFilter(source) ;
return filter ;
}
當(dāng)然你也可以使用自定義的Filter來(lái)解決CORS問(wèn)題。
@WebFilter("/*")
public class WebCORSFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res ;
HttpServletRequest request = (HttpServletRequest) req ;
String origin = request.getHeader("Origin") ;
request.setCharacterEncoding("UTF-8") ;
response.setHeader("Access-Control-Allow-Origin", origin) ;
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT") ;
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true") ;
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Account, access-token") ;
chain.doFilter(req, res) ;
}
}
以上是本篇文章的全部?jī)?nèi)容,希望對(duì)你有幫助。