從零搭建開發(fā)腳手架 跨域請求原理、同源協(xié)議以及常見跨域請求解決方案
“現(xiàn)在的開發(fā)架構大部分都是前后端分離架構,跨域請求也變成了常見的高頻問題,這里分析下跨域請求原理,總結下常見的幾種跨域解決方案。
什么是跨域、同源協(xié)議
說道跨域,先看下瀏覽器的同源協(xié)議(Same-Origin-Policy),它是瀏覽器的默認安全措施,一般一個網址通常由 protocol,domain,port 三個部分所組成,根據(jù)SOP同源協(xié)議,如果一個網址只要至少一個部分的不符合,便不能進入到先前進入的非同源地址,這種行為就是跨域。
非同源限制
無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB
無法接觸非同源網頁的 DOM
無法向非同源地址發(fā)送 AJAX 請求
“
如果沒有瀏覽器的同源協(xié)議,那么web安全就亂套了,你可以隨意攻擊別人的網站。 另外這個同源協(xié)議是防止跨站請求偽造(CSRF)攻擊,具體可參考跨站請求偽造(CSRF)示例、原理及其防御措施
跨域請求是HTTP請求,其中請求的源和目標是不同的。例如,從瀏覽器訪問一個域(前端服務:https://www.site.com)其提供Web應用程序頁面并且瀏覽器向另一個域(后端接口服務:https://www.api.com)中的服務器發(fā)送AJAX請求數(shù)據(jù)。
下面是常見的跨域錯誤截圖,從我們的網站http://localhost:3001向http://www.google.com發(fā)出GET請求,Chrome瀏覽器發(fā)生的錯誤如下:
跨域場景總結
幾種跨域場景總結如下:
“只要協(xié)議、域名、端口有任何一個不同,就是跨域。
對https://www.baidu.com/index.html進行跨域比較:
| URL | 是否跨域 | 原因 |
|---|---|---|
| https://www.baidu.com/more/index.html | 不跨域 | 三要素相同 |
| https://map.baidu.com/ | 跨域 | 域名不同 |
| http://www.baidu.com/index.html | 跨域 | 協(xié)議不同 |
| https://www.baidu.com:81/index.html | 跨域 | 端口號不同 |
什么是跨域資源共享(CORS)
“cors相關內容來自:https://www.baeldung.com/cs/cors-preflight-requests
CORS(Cross-origin resource sharing)是為了滿足訪問第三方API的需求,CORS策略確定了一個來源提供的腳本如何請求另一個來源上的資源,是W3C標準,是一種機制。
CORS定義了需要包含在請求/響應交互中的特定HTTP標頭,允許服務器傳達允許來自哪個來源的請求。然后,瀏覽器通過允許或阻止腳本訪問響應來強制執(zhí)行此操作。
“簡單來說就是為了解決跨域請求而制定的一種機制、標準。
當涉及跨域請求時,瀏覽器可以處理三種類型:
簡單請求
符合簡單請求的條件:
請求類型:GET、POST或者HEAD 請求頭:僅發(fā)送自動用戶代理標頭或CORS安全列出的頭,例如Accept,Accept-Language,Content-Language,Content-Type Content-Type:只限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
舉例:在瀏覽器訪問前端https://www.site.com和后端接口服務https://www.api.com
首先,瀏覽器將帶有標識原始來源的 origin標頭的請求(origin: https://www.site.com)發(fā)送到https://www.api.com服務器服務器以請求的數(shù)據(jù)作為響應,并且還包含一個 設置為 https://www.site.com的access-control-allow-origin頭,該響應頭向瀏覽器指示服務器允許該來源的請求

access-control-allow-origin標頭是主要的CORS頭,一臺服務器可以用它來顯示什么允許的域。此標頭的值可以是一個單一的來源,以告訴瀏覽器允許訪問該特定來源,也可以是*,它指示瀏覽器允許任何來源。
access-control-allow-origin是CORS的重要響應頭,如果服務器不響應此標頭,或者標頭值是與請求的來源不匹配的域(origin != access-control-allow-origin),瀏覽器將阻止將響應傳遞回腳本。這可能會導致控制臺發(fā)生如下錯誤。

非簡單請求
任何不是簡單請求的請求都將被視為非簡單請求或預檢請求。瀏覽器對這類請求的處理略有不同。在發(fā)送實際請求之前,瀏覽器將發(fā)送我們稱為預檢請求的內容,以與服務器進行檢查,以確認是否允許這種類型的請求。預檢請求是一個OPTIONS請求,其中包括以下標頭:
origin –告訴服務器請求的來源 access-control-request-method –告訴服務器請求包含哪種HTTP方法 access-control-request-headers –告訴服務器請求包含哪些頭
服務器可以通過響應以下標頭來決定是否接受來自此來源的此類請求:
access-control-allow-origin –服務器允許的來源 access-control-allow-methods –服務器允許的方法的逗號分隔列表 access-control-allow-headers -服務器將允許的逗號分隔的頭列表 access-control-max-age –告訴瀏覽器將對預檢請求的響應緩存多長時間(以秒為單位) MDN Web文檔中列出了可能的CORS響應標頭的完整列表。
與簡單請求類似,如果服務器不包含任何CORS標頭,則瀏覽器將假定該服務器不允許此請求,并且不會繼續(xù)實際請求。
舉例:添加一個自定義標頭custom-header,變?yōu)榉呛唵握埱蟆?/p>
瀏覽器會將該請求標識為非簡單請求,并將向服務器發(fā)起預檢請求,以檢查其是否允許該請求。讓我們看一下如果https://www.api.com服務器允許這種請求,則這種交互的流程:

服務器以正確的頭響應,瀏覽器繼續(xù)發(fā)出實際請求。如果服務器響應時沒有正確的標頭,則瀏覽器將阻止該請求發(fā)出。
在這里,我們有一個瀏覽器示例,其中我試圖通過一個包含自定義標頭的非簡單請求來訪問Google Book API。我們可以在瀏覽器控制臺中看到一個略有不同的錯誤,因為API并未使用所需的標頭來響應預檢請求:

憑證請求
憑據(jù)可以是cookie,授權標頭或TLS客戶端證書。默認情況下,除非兩個請求都包括一個包含憑據(jù)的標記,并且服務器以將access-control-allow-credentials設置為true進行響應,否則CORS策略不允許在跨域請求中包含憑據(jù)。
要將憑證包含在我們的請求中,讓我們通過將withCredentials屬性設置為true來更新XMLHttpRequest:
const xhr = new XMLHttpRequest();
const url = 'https://www.api.com?q=test';
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();
如果服務器響應中不包含設置為true的access-control-allow-credentials和與請求來源相同的access-control-allow-origin頭,瀏覽器將阻止我們的請求。下面顯示了一個示例,在該示例中,我們嘗試向不允許憑據(jù)的Google Book API發(fā)出相同的請求:

如何解決跨域請求
一、反向代理
例如使用Nginx反向代理。
前端:https://www.site.com
后端接口服務:https://www.api.com
nginx配置示例:
server {
location /api/{
proxy_pass https://www.api.com/;
}
...
反向代理前
瀏覽器訪問的時序:
1.https://www.site.com2.https://www.api.com/getinfo
反向代理后
瀏覽器訪問的時序:
1.https://www.site.com2.https://www.site.com/api/getinfo
“即把不同的域,通過反向代理變?yōu)橄嗤挠颉?/p>
二、跨域資源共享(CORS)
看了前面的一堆介紹,只要我們的服務器端設置好CORS相關的相應頭即可實現(xiàn)跨域
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT, OPTIONS");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
“可以在每個請求的相應頭去這樣設置,但是這種做法有點蠢,所以,一般都是使用filter來做這件事。
以下幾種方式原理都是依托上面的原理實現(xiàn),在實際開發(fā)中,按照需求任選其一即可。
1、CorsFilter
1.自己寫過濾器在response中寫入這些響應頭
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "CorsFilter", urlPatterns = "/*")
public class CorsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT, OPTIONS");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
2.使用SpringMvc的CorsFilter
其實SpringMvc中已經有CorsFilter了,可以直接拿來用。
org.springframework.web.filter.CorsFilter
//@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}
3.使用tomcat的CorsFilter
tomcat中也已經有實現(xiàn)好了的過濾器。
org.apache.catalina.filters.CorsFilter
@Configuration
public class TomcatCorsFilterConfig {
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CorsFilter());
//Defaults: false
registration.addInitParameter(CorsFilter.PARAM_CORS_SUPPORT_CREDENTIALS, "false");
//這個默認是"",不允許訪問的,可直接設置成 *
//Defaults: The empty String. (No origin is allowed to access the resource).
registration.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_ORIGINS, "*");
//Defaults: GET, POST, HEAD, OPTIONS 我測試的tomcat9.0.41 不支持 * 寫法
registration.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_METHODS, "GET, POST, HEAD, OPTIONS");
//Defaults: Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers
registration.addInitParameter(CorsFilter.PARAM_CORS_ALLOWED_HEADERS, "*");
//Defaults: 1800。3600表示一個小時
registration.addInitParameter(CorsFilter.PARAM_CORS_PREFLIGHT_MAXAGE, "3600");
registration.setName("TomcatCorsFilter"); //過濾器名稱
registration.addUrlPatterns("/*");//過濾路徑
registration.setOrder(1);//設置順序
return registration;
}
}
“注意過濾器的順序。
2 、@CrossOrigin注解
@RestController
@CrossOrigin
public class ResourceController {
@GetMapping("/user")
@CrossOrigin
public String user(Principal principal) {
return principal.getName();
}
}
“可以精確控制到某個類或者某個方法跨域。
3、Spring Boot 全局配置CorsRegistry
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
三、Jsonp
JSONP 是服務器與客戶端跨源通信的常用方法之一。最大特點就是簡單適用,兼容性好(兼容低版本IE),缺點是只支持get請求,所以一般我們也不用它。
核心思想:網頁通過添加一個<script>元素,向服務器請求 JSON 數(shù)據(jù),服務器收到請求后,將數(shù)據(jù)放在一個指定名字的回調函數(shù)的參數(shù)位置傳回來。
前端實現(xiàn):
<script src="http://localhost:8080/map?callback=dosomething"></script>
// 向服務器發(fā)出請求,該請求的查詢字符串有一個callback參數(shù),用來指定回調函數(shù)的名字
// 處理服務器返回回調函數(shù)的數(shù)據(jù)
<script type="text/javascript">
function dosomething(res){
console.log(res)// 處理獲得的數(shù)據(jù)
}
</script>
后端實現(xiàn):
@RestController
@Slf4j
public class MapController {
@RequestMapping(value = "/map")
public String transfer(String callback) {
log.info(callback);
return callback + "('lakertest')";
}
}
總結
一般項目開發(fā)針對跨域請求的解決方案選擇Nginx反向代理或者CORSFilter+域的黑白名單方式。
以上相關內容我都做了代碼驗證,代碼位置:https://gitee.com/lakernote/lakernote
備注:還有一些其他前端解決跨域的方式,可自行查詢。
參考:
https://www.baeldung.com/cs/cors-preflight-requests
https://blog.csdn.net/qq_38128179/article/details/84956552
我已經更新了《10萬字Springboot經典學習筆記》,點擊下面小卡片,進入【武哥聊編程】,回復:筆記,即可免費獲取。
點贊是最大的支持

