SpringBoot處理跨域的四種方式


一、介紹

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】無法讀取非同源網頁的 CookieLocalStorageIndexedDB

【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;
    }
}
HelloClass.java

前端頁面 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>
index.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");

 

參考:什么是跨域?跨域解決方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM