一、介绍
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.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; } }
前端页面 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=" 220px;" id="urlText" value="http://99.48.59.195:8082/testGet" /> <input type="button" id="cors" value="testGet" /><br /> <input type="text" style=" 220px;" id="urlText1" value="http://99.48.59.195:8082/testGet2" /> <input type="button" id="cors1" value="testGet2" /><br /> <input type="text" style=" 220px;" id="urlText2" value="http://99.48.59.195:8082/testPost" /> <input type="button" id="cors2" value="testPost" /><br /> <input type="text" style=" 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");
参考:什么是跨域?跨域解决方法