一、介紹
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");
參考:什么是跨域?跨域解決方法
