同源策略
在瀏覽器中,如果我們直接使用 AJAX 發(fā)送一個(gè)對(duì)其他網(wǎng)站的請(qǐng)求(跨域請(qǐng)求),默認(rèn)情況下是無(wú)法獲取到響應(yīng)的。
這是因?yàn)闉g覽器內(nèi)置的 同源策略 對(duì)客戶端腳本的限制。
默認(rèn)情況下,同源策略 只允許腳本請(qǐng)求同源資源,而對(duì)于請(qǐng)求不同源的腳本在沒(méi)有明確授權(quán)的情況下,無(wú)法讀取對(duì)方資源。
注:同源 指的是:協(xié)議、域名 和 端口 三者都相同
同源策略是瀏覽器內(nèi)置的一個(gè)最核心,也是最基礎(chǔ)的安全功能,它保障了用戶的上網(wǎng)安全。
但是,如果我們確信某個(gè)非同源網(wǎng)站是安全的,我們希望能夠?qū)ζ滟Y源進(jìn)行訪問(wèn),那么,就需要通過(guò)相應(yīng)的機(jī)制進(jìn)行跨域請(qǐng)求。
最常見(jiàn)的前端跨域請(qǐng)求解決方案是 JSONP,它的原理是借助script標(biāo)簽不受瀏覽器同源策略限制,允許跨域請(qǐng)求資源,因此可以通過(guò)script標(biāo)簽的src屬性,進(jìn)行跨域訪問(wèn)。如下代碼所示:
// 1. 前端定義一個(gè) 回調(diào)函數(shù) handleResponse 用來(lái)接收后端返回的數(shù)據(jù)
function handleResponse(data) {
console.log(data);
};
// 2. 動(dòng)態(tài)創(chuàng)建一個(gè) script 標(biāo)簽,并且告訴后端回調(diào)函數(shù)名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran.cn/json?callback=handleResponse';
body.appendChild(script);
// 3. 通過(guò) script.src 請(qǐng)求 `http://www.laixiangran.cn/json?callback=handleResponse`,
// 4. 后端能夠識(shí)別這樣的 URL 格式并處理該請(qǐng)求,然后返回 handleResponse({"name": "laixiangran"}) 給瀏覽器
// 5. 瀏覽器在接收到 handleResponse({"name": "laixiangran"}) 之后立即執(zhí)行 ,也就是執(zhí)行 handleResponse 方法,獲得后端返回的數(shù)據(jù),這樣就完成一次跨域請(qǐng)求了。
雖然 JSONP 可以完成跨域請(qǐng)求,但是它只支持GET請(qǐng)求方式,限制非常大。
于是,為了更好地支持跨域資源請(qǐng)求,W3C 標(biāo)準(zhǔn)就發(fā)布了一套瀏覽器跨域資源共享標(biāo)準(zhǔn):CORS(Cross-origin resource sharing,跨域資源共享)
CORS(跨域資源共享)
CORS 支持多種 HTTP 請(qǐng)求,它其實(shí)就是定義了一套跨域資源請(qǐng)求時(shí),瀏覽器與服務(wù)器之間的交互方式。基本的原理就是通過(guò)自定義的 HTTP 請(qǐng)求頭來(lái)傳遞信息,進(jìn)行驗(yàn)證。
瀏覽器中,將 CORS 請(qǐng)求分為兩種類(lèi)型:
-
簡(jiǎn)單請(qǐng)求:同時(shí)滿足以下兩大條件的請(qǐng)求,即為簡(jiǎn)單請(qǐng)求:
- 請(qǐng)求的方法是
HEAD、GET或者是POST三種之一 - 請(qǐng)求頭不超出以下幾種字段:
Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(其值為:application/x-www-form-urlencoded、multipart/form-data或text/plain三者之一)
- 請(qǐng)求的方法是
非簡(jiǎn)單請(qǐng)求:不是簡(jiǎn)單請(qǐng)求的都屬于非簡(jiǎn)單請(qǐng)求。
瀏覽器對(duì)于 簡(jiǎn)單請(qǐng)求 和 非簡(jiǎn)單請(qǐng)求 的 CORS 處理機(jī)制不一樣,具體如下:
-
簡(jiǎn)單請(qǐng)求:對(duì)于簡(jiǎn)單請(qǐng)求的 CORS,瀏覽器的處理機(jī)制流程如下:
- 瀏覽器會(huì)在請(qǐng)求頭添加一個(gè)額外的
Origin頭部,其值為當(dāng)前請(qǐng)求頁(yè)面的源信息(即:協(xié)議 + 域名 + 端口)。如下所示:
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...- 服務(wù)器接收到請(qǐng)求后,查看到
Origin頭部指定的源信息,如果同意該請(qǐng)求,就會(huì)為下發(fā)的響應(yīng)添加頭部Access-Control-Allow-Origin,其值為請(qǐng)求的源信息(或者是*,表示允許任意源信息)。如下所示:
Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8- 瀏覽器接收到響應(yīng)后,會(huì)查看下是否有
Access-Control-Allow-Origin頭部信息,如果沒(méi)有或者其值不匹配當(dāng)前源信息,那么瀏覽器就會(huì)禁止響應(yīng)該 CORS 請(qǐng)求,當(dāng)前頁(yè)面的 AJAX 請(qǐng)求的onerror函數(shù)會(huì)得到回調(diào)。
反之,如果瀏覽器驗(yàn)證通過(guò),則跨域請(qǐng)求成功。
注:CORS 請(qǐng)求默認(rèn)不發(fā)送 Cookie 和 HTTP 認(rèn)證信息,如果需要把 Cookie 發(fā)送給服務(wù)器,則 AJAX 和 服務(wù)器必須同時(shí)打開(kāi) Credentials 字段,如下所示:
- 服務(wù)器需設(shè)置:
Access-Control-Allow-Credentials: true - AJAX 需設(shè)置:
new XMLHttpRequest().withCredentials = true;
注:如果 AJAX 發(fā)送了 Cookie,那么服務(wù)器的
Access-Control-Allow-Origin則不能設(shè)置為*,必須指定該明確的、與請(qǐng)求網(wǎng)頁(yè)一致的域名。 - 瀏覽器會(huì)在請(qǐng)求頭添加一個(gè)額外的
-
非簡(jiǎn)單請(qǐng)求:非簡(jiǎn)單請(qǐng)求是那種對(duì)服務(wù)器有特殊要求的請(qǐng)求,比如
PUT或DELETE請(qǐng)求,或者是Content-type: application/json請(qǐng)求...
瀏覽器檢測(cè)到非簡(jiǎn)單請(qǐng)求的 CORS 時(shí),在正式發(fā)送請(qǐng)求前,會(huì)先進(jìn)行一次探測(cè)請(qǐng)求(preflight),通過(guò)才會(huì)發(fā)送正式請(qǐng)求,具體過(guò)程如下:- 瀏覽器檢測(cè)到非簡(jiǎn)單 CORS 請(qǐng)求,則先發(fā)送一個(gè)探測(cè)請(qǐng)求,請(qǐng)求方式為
OPTIONS,如下所示:
OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...可以看到
OPTIONS請(qǐng)求,除了攜帶Origin請(qǐng)求頭外,還額外攜帶了以下幾個(gè)請(qǐng)求頭:-
Access-Control-Request-Method:該字段必須攜帶,表示 CORS 請(qǐng)求使用的 HTTP 請(qǐng)求方法 -
Access-Control-Request-Headers:可選字段,表示 CORS 請(qǐng)求發(fā)送的自定義頭部信息,多個(gè)頭部以逗號(hào)進(jìn)行分隔
- 服務(wù)器收到瀏覽器發(fā)送的探測(cè)請(qǐng)求后,檢測(cè)
Origin、Access-Control-Request-Method和Access-Control-Request-Headers都在自己的許可名單時(shí),就會(huì)允許跨域請(qǐng)求,返回響應(yīng)。如下所示:
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain響應(yīng)主要包含如下請(qǐng)求頭信息:
-
Access-Control-Allow-Origin:表示允許進(jìn)行跨域請(qǐng)求的域 -
Access-Control-Allow-Methods:必須字段,表示允許 CORS 請(qǐng)求的方法 -
Access-Control-Allow-Headers:表示允許 CORS 請(qǐng)求的頭部 -
Access-Control-Max-Age:表示探測(cè)請(qǐng)求緩存時(shí)間(單位:秒)
- 一旦瀏覽器通過(guò)探測(cè)請(qǐng)求,以后每次進(jìn)行 CORS 請(qǐng)求時(shí),就重復(fù)簡(jiǎn)單請(qǐng)求步驟(直至探測(cè)請(qǐng)求緩存過(guò)期)。
而如果探測(cè)請(qǐng)求通不過(guò)(即響應(yīng)沒(méi)有任何 CORS 相關(guān)的頭部信息字段),瀏覽器就知道服務(wù)器會(huì)拒絕該 CORS 請(qǐng)求,于是就直接觸發(fā)一個(gè)錯(cuò)誤,回調(diào)給 AJAX 請(qǐng)求的onerror方法。
- 瀏覽器檢測(cè)到非簡(jiǎn)單 CORS 請(qǐng)求,則先發(fā)送一個(gè)探測(cè)請(qǐng)求,請(qǐng)求方式為
Spring Boot 配置支持 CORS
一個(gè)很幸運(yùn)的事情就是:瀏覽器會(huì)自動(dòng)幫我們完成 CORS 相關(guān)操作,用戶完全無(wú)感知。
對(duì)于開(kāi)發(fā)者來(lái)說(shuō),前端代碼無(wú)需修改,如果是 CORS 請(qǐng)求,瀏覽器會(huì)自動(dòng)幫我們加上相應(yīng)的請(qǐng)求頭進(jìn)行請(qǐng)求...
因此,實(shí)現(xiàn) CORS 通信需要配置的就只是服務(wù)器端。
下面介紹下在 Spring Boot 中配置 CORS 通信,主要介紹幾種常用的配置方法,如下所示:
-
@CrossOrigin:該注解可用于方法和類(lèi)上,注解在方法上,表示對(duì)該方法的請(qǐng)求進(jìn)行 CORS 校驗(yàn),注解在類(lèi)上(即Controller上),表示該類(lèi)內(nèi)的方法都遵循該 CORS 校驗(yàn)。如下所示:注:前端頁(yè)面 AJAX 請(qǐng)求源碼可查看 附錄 內(nèi)容。
@Slf4j @RestController @RequestMapping("cors") @CrossOrigin( value = "http://127.0.0.1:5500", maxAge = 1800, allowedHeaders = "*") public class CorsController { @PostMapping("/") public String add(@RequestParam("name") String name, @RequestHeader("Origin") String origin) { log.info("Request Header ==> Origin: " + origin); return "add successfully: " + name; } @DeleteMapping("/{id}") public String delete(@PathVariable("id") Long id) { return String.valueOf(id) + " deleted!"; } }上述代碼在
Controller類(lèi)上使用@CrossOrigin進(jìn)行注解配置 CORS,這樣前端頁(yè)面就可以進(jìn)行 CORS 請(qǐng)求當(dāng)前Controller下的所有接口。其中,
@CrossOrigin注解可選參數(shù)如下:方法 作用 value表示支持的域,即 Access-Control-Allow-Origin的值origins表示支持的域數(shù)組 methods表示支持的 CORS 請(qǐng)求方法,即 Access-Control-Allow-Methods的值。
其默認(rèn)值與綁定的控制器方法一致maxAge表示探測(cè)請(qǐng)求緩存時(shí)間(單位:秒),即 Access-Control-Max-Age的值。
其默認(rèn)值為1800,也即 30 分鐘allowedHeaders表示允許的請(qǐng)求頭,即 Access-Control-Allow-Headers的值
默認(rèn)情況下,支持所有請(qǐng)求頭exposedHeaders表示下發(fā)其他響應(yīng)頭字段給瀏覽器,即 Access-Control-Expose-Headers的值。
默認(rèn)不下發(fā)暴露字段allowCredentials表示是否支持瀏覽器發(fā)送認(rèn)證信息(比如 Cookie),即 Access-Control-Allow-Credentials的值。
默認(rèn)不支持接收認(rèn)證信息 -
全局配置:如果想全局配置 CORS 通信,只需添加一個(gè)配置類(lèi)。如下所示:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") //設(shè)置允許跨域的路徑 .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*") .maxAge(1800) .allowCredentials(true); } }只需創(chuàng)建一個(gè)配置類(lèi)實(shí)現(xiàn)接口
WebMvcConfigurer,然后覆寫(xiě)方法addCorsMappings即可。
addCorsMappings方法中,registry.addMapping用于設(shè)置可以進(jìn)行跨域請(qǐng)求的路徑,比如/cors/**表示路徑/cors/下的所有路由都支持 CORS 請(qǐng)求。其他的設(shè)置與注解@CrossOrigin一樣,無(wú)需介紹。注:這里也可以直接通過(guò)注入一個(gè)
WebMvcConfigurer的 Bean 實(shí)例,自定義跨域規(guī)則:@Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"); } }; } -
通過(guò)
Filter配置:通過(guò)過(guò)濾器Filter可以讓我們手動(dòng)控制響應(yīng),自然就能完成 CORS 配置。如下所示:@Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEAD"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With"); filterChain.doFilter(servletRequest, servletResponse); } }
附錄
-
CORS 前端頁(yè)面 AJAX 請(qǐng)求源碼如下所示:
<html lang="en"> <!-- ... --> <body> <button id="cors_post">CORS - POST</button> <button id="cors_delete">CORS - DELETE</button> <script> const BASE_URL = 'http://localhost:8080/cors/'; const postBtn = document.querySelector('#cors_post'); postBtn.addEventListener('click', async () => { // 簡(jiǎn)單請(qǐng)求 const response = await fetch(BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, body: 'name=Whyn', }); response.text().then((text) => console.log(text)); }); const delBtn = document.querySelector('#cors_delete'); delBtn.addEventListener('click', async () => { // 非簡(jiǎn)單請(qǐng)求 const response = await fetch(BASE_URL + '1', { method: 'DELETE', }); response.text().then((text) => console.log(text)); }); </script> </body> </html>注:前端頁(yè)面運(yùn)行在本地:
http://127.0.0.1:5500 -
Spring Security 配置跨域:如果項(xiàng)目中使用了 Spring Security 框架,那么也可以直接配置 Spring Security 支持跨域即可:
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 允許跨域資源請(qǐng)求 // by default uses a Bean by the name of corsConfigurationSource http.cors(Customizer.withDefaults()); } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList("GET","POST","OPTIONS")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 所有 url 都使用 configuration 定制的跨域規(guī)則 source.registerCorsConfiguration("/**", configuration); return source; } }