一、介紹
1.1 為什么會出現跨域?
出於瀏覽器的同源策略限制。同源策略(Sameoriginpolicy)是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能可能都會受到影響。可以說 Web 是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現。同源策略會阻止一個域的javascript腳本和另外一個域的內容進行交互。所謂同源(即指在同一個域)就是兩個頁面具有相同的協議(protocol),主機(host)和端口號(port)
1.2 什么是跨域?
當一個請求 url 的協議、域名、端口三者之間任意一個與當前頁面 url 不同即為跨域
請求頁面url | 當前頁面url | 是否跨域 | 原因 |
http://www.test.com/ | http://www.test.com/index.html | 否 | 同源(協議、域名、端口號相同) |
http://www.test.com/ | https://www.test.com/index.html | 跨域 | 協議不同(http/https) |
http://www.test.com/ | http://www.baidu.com/ | 跨域 | 主域名不同(test/baidu) |
http://www.test.com/ | http://blog.test.com/ | 跨域 | 子域名不同(www/blog) |
http://www.test.com:8080/ | http://www.test.com:7001/ | 跨域 | 端口號不同(8080/7001) |
1.3 非同源限制
【1】無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB
【2】無法接觸非同源網頁的 DOM
【3】無法向非同源地址發送 AJAX 請求
二、案例
假設我們是前后段分離的項目,分別部署在以下兩個ip上
前端頁面的地址為 http://127.0.0.1:8848/test/index.html
后台服務的地址為 http://99.48.59.195:8082/
前后端的主要代碼如下所示:
后端接口 HelloController.class

import com.example.security.entity.User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController public class HelloController { @GetMapping("/testGet") public String testGet(String username) { return username; } @GetMapping("/testGet2") public String testGet2(String username, String password) { return username + "," + password; } @PostMapping("/testPost") public Map testPost(@RequestBody Map<String, Object> map) { return map; } @PostMapping("/testPost2") public User testPost2(User user) { return user; } }
前端頁面 index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <link type="test/css" href="css/style.css" rel="stylesheet"> <body> <input type="text" style="width: 220px;" id="urlText" value="http://99.48.59.195:8082/testGet" /> <input type="button" id="cors" value="testGet" /><br /> <input type="text" style="width: 220px;" id="urlText1" value="http://99.48.59.195:8082/testGet2" /> <input type="button" id="cors1" value="testGet2" /><br /> <input type="text" style="width: 220px;" id="urlText2" value="http://99.48.59.195:8082/testPost" /> <input type="button" id="cors2" value="testPost" /><br /> <input type="text" style="width: 220px;" id="urlText3" value="http://99.48.59.195:8082/testPost2" /> <input type="button" id="cors3" value="testPost2" /> <script type="text/javascript" src="jquery-3.4.1.min.js"></script> <script type="text/javascript"> $(function() { $("#cors").click( function() { var url2 = $("#urlText").val(); $.get({ url: url2, data: "username=jack", success: function(data) { alert("username is " + data); } }) }); $("#cors1").click( function() { var url2 = $("#urlText1").val(); $.get(url2, { username: "John", password: "2pm" }, function(data) { alert("Data Loaded: " + data); }); }); $("#cors2").click( function() { var url2 = $("#urlText2").val(); $.post({ dataType: 'application/json', contentType: 'application/json', url: url2, data: JSON.stringify({ username: "John", password: "2pm" }), // 指定dataType為json時可能不能執行success回調,可參考https://blog.csdn.net/zls986992484/article/details/51404429 success: function(data) { console.log(11); alert("success"); } }) }); // 這種方式參數為formDate格式 $('#cors3').click(function() { var url2 = $("#urlText3").val(); $.post( url2, { username: 'admin', password: '123' }, function(result) { alert("success"); }, "json" ); }); }); </script> </body> </html>
直接調用接口時,根據瀏覽器的同源策略可以知道如果我們此時不進行跨域處理的話,訪問后端地址是會失敗的,控制台會打印如下錯誤信息
三、解決方案
3.1 實現WebMvcConfigurer,重寫跨域處理方法
添加 CORS 的配置信息,我們創建一個 CORSConfiguration 配置類重寫如下方法,如下所示:
WebMvcConfigurer.java
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 這里我們的CORSConfiguration配置類繼承了WebMvcConfigurer父類並且重寫了addCorsMappings方法,我們來簡單介紹下我們的配置信息 * allowedOrigins:允許設置的請求域名訪問我們的跨域資源,可以固定單條或者多條內容,如:"http://www.baidu.com",只有百度可以訪問我們的跨域資源。 * addMapping:配置可以被跨域的路徑,可以任意配置,可以具體到直接請求路徑。 * allowedMethods:設置允許的請求方法類型訪問該跨域資源服務器,如:POST、GET、PUT、OPTIONS、DELETE等。 * allowedHeaders:允許所有的請求header訪問,可以自定義設置任意請求頭信息,如:"X-YYYY-TOKEN" * allowCredentials: 是否允許請求帶有驗證信息,用戶是否可以發送、處理 cookie */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**")//項目中的所有接口都支持跨域 .allowedOrigins("*")//所有地址都可以訪問,也可以配置具體地址 .allowCredentials(true) //是否允許請求帶有驗證信息 .allowedMethods("*")//"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" .allowedHeaders("*").maxAge(3600);// 跨域允許時間 } }
3.2 使用過濾器
方案一:
配置如下過濾器
CorsFilter.java
import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Configuration public class CorsFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; // 這里填寫你允許進行跨域的主機ip,*表示所有(正式上線時可以動態配置具體允許的域名和IP) // response.setHeader("Access-Control-Allow-Origin", "*"); HttpServletRequest request = (HttpServletRequest) servletRequest; //獲取來源網站 String originStr = request.getHeader("Origin"); //允許該網站進行跨域請求 response.setHeader("Access-Control-Allow-Origin", originStr); // 允許的訪問方法 response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE"); // Access-Control-Max-Age 用於 CORS 相關配置的緩存 response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, client_id, uuid, Authorization"); response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); //表示是否允許請求攜帶憑證信息,若要返回cookie、攜帶seesion等信息則將此項設置為true response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Pragma", "no-cache"); filterChain.doFilter(servletRequest, response); } @Override public void destroy() { } }
方案二:
利用過濾器配置跨域還可以使用如下方法
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class CorsFilter { @Bean public FilterRegistrationBean<CorsFilter> corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); //表示允許所有,可以設置需要的地址 config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); //表示是否允許請求帶有驗證信息 config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); //CORS配置對所有接口都有效 source.registerCorsConfiguration("/**", config); FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source)); bean.setOrder(0); return bean; } }
3.3 使用 @CrossOrigin 注解
import com.example.security.entity.User; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.util.Map; /** * 代碼說明: * @CrossOrigin這個注解可以用在方法上,也可以用在類上,用在類上時,表示該controller所有映射都支持跨域請求。 * 如果不設置他的value屬性,或者是origins屬性,就默認是可以允許所有的URL/域訪問。 * value屬性可以設置多個URL。 * origins屬性也可以設置多個URL。 * maxAge屬性指定了准備響應前的緩存持續的最大時間。就是探測請求的有效期。 * allowCredentials屬性表示用戶是否可以發送、處理 cookie。默認為false * allowedHeaders 屬性表示允許的請求頭部有哪些。 * methods 屬性表示允許請求的方法,默認get,post,head。 */ //直接在Controller類上面添加/@CrossOrigin注解。表示該controller所有映射都支持跨域請求。 //@CrossOrigin(origins = "http://127.0.0.1:8848", maxAge = 3600) @CrossOrigin @RestController public class HelloController { @GetMapping("/testGet") public String testGet(String username) { return username; } @GetMapping("/testGet2") public String testGet2(String username, String password) { return username + "," + password; } @PostMapping("/testPost") public Map testPost(@RequestBody Map<String, Object> map) { return map; } @PostMapping("/testPost2") public User testPost2(User user) { return user; } }
3.4 nginx 轉發請求處理跨域
前面我們介紹過跨域產生的幾種情況,只要保證同源(協議、域名、端口號相同),就不會出現跨域問題。
我們現在前端頁面服務器所在IP為 http://127.0.0.1:8848
需要調用的后台服務的地址為 http://99.48.59.195:8082/test/**
那么我們可以在前端服務器的 nginx 配置文件中添加如下代理:
server {
listen 8084;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/local/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /test/ {
proxy_pass http://99.48.59.195:8082/test/;
proxy_read_timeout 150;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
這段配置表示的當前端服務器調用 8084 端口的請求時,會自動將請求轉發到 http://99.47.134.33:8090/ 。對於前端請求來說此時的協議、域名、端口號都是相同的,那么就不會出現跨域問題。
三、測試
點擊按鈕調用接口,成功返回數據,說明我們這里成功進行了跨域處理。
注意:
1.如果項目帶有登錄功能,需要驗證登錄憑證cookie時,此時需要在跨域配置中設置 Access-Control-Allow-Credentials 屬性:
//表示是否允許請求攜帶憑證,若要返回cookie、攜帶seesion等信息則將此項設置為true response.setHeader("Access-Control-Allow-Credentials", "true");
否則會出現如下錯誤信息,這句話明確表明了此時要將 Access-Control-Allow-Credentials 頭設置為 true
The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'
2.在使用過濾器方案一處理跨域時,如果使用了如下配置:
// 這里填寫你允許進行跨域的主機ip,*表示所有(正式上線時可以動態配置具體允許的域名和IP) response.setHeader("Access-Control-Allow-Origin", "*"); //表示是否允許請求攜帶憑證信息,若要返回cookie、攜帶seesion等信息則將此項設置為true response.setHeader("Access-Control-Allow-Credentials", "true");
這里表示請求需要攜帶憑證信息,允許所有 ip 進行跨域。理論上是沒有問題的,但是在測試的時候會發現控制台會拋出如下錯誤信息:
錯誤表明當請求的憑據模式為 “include” 時,響應中的標頭不可以使用通配符 “*”。需要指定域名,這時我們可以對跨域配置作如下修改:
HttpServletRequest request = (HttpServletRequest) servletRequest; //獲取來源網站 String originStr = request.getHeader("Origin"); //允許該網站進行跨域請求 response.setHeader("Access-Control-Allow-Origin", originStr); //表示是否允許請求攜帶憑證信息,若要返回cookie、攜帶seesion等信息則將此項設置為true response.setHeader("Access-Control-Allow-Credentials", "true");
參考:什么是跨域?跨域解決方法