瀏覽器的同源策略阻止讀取來(lái)自不同來(lái)源的資源。這種機(jī)制阻止惡意站點(diǎn)讀取另一個(gè)站點(diǎn)的數(shù)據(jù),但它也阻止合法使用。
一般情況下,我們可以通過(guò)兩種方式解決瀏覽器的同源策略,JSONP和CORS,CORS解決方案更為通用(推薦)。
瀏覽器如何做資源請(qǐng)求的?
瀏覽器和服務(wù)器可以使用超文本傳輸??協(xié)議 (HTTP) 通過(guò)網(wǎng)絡(luò)交換數(shù)據(jù)。 HTTP 定義了請(qǐng)求者和響應(yīng)者之間的通信規(guī)則,包括獲取資源所需的信息。
HTTP 頭用于協(xié)商客戶端和服務(wù)器之間的消息交換類(lèi)型,并用于確定訪問(wèn)權(quán)限。瀏覽器的請(qǐng)求和服務(wù)器的響應(yīng)消息都分為header和body兩部分:
header
有關(guān)消息的信息,例如消息類(lèi)型或消息編碼。標(biāo)頭可以包括表示為鍵值對(duì)的各種信息。請(qǐng)求頭和響應(yīng)頭包含不同的信息。
Sample Request header
Accept: text/html
Cookie: Version=1
以上相當(dāng)于說(shuō)“我想接收 HTML 作為響應(yīng)。這是我擁有的一個(gè) cookie”。
Sample Response header
Content-Encoding: gzip
Cache-Control: no-store
以上相當(dāng)于說(shuō)“數(shù)據(jù)是用gzip編碼的。請(qǐng)不要緩存它。”
body
消息本身。這可以是純文本、二進(jìn)制圖像、JSON、HTML 等。
CORS是如何工作的?
同源策略告訴瀏覽器阻止跨源請(qǐng)求。當(dāng)你想從不同的源獲取公共資源時(shí),資源提供服務(wù)器需要告訴瀏覽器“請(qǐng)求來(lái)自的這個(gè)源可以訪問(wèn)我的資源”。瀏覽器記住這一點(diǎn)并允許跨源資源共享。
步驟一:客戶端(瀏覽器)請(qǐng)求
當(dāng)瀏覽器發(fā)出跨域請(qǐng)求時(shí),瀏覽器會(huì)添加一個(gè)帶有當(dāng)前源(方案、主機(jī)和端口)的 Origin 標(biāo)頭。
步驟二:服務(wù)器響應(yīng)
在服務(wù)器端,當(dāng)服務(wù)器看到此標(biāo)頭并希望允許訪問(wèn)時(shí),它需要在響應(yīng)中添加一個(gè) Access-Control-Allow-Origin 標(biāo)頭,指定請(qǐng)求來(lái)源(或 * 以允許任何來(lái)源。)
步驟三:瀏覽器收到響應(yīng)
當(dāng)瀏覽器看到帶有適當(dāng) Access-Control-Allow-Origin 標(biāo)頭的響應(yīng)時(shí),瀏覽器允許與客戶端站點(diǎn)共享響應(yīng)數(shù)據(jù)。
CORS 實(shí)戰(zhàn)
打開(kāi)瀏覽器控制臺(tái)。(以Chrome為例,F(xiàn)12即可打開(kāi))
輸入以下命令,表現(xiàn)如下:
fetch('https://cors-demo.glitch.me/', {mode:'cors'})
其中https://cors-demo.glitch.me/的響應(yīng)頭如下:
accept-ranges: bytes
cache-control: public, max-age=0
content-length: 53
content-type: application/json; charset=UTF-8
date: Sun, 26 Sep 2021 02:24:57 GMT
etag: W/"35-166a7329ce0"
last-modified: Wed, 24 Oct 2018 17:50:04 GMT
x-powered-by: Express
換另外一個(gè)網(wǎng)站試試:
fetch('https://cors-demo.glitch.me/allow-cors', {mode:'cors'})
其中https://cors-demo.glitch.me/allow-cors的響應(yīng)頭如下:
accept-ranges: bytes
access-control-allow-origin: *
cache-control: public, max-age=0
content-length: 53
content-type: application/json; charset=UTF-8
date: Sun, 26 Sep 2021 02:24:37 GMT
etag: W/"35-166a7329ce0"
last-modified: Wed, 24 Oct 2018 17:50:04 GMT
x-powered-by: Express
僅僅因?yàn)樘砑恿隧憫?yīng)頭access-control-allow-origin即解決了跨域問(wèn)題。、
CORS解決帶cookie跨域問(wèn)題
出于隱私原因,CORS 通常用于“匿名請(qǐng)求”——請(qǐng)求未識(shí)別請(qǐng)求者的請(qǐng)求。如果您想在使用 CORS(可以識(shí)別發(fā)送者)時(shí)發(fā)送 cookie,您需要向請(qǐng)求和響應(yīng)添加額外的標(biāo)頭。
請(qǐng)求
添加credentials: 'include'
到請(qǐng)求參數(shù)中即可實(shí)現(xiàn)帶cookie進(jìn)行跨域請(qǐng)求
fetch('https://example.com', {
mode: 'cors',
credentials: 'include'
})
響應(yīng)
Access-Control-Allow-Origin
必須設(shè)置特定的值 (不能使用通配符*
) 并且必須設(shè)置Access-Control-Allow-Credentials 為 true
.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
預(yù)檢請(qǐng)求
如果 Web 應(yīng)用程序需要復(fù)雜的 HTTP 請(qǐng)求,瀏覽器會(huì)在請(qǐng)求鏈的前面添加一個(gè)預(yù)檢請(qǐng)求。"預(yù)檢請(qǐng)求“的使用,可以避免跨域請(qǐng)求對(duì)服務(wù)器的用戶數(shù)據(jù)產(chǎn)生未預(yù)期的影響。
CORS 規(guī)范將復(fù)雜請(qǐng)求定義為
- 請(qǐng)求采用了除 GET, POST, or HEAD之外的請(qǐng)求方法
- 請(qǐng)求采用除
Accept
,Accept-Language
orContent-Language
之前的請(qǐng)求頭 - 請(qǐng)求頭包含
Content-Type
但值不是application/x-www-form-urlencoded
,multipart/form-data
,text/plain
如果需要,瀏覽器會(huì)創(chuàng)建預(yù)檢請(qǐng)求。這是一個(gè)如下所示的 OPTIONS 請(qǐng)求,并在實(shí)際請(qǐng)求消息之前發(fā)送。
OPTIONS /data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: DELETE
在服務(wù)器端,應(yīng)用程序需要使用有關(guān)應(yīng)用程序從該源接受的方法的信息來(lái)響應(yīng)預(yù)檢請(qǐng)求。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, DELETE, HEAD, OPTIONS
服務(wù)器響應(yīng)還可以包含一個(gè) Access-Control-Max-Age 標(biāo)頭,以指定緩存預(yù)檢結(jié)果的持續(xù)時(shí)間(以秒為單位),因此客戶端無(wú)需在每次發(fā)送復(fù)雜請(qǐng)求時(shí)都發(fā)出預(yù)檢請(qǐng)求。
完整請(qǐng)求鏈路如下:
Java中如何解決CORS
為特定請(qǐng)求跨域
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@RequestMapping(method = RequestMethod.GET, path = "/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
controller跨域
@CrossOrigin(origins = "http://example.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@RequestMapping(method = RequestMethod.GET, path = "/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
混合
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("http://example.com")
@RequestMapping(method = RequestMethod.GET, "/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
全局跨域
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
Spring Security CORS
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()...
}
}
前端如何解決CORS
- 前端代理(Angular自帶)
- Nginx反向代理
ISTIO CORS
https://istio.io/latest/docs/reference/config/networking/virtual-service/#CorsPolicy
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: ratings-route
spec:
hosts:
- ratings.prod.svc.cluster.local
http:
- route:
- destination:
host: ratings.prod.svc.cluster.local
subset: v1
corsPolicy:
allowOrigins:
- exact: https://example.com
allowMethods:
- POST
- GET
allowCredentials: false
allowHeaders:
- X-Foo-Bar
maxAge: "24h"
allowOrigins *
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: ratings-route
spec:
hosts:
- ratings.prod.svc.cluster.local
http:
- route:
- destination:
host: ratings.prod.svc.cluster.local
subset: v1
corsPolicy:
allowOrigins:
- regex: '.*'
allowMethods:
- POST
- GET
allowCredentials: false
allowHeaders:
- X-Foo-Bar
maxAge: "24h"