首页 > 技术文章 > SpringBoot处理跨域的四种方式

sueyyyy 2018-12-17 09:27 原文

一、介绍

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.java

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");

 

参考:什么是跨域?跨域解决方法

推荐阅读