Spring boot前後端分離後,跨域問題怎麼解決?

現在基於spring boot前後端分離的開發模式越來越普遍,那麼,由於前後端分離引發的跨域問題,你知道怎麼解決嗎?

什麼是跨域

跨域是指 不同域名之間相互訪問。即瀏覽器控制當前網頁下不能執行其他網站的腳本,這是由瀏覽器的同源策略造成的,是瀏覽器對JavaScript施加的安全限制。
也就是如果在A網站中,我們希望使用Ajax來獲得B網站中的特定內容
如果A網站與B網站不在同一個域中,那麼就出現了跨域訪問問題。

跨域的安全限制都是對瀏覽器端來說的,服務器端是不存在跨域安全限制的。

同源策略
同源策略/SOP(Same origin policy)是一種約定,由Netscape公司1995年引入瀏覽器,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,瀏覽器很容易受到XSS、CSFR等攻擊。所謂同源是指"協議+域名+端口"三者相同,即便兩個不同的域名指向同一個ip地址,也非同源。
前端發起的請求只要不符合同源策略就會出現跨域問題。

案例分析

URL 說明 是否允許通信
http://www.a.com/a.js http://www.a.com/b.js 同一域名下 允許
http://www.a.com/lab/a.js http://www.a.com/script/b.js 同一域名下不同文件夾 允許
http://www.a.com:8000/a.js http://www.a.com/b.js 同一域名,不同端口 不允許
http://www.a.com/a.js https://www.a.com/b.js 同一域名,不同協議 不允許
http://www.a.com/a.js http://70.32.92.74/b.js 域名和域名對應ip 不允許
http://www.a.com/a.js http://script.a.com/b.js 主域相同,子域不同 不允許
http://www.a.com/a.js ;http://a.com/b.js 同一域名,不同二級域名(同上) 不允許(cookie這種情況下也不允許訪問)
http://www.cnblogs.com/a.js;http://www.a.com/b.js 不同域名 不允許

爲什麼前後端分離後會導致跨域問題?

前後端分離後,前端代碼和後端代碼都是獨立部署的,一般前端採用Nginx作爲web服務器部署,後端spring boot由於內置了tomcat,一般都是通過jar包直接啓動。
假設前後端部署在同一臺服務器上,那麼2者訪問的端口必定不一致,不符合同源策略,所以出現跨域問題。
如果前後端部署在不同服務器上,那麼訪問的ip或者域名必然不一致,也會出現跨域問題。

通過jsonp實現跨域

jsonp 全稱是JSON with Padding,是爲了解決跨域請求資源而產生的解決方案,是一種依靠開發人員創造出的一種非官方跨域數據交互協議。
一個是描述信息的格式,一個是信息傳遞雙方約定的方法。

jsonp的產生:

1.AJAX直接請求普通文件存在跨域無權限訪問的問題,不管是靜態頁面也好.

2.不過我們在調用js文件的時候又不受跨域影響,比如引入jquery框架的,或者是調用相片的時候

3.如果想通過純web端跨域訪問數據只有一種可能,那就是把遠程服務器上的數據裝進js格式的文件裏.

4.而json又是一個輕量級的數據格式,還被js原生支持

5.爲了便於客戶端使用數據,逐漸形成了一種非正式傳輸協議,人們把它稱作JSONP,該協議的一個要點就是允許用戶傳遞一個callback 參數給服務端,

前端json請求示例:

<!DOCTYPE html>
<html>
<head>
<title>測試跨域訪問</title>
<meta charset="utf-8" />
</head>
<body>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script type="text/javascript">
    	$(document).ready(function() {
        	$.ajax({
        		type : "get",
        		async : true,
        		jsonp : "callbackName",// 後端接口參數名
        		jsonpCallback : "callbackFunction", // 回調函數名
        		url : "http://A/hello/map/getUser.json",
        		dataType : "jsonp", // 數據格式爲 jsonp
        		success : function(data) {
        			console.log("success");
        		}
        	});
    	});
    </script>
    <script type="text/javascript">
    	var callbackFunction = function(data) {
    		alert('接口返回的數據是:' + JSON.stringify(data));
    	};
    </script>
</body>
</html>

後端jsonp代碼參考:

/**
 * 
 * The class JsonBackController.
 *
 * Description:該控制器返回一串簡單的json數據,json數據由一個簡單的User對象組成
 *
 * @author: huangjiawei
 * @since: 2018年6月12日
 * @version: $Revision$ $Date$ $LastChangedBy$
 *
 */
@RestController
@RequestMapping(value = "/map")
public class JsonBackController {
    private static final Logger logger = LoggerFactory.getLogger(JsonBackController.class);
    /**
     * 解決跨域請求數據
     * @param response
     * @param callbackName 前端回調函數名
     * @return
     */
    @RequestMapping(value = "getUser.json")
    public void getUser(HttpServletResponse response, @RequestParam String callbackName) {
        User user = new User("huangjiawei", 22);
        response.setContentType("text/javascript");
        Writer writer = null;
        try {
        	writer = response.getWriter();
        	writer.write(callbackName + "(");
        	writer.write(user.toString());
        	writer.write(");");
        } catch (IOException e) {
        	logger.error("jsonp響應寫入失敗! 數據:" + user.toString(), e);
        } finally {
        	if (writer != null) {
        		try {
        			writer.close();
        		} catch (IOException e) {
        			logger.error("輸出流關閉異常!", e);
        		}
        		writer = null;
        	}
        }
    }
}

由於JSONP的原理其實就是欺騙瀏覽器,以方式調用接口,解析數據。所以,jsonp的請求方式只能是get方式,如果接口只接受post請求方式,則再做考慮。
這種方式代碼侵入強,請求方式也有限制,現在基本不再使用。

Spring Boot對JSONP的支持
4.1版本以後的SpringMVC中,爲我們提供了一個AbstractJsonpResponseBodyAdvice的類用來支持jsonp的數據(SpringBoot接收解析web請求是依賴於SpringMVC實現的)
使用AbstractJsonpResponseBodyAdvice來支持跨域請求很簡單,只需要繼承這個類就可以了。具體代碼如下:

全局jsonp請求配置:

package com.laowan.jsonp.config;
 
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;
 
/**
 * Created by wb-zhangkenan on 2016/12/1.
 */
@ControllerAdvice(basePackages = "com.laowan.jsonp.controller")
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice{
    public JsonpAdvice() {
        super("callback","jsonp");
    }
}

測試接口:

package com.laowan.jsonp.controller;
 
import com.laowan.jsonp.domain.PersonDomain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
/**
 * Created by wb-zhangkenan on 2016/12/1.
 */
@RestController
@RequestMapping("/jsonp")
public class JsonpTestController {
    @Autowired
    private PersonDomain personDomain;
 
    @RequestMapping(value = "/testJsonp",produces = MediaType.APPLICATION_JSON_VALUE)
    public PersonDomain testJsonp(){
        return personDomain;
    }
}

當發送的請求爲:http://localhost:8003/jsonp/testJsonp?callback=callback的時候,返回的數據就是jsonp的;
當我們請求參數中不帶callback的時候:http://localhost:8003/jsonp/testJsonp,返回的數據是json的。

利用Nginx解決跨域

通過反向代理服務器監聽同端口,同域名的訪問,不同路徑映射到不同的地址,比如,在nginx服務器中,監聽同一個域名和端口,不同路徑轉發到客戶端和服務器,把不同端口和域名的限制通過反向代理,來解決跨域的問題。

通過Nginx反向代理,將跨域請求轉變爲非跨域請求,不同請求路徑代理到不同的地址:

server {
        listen       80;
        server_name  abc.com;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;

        location /client { #訪問客戶端路徑
            proxy_pass http://localhost:81;
            proxy_redirect default;
        }
        location /apis { #訪問服務器路徑
            rewrite  ^/apis/(.*)$ /$1 break;
            proxy_pass   http://localhost:82;
       }
}

通過Nginx在請求頭中添加CORS參數解決跨域

location / {
   add_header Access-Control-Allow-Origin *;
   add_header Access-Control-Allow-Headers X-Requested-With;
   add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;

   if ($request_method = 'OPTIONS') {
     return 204;
   }
}

利用CORS解決跨域問題

CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。

它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低於IE10

整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對於開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。

因此,實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨源通信。

CORS與JSONP的使用目的相同,但是比JSONP更強大。
JSONP只支持GET請求,CORS支持所有類型的HTTP請求。JSONP的優勢在於支持老式瀏覽器,以及可以向不支持CORS的網站請求數據。

CORS核心參數介紹。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods
該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是爲了避免多次"預檢"請求。

(2)Access-Control-Allow-Headers
如果瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在"預檢"中請求的字段。

(3)Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。設爲true,即表示服務器明確許可,Cookie可以包含在請求中,一起發給服務器。這個值也只能設爲true,如果服務器不要瀏覽器發送Cookie,刪除該字段即可。

(4)Access-Control-Max-Age
該字段可選,用來指定本次預檢請求的有效期,單位爲秒。上面結果中,有效期是20天(1728000秒),即允許緩存該條迴應1728000秒(即20天),在此期間,不用發出另一條預檢請求。

更多原理可以參看: 阮一峯 寫的跨域資源共享 CORS 詳解
http://www.ruanyifeng.com/blog/2016/04/cors.html

spring boot中使用CORS

方式一:在需要指出跨域的Controller類或方法上添加@CrossOrigin註解

@CrossOrigin // 註解方式  
@RestController  
public class HandlerScanController {  
    @CrossOrigin(allowCredentials="true", allowedHeaders="*", methods={RequestMethod.GET,  
            RequestMethod.POST, RequestMethod.DELETE, RequestMethod.OPTIONS,  
            RequestMethod.HEAD, RequestMethod.PUT, RequestMethod.PATCH}, origins="*")  
    @PostMapping("/confirm")  
    public Response handler(@RequestBody Request json){  
        return null;  
    }  
}  

方式二:配置CorsFilter,可以全局生效 (推薦)

/**
 * 實現基本的跨域請求
 * @author linhongcun
 */
@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 允許任何域名使用
        corsConfiguration.addAllowedHeader("*"); // 允許任何頭
        corsConfiguration.addAllowedMethod("*"); // 允許任何方法(post、get等)
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig()); // 對接口配置跨域設置
        return new CorsFilter(source);
    }
}

當然,如果微服務多的話,需要在每個服務的主類上都加上這麼段代碼,這違反了DRY原則,更好的做法是在zuul的網關層解決跨域問題,一勞永逸。

方式三:通過自定義WebMvcConfigurer重寫addCorsMappings(CorsRegistry)實現全局跨域控制
這也是官網推薦的寫法

@Configuration  
    public class MyConfiguration {  
        @Bean  
        public WebMvcConfigurer corsConfigurer() {  
            return new WebMvcConfigurerAdapter() {  
                @Override  
                public void addCorsMappings(CorsRegistry registry) {  
                    registry.addMapping("/**");
                }  
            };  
        }  
    } 

總結

1、什麼是跨域?爲什麼會產生跨域問題?
2、爲什麼前後端分離後容易產生跨域問題?
3、解決跨域有哪些方式?

參考:
https://blog.csdn.net/zknxx/article/details/53443181
https://blog.csdn.net/qq_43486273/article/details/83272500
http://www.ruanyifeng.com/blog/2016/04/cors.html

覺得有用,記得點贊加關注。
跟着老萬學java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章