本文已参与「博主入驻有礼」活动, 一起开启掘金创作之路。
本文从web开发者角度,浅谈跨域原理,总结处理方法。
为什么会有跨域问题?
简单来说,浏览器不允许访问除当前页面所在源之外的其他源。
协议、域名、端口组成同一源(origin)
在前后端不分离的单体应用中,我们访问的前端页面和他的后端接口通常处于同一个端口下,因此可以直接访问;
大多数开发者第一次接触跨域请求应该是在学习前后端分离的B/S应用时。
即由于浏览器的安全性限制,不允许 AJAX 访问协议不同、域名不同、端口号不同的数据接口,否则会出报 No 'Access-Control-Allow-Origin' header is present on the requested resource.错误。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
同源策略控制不同源之间的交互,例如在使用XMLHttpRequest 或 <img>标签时则会受到同源策略的约束。通常允许跨源写和资源嵌入(如<img>嵌入其他源的图片),但不允许跨源读。
简单来说,浏览器会禁止当前访问的页面请求其他源的资源。我们可以认为当前源是安全的(默认当前源是用户主动要访问的),但此时由程序发起请求访问的其他源却不是用户主动进行的行为,而这可能存在风险。
不论是请求的源还是响应的源,都不欢迎跨域。
用户的cookies信息只在当前源下有用,同源策略可以阻止一个页面上的恶意脚本通过页面的DOM对象获得访问另一个页面上敏感信息的权限,如获取cookie。
如图,像cookie中存储了当前源的信息。如果当前文档随意访问其他源的资源并带回来了不好的东西(脚本之类),会有危险。
当我们访问了一个恶意网站 如果没有同源策略 那么这个网站就能通过js 访问document.cookie 得到用户关于的各个网站的sessionID ,可以对服务器发送CSRF攻击。
api跨域:
有些公共Api也没有设置跨域访问。此时有三个源:我们访问的前端项目、后端项目、公共api。可以通过代理的方式,让浏览器(一直在访问前端)一直访问代理,代理做跨域配置
虽然一直在访问nginx,但是代理服务器如果只做转发的话,http请求中的数据没有动,浏览器解析时还是会当作跨域请求(检查header?)
使用 CORS(跨资源共享)解决跨域问题
之前我们讨论了“为什么浏览器要阻止跨域”,但事实上很多应用都需要跨域来实现功能。因此现在大多浏览器都支持CORS,只需设置服务端也支持CORS即可。而支持CORS使浏览器请求某个资源的过程多了几个步骤。
原理浅析
CORS 是一个 W3C 标准,全称是"跨源(域)资源共享"(Cross-origin resource sharing)。它由一系列传输的HTTP头(CORS头,包含于HTTP头之中)组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。
- Ajax基于XMLHttpRequset对象实现的,而各种请求库又是对Ajax技术的实现、封装
- Fetch API
-
Failed to fetch version info for CodeFarmer1999/img_cloud.
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于前端开发者来说,CORS 通信与同源的 平时没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
简单来说,现在的浏览器允许发出跨院请求对于收到的response,
CORS有两种处理方式,一种是只在请求头和响应头增加CORS头的信息,另一种是发送预检请求
-
增加Header信息(示例中省去部分请求头和相应头信息)
在简单模式下的跨域请求(详情见官方文档)不需要发送预检请求,只需单纯的增加Header信息即可。
满足简单请求的3个条件:
- 请求方法为:GET/POST/HEAD 之一;
- 当请求方法为POST时,Content-Type是application/x-www-form-urlencoded,multipart/form-data或text/plain之一;
- 没有自定义请求头;
-
不使用cookie;
以下是浏览器发送给服务器的请求报文:
GET /resources/public-data/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Origin: https://foo.example请求首部字段 Origin 表明该请求来源于
http://foo.example。HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 00:23:53 GMT Server: Apache/2 Access-Control-Allow-Origin: * [XML Data]
本例中,服务端返回的 `Access-Control-Allow-Origin: *` 表明,该资源可以被 任意 外域访问。
`Access-Control-Allow-Origin` 响应头指定了该响应的资源是否被允许与给定的origin共享。
- **发送预检请求 Preflight request**
一个 CORS 预检请求是用于检查服务器是否支持 [CORS](https://developer.mozilla.org/zh-CN/docs/Glossary/CORS) 即跨域资源共享。
它一般是用了以下几个 HTTP 请求首部的 [`OPTIONS`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONS) 请求:[`Access-Control-Request-Method`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Method) 和 [`Access-Control-Request-Headers`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Headers),以及一个 [`Origin`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Origin) 首部。
当有必要的时候,浏览器会自动发出一个预检请求;所以在正常情况下,前端开发者不需要自己去发这样的请求。
举个例子,一个客户端可能会在实际发送一个 `DELETE` 请求之前,先向服务器发起一个预检请求,用于询问是否可以向服务器发起一个 [`DELETE`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/DELETE) 请求:
```http
OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org
如果服务器允许,那么服务器就会响应这个预检请求。并且其响应首部 Access-Control-Allow-Methods 会将 DELETE 包含在其中:
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400
同时满足下列以下条件,就属于简单请求,否则属于非简单请求(参考HTTP访问控制(CORS))
1.请求方式只能是:GET、POST、HEAD
2.HTTP请求头限制这几种字段(不得人为设置该集合之外的其他首部字段): Accept、Accept-Language、Content-Language、Content-Type(需要注意额外的限制)、DPR、Downlink、Save-Data、Viewport-Width、Width
3.Content-type只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain
4.请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
5.请求中没有使用 ReadableStream 对象。
预检请求的结果可能被缓存
若后端设置Access-Control-Allow-Origin:*,当前端携带Credentials发来请求时,可能会遇到withCredentials问题(前后端都可能遇到报错)(
毕竟公交车不在意别人身份证)。此时可以设置后端只允许部分域名访问(推荐,因为安全),或是设置请求头中不携带凭证。Access-Control-Allow-Credentials:
唯一有效值为true。如果不需要credentials,相比将其设为false,MDN更建议忽视这个头。当然,很多后端代码实现可以提供设置为false的方法
作为普通响应时:表示是否可以将对请求的响应暴露给页面。返回true则可以,其他值均不可以。
- 如Get请求时,浏览器不会发送预检请求。如果服务端回来的响应头中没有该项或不为true,则不会显示响应内容。
最为预检请求的响应时:表示是否真正的请求可以使用credentials。
- 如果不允许对资源的请求头携带凭证(如Access-Control-Allow-Origin:*默认不允许),假如请求头真的带了凭证,反而会报错。就好像问公交车需不需要提供身份证,人家会觉得你太正经。
- Credentials可以是 cookies, authorization headers (token)或 TLS client certificates。
下面展示开发过程中如何解决跨域问题。注意不论是通过代码、配置文件进行配置,大多都是基于CORS标准诞生的解决方法。
采用一种方法解决问题即可!我们的目的是在响应头中添加允许跨域的信息!
利用代理服务器配置
IIS配置:
只需要在IIS添加HTTP响应标头即可
Access-Control-Allow-Headers:Content-Type, api_key, Authorization
Access-Control-Allow-Origin:*
Apache配置:修改http.conf
<Directory "/Users/cindy/dev">
AllowOverride ALL
Header set Access-Control-Allow-Origin *
</Directory>
或者,修改Apache伪静态规则文件.htaccess
Nginx(推荐使用)
修改nginx.conf,用add_header来为响应头添加信息
location ~* .(eot|ttf|woff|svg|otf)$ {
add_header Access-Control-Allow-Origin *;
}
- 为什么nginx确定是添加到响应了啊喂
前端开发时代理服务器
vue代理
proxyTable
dev{
proxyTable: {
'/api': {
target: 'http://192.168.0.1:200', // 要代理的域名
changeOrigin: true,//允许跨域
pathRewrite: {
'^/api': '' // 这个是定义要访问的路径,名字随便写
}
}
}
配置好之后由http-proxy-middleware中间件做跨域
springboot
注解
直接在需要跨域访问的类或方法上配置@CrossOrigin注解即可。
扩展:@CrossOrigin 注解还支持更加丰富的参数配置:
value:表示支持的域。这里表示来自 http://localhost:8081 域的请求是支持跨域的。默认为 *,表示所有域都可以。
maxAge:表示探测请求的有效期(先进行判断是否有效)。探测请求不用每次都发送,可以配置一个有效期,有效期过了之后才会发送探测请求。默认为 1800 秒,即 30 分钟。
allowedHeaders:表示允许的请求头。默认为 *,表示该域中的所有的请求都被允许。
@CrossOrigin(value = "http://localhost:8081", maxAge = 1800, allowedHeaders ="*")
全局配置:
创建webMvc配置类
全局配置需要添加自定义类继承WebMvcConfigurer类,然后实现接口中的 addCorsMappings 方法。下面是一个简单的样例代码,请根据需要配置:
从上往下依次为:
addMapping: 表示对哪种格式的请求路径进行处理。
allowedHeaders: 表示允许的请求头,默认允许所有的请求头信息。也可以在这里配置请求方法。
allowedMethods: 表示允许的请求方法,默认是 GET、POST 和 HEAD。这里配置为 * 表示支持所有的请求方法。
maxAge: 最大响应时间
allowedOrigins: 该域发出的请求允许跨域
.allowCredentials(true): 允许携带token
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/controller/**")
.allowedHeaders("*")
.allowedMethods("*")
.maxAge(1800)
.allowedOrigins("http://localhost:8081")
.allowCredentials(true);
}
}
配置专门的跨域配置类
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 解决跨越配置类
*/
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
/**
* 跨域过滤器
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
过滤器
import javax.servlet.*;
@Component
public class CorsFilter implements Filter {
// 只有在列表中的才允许访问
private final List<String> allowedOrigins = Arrays.asList("http://localhost:8089");
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
// Lets make sure that we are working with HTTP (that is, against HttpServletRequest and HttpServletResponse objects)
if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// Access-Control-Allow-Origin
String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : "*");
response.setHeader("Vary", "Origin");
// Access-Control-Max-Age
response.setHeader("Access-Control-Max-Age", "3600");
// Access-Control-Allow-Credentials
response.setHeader("Access-Control-Allow-Credentials", "true");
// Access-Control-Allow-Methods
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
// Access-Control-Allow-Headers
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, " + "X-CSRF-TOKEN");
}
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {
}
}
Spring Cloud Gateway网关配置
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOriginPatterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
exposedHeaders: "Content-Disposition,Content-Type,Cache-Control"