URL结构

URL 代表着是统一资源定位符(Uniform Resource Locator)。URL 无非就是一个给定的独特资源在 Web 上的地址。

URL有如下结构组成:

  • Schme 或者 Protocol : http http
  • Domain Name 也叫做host域名
  • port 端口号
  • Parameters参数
  • Anchor 锚点,一般用于定位位置 如#1

什么是跨域

浏览器因为安全考虑,所以设置了同源策略。同源策略简单理解就是DNS域名,端口号,协议完全相同就称为同源。同源下的页面之间才能进行js的dom操作,同源例子

例子 原因
http://example.com/app1/index.html http://example.com/app2/index.html 相同的 scheme http 和host
http://Example.com:80 http://example.com http 默认80端口所以同源

如果不在同一个源下任何跨文档dom访问都是被阻止的。不同源下的访问可以称之为跨域访问同源不同源一句话就可以判断:就是url中 scheme host port 都相同即为同源。

情况 举例
端口号不同 http://www.baidu.com/a.js
vs.
http://www.baidu.com:8080/b.js
主域相同子域不同 http://www.baidu.com/a.js
vs.
http://blog.baidu.com/b.js
协议不同 http://www.baidu.com/a.js
vs.
https://www.baidu.com/b.js
域名不同 http://www.baidu.com/a.js
vs.
http://www.qq.com/b.js
域名和对应ip http://www.baidu.com/a.js
vs.
http://192.168.2.2/b.js

当然在实际应用中,多数出现在ajax请求时,在不同域下请求数据会遇到禁止跨域的问题。

为什么要有同源策略

  1. 为了防止恶意网页可以获取其他网站的本地数据。

  2. 为了防止恶意网站iframe其他网站的时候,获取数据。

  3. 为了防止恶意网站在自已网站有访问其他网站的权利,以免通过cookie免登,拿到数据。

跨域问题

在没有前后端分离的时候,跨域问题往往是很少的。因为前后端都部署到一起。现在前后端分离不管vue /react 面临跨域请求的问题。下面是引用官网描述的一张图来解释跨域:

跨源域资源共享(CORS)机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

常见的跨域方法的原理

JSONP

JSONP的原理非常简单,就是HTML标签中,很多带src属性的标签都可以跨域请求内容,比如我们熟悉的img图片、iframe标签。同理,script标签也可以,可以利用script标签来执行跨域的javascript代码。通过这些代码,我们就能实现前端跨域请求数据。

例如baidu.com引用了CDN的jquery。所以我通过调用js脚本的方式,从服务器上获取JSON数据绕过同源策略。不过JSONP只能解决GET提交方式的跨域问题

webpack-dev-server

前端无论是vue项目还是react 项目大多数都会以webpack-dev-server 来运行,webpack-dev-server 可以设置代理,前端可以在开发环境设置代理解决跨域问题。

1
2
3
4
5
6
7
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' },
changeOrigin: true,
},
}

nginx反向代理

当你访问baidu.com/api/login的时候,通过在baidu.com的nginx服务器会识别你是api下的资源,会自动代理到api.baidu.com/login,浏览器本身是不知道我实际上是访问的api.baidu.com的数据,和前端资源同源,所以也就不会触发浏览器的同源策略。

1
2
3
4
location /api {
proxy_pass https://b.test.com; # 设置代理服务器的协议和地址
}

CORS

CORS(跨域资源共享)使用专用的HTTP头,服务器(api.baidu.com)告诉浏览器,特定URL(baidu.com)的ajax请求可以直接使用,不会激活同源策略。

什么是CORS

  • CORS是—个w3c标准,全称是“跨域资源共享”(Cross-origin resource sharing)。
  • ·它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAx只能同源使用的限制。
  • CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
  • CORS原理:
    • 客户端自动向请求头header中注入origin。
    • 服务器端需要向响应头header中注入Access-Control-Allow-Origin
    • 浏览器检测到header中的Access-Control-Allow-Origin,则就可以跨域操作了。

CORS 处理流程

浏览器的请求可以分为简单请求和非简单请求两种。浏览器对不同类型的请求进行 CORS 处理的方式有所不同。

简单请求

满足下面三个条件的请求可以被称为简单请求。

  1. 请求方法为 GET、HEAD、POST 之一。
  2. 允许设置的请求头包括 AcceptAccept-LanguageContent-LanguageContent-Type
  3. Content-Type 的值仅限于 text/plainmultipart/form-dataapplication/x-www-form-urlencoded

浏览器发起请求时在请求头添加 Origin 字段,表示当前资源所在的源,服务端收到请求后检查该字段,如果允许该请求则在响应头添加Access-Control-Allow-Origin 字段,否则可以拒绝处理请求并返回错误的 HTTP 响应码,浏览器收到响应后发现没有 Access-Control-Allow-Origin 字段或者 Access-Control-Allow-Origin 字段值有误则会在控制台打印不允许跨域的错误信息。

非简单请求

对于非简单请求,浏览器首先会发起预请求 Preflight Request 检查是否允许跨域,如果允许跨域才会执行真正的请求,

预请求的 HTTP 方法为 OPTIONS,携带的请求头如下:

  • Origin:表示资源所在的源。

  • Access-Control-Request-Method:表示真实 HTTP 请求方法的请求头。

  • Access-Control-Request-Headers:表示真实 HTTP 请求方法自定义的请求头 。

如果服务端允许跨域请求,会在响应头添加如下的字段:

  • Access-Control-Allow-Origin :表示跨域请求允许的源,* 表示允许任何源。

  • Access-Control-Request-Method:表示跨域请求允许使用的请求方法,可以比请求头中的多。

  • Access-Control-Request-Headers:表示跨域请求允许携带的请求头。

如果服务端不允许跨域请求,则可以直接返回表示错误的 HTTP 响应码。

浏览器收到预请求响应后检查响应头判断是否允许跨域,如果不允许跨域则直接在控制台打印跨域报错信息。如果允许跨域再正常发起请求,携带请求头 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 以及自定义的请求头。

携带用户身份的跨域请求

服务端检查跨域请求后,除了返回基本的响应头,还可以添加如下额外的响应头:

  • Access-Control-Max-Age:表示跨域检查结果在浏览器中可以缓存的秒数。
  • Access-Control-Expose-Headers:默认情况 JS 只能获取一些基本的响应头,这个字段允许 JS 可以获取除基本响应头的其他响应头。

除此之外,服务端还可以返回值为 true 的响应头 Access-Control-Allow-Credentials,这个响应头可以让浏览器在跨域请求时携带 Cookie 信息,当然了,需要在发起请求时配置 withCredentials=true

如果服务端返回了响应头 Access-Control-Allow-Credentials,此时 Access-Control-Allow-Origin 不能返回 *,否则请求将会失败。

Http 协议CORS头

跨域其实也是http层面上可以解决的问题,后端解决也是比较简单的,也是项目常见的解决手法。

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

同源安全策略 默认阻止“跨域”获取资源。但是 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。

  • Access-Control-Allow-Origin 指示请求的资源能共享给哪些域。
  • Access-Control-Allow-Credentials 指示当请求的凭证标记为 true 时,是否响应该请求。
  • Access-Control-Allow-Headers 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。
  • Access-Control-Allow-Methods 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。
  • Access-Control-Expose-Headers 指示哪些 HTTP 头的名称能在响应中列出。
  • Access-Control-Max-Age 指示预请求的结果能被缓存多久。
  • Access-Control-Request-Headers 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。
  • Access-Control-Request-Method 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。
  • Origin 指示获取资源的请求是从什么域发起的。

Spring MVC CORS 处理

由于每个接口都需要处理跨域请求,因此在传统的 Java Web 项目中通常使用 Filter 进行全局处理。

Spring MVC 中进行跨域处理的核心类是 HandlerMapping,当请求到达 DispatchServlet,如果请求是预请求 Spring 会将处理器替换为跨域处理器,如果请求是非预请求 Spring 将在拦截器链前面添加跨域拦截器,然后根据 CORS 配置进行相应的处理。再把 DispatcherServlet 流程图祭出,和 CORS 相关的部分可以见右上角。

配置文件 web.xml 配置

Spring MVC 基于 Servlet 规范,Spring 早期,Servlet 和 Filter 配置方式与传统的 Java Web 项目并没有任何区别,需要在 web.xml 配置 Filter 清单。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<filter>
<filter-name>cors</filter-name>
<filter-class>com.zzuhkp.mvc.CorsFilter</filter-class>
<init-param>
<param-name>allowedMethods</param-name>
<param-value>GET,POST</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>

@WebFilter 注解配置

Java 5 注解诞生后,Servlet 在 3.0 新引入了 @WebFilter 注解,用来替代 web.xml 文件中 Filter 的配置。Servlet 容器启动后会扫描类路径下的文件,遇到携带 @WebFilter 的注解后就会将这个类注册到容器中。因此在 Spring MVC 环境下也可以直接使用这个注解,和 xml 配置等同的注解配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@WebFilter(filterName = "CrosFilter",urlPatterns = {"/*"})
// @WebFilter(urlPatterns = "/*", initParams = {@WebInitParam(name = "allowedMethods", value = "GET,POST")})
public class CrosFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest) request;
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
resp.setHeader("Access-Control-Max-Age", "3600");
resp.setHeader("Access-Control-Allow-Headers", "x-requested-with, Content-Type, Accept, Origin");
resp.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(req, resp);
}

@Override
public void destroy() {

}
}

ServletContainerInitializer 配置

除了常规的 Servlet 规范中的 xml 和 @WebFilter 配置方式, Servlet 3.0 规范还提供了一个 ServletContainerInitializer 接口,Servlet 容器启动后会扫描类路径,标注了 @HandlesTypes 注解的 ServletContainerInitializer 接口实现将会被回调。因此,在 Spring MVC 中也可以利用这个特性添加 Filter,具体代码如下:

1
2
3
4
5
6
7
8
9
10
@HandlesTypes({})
public class FilterInitializer implements ServletContainerInitializer {

@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
FilterRegistration.Dynamic dynamic = ctx.addFilter("cors", new CorsFilter());
dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
dynamic.setInitParameter("allowedMethods","GET,POST");
}
}

WebApplicationInitializer 配置

Spring 3.1 版本利用了上述 Servlet 规范中 ServletContainerInitializer 的特性,提供了这个接口的实现 SpringServletContainerInitializer,并在实现中回调了 Spring 提供的 WebApplicationInitializer 接口。因此,Spring MVC 环境也可以直接实现 WebApplicationInitializer 来手动配置 Filter。注意:只需要实现接口,无需特定配置,Servlet 容器会把这个类告诉 SpringServletContainerInitializer。示例代码如下:

1
2
3
4
5
6
7
8
public class CorsWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
FilterRegistration.Dynamic dynamic = servletContext.addFilter("cors", new CorsFilter());
dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
dynamic.setInitParameter("allowedMethods", "GET,POST");
}
}

Spring Bean 配置

1
2
3
4
5
6
7
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
}

CorsFilter

Filter 是解决跨域的传统方式,Spring 出现前,我们经常会写一个解决跨域的 Filter,当请求到来时向响应头中添加固定的字段。

Spring MVC 提供了一个具有相同功能的 CorsFilter,这样以后我们就不需要每个项目都单独写一个处理跨域的 Filter 了。

SpringBoot 环境下配置 CorsFilter 示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class WebMvcConfig {

@Bean
public Filter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("http://hukp.cn");
corsConfiguration.addAllowedMethod(HttpMethod.POST);
corsConfiguration.addAllowedHeader("token");
corsConfiguration.setExposedHeaders(Arrays.asList("header1", "header2"));
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/*", corsConfiguration);
CorsFilter corsFilter = new CorsFilter(corsConfigurationSource);
return corsFilter;
}

}

实例化 CorsFilter 时需要指定一个 CorsConfigurationSource 实例用来获取跨域配置 CorsConfiguration,常用的实现是 UrlBasedCorsConfigurationSource。

全局 CORS 配置

Spring MVC 官方解决 CORS 的做法是在 HandlerMappping 获取处理器链时根据是否为预请求使用 PreFlightHandler 作为处理器或者添加拦截器 CorsInterceptor,具体可以参见源码AbstractHandlerMapping#getCorsHandlerExecutionChain。

对于用户而言,只需要进行 CORS 配置就可以了,而配置分为全局配置和局部配置,Spring 会把这两个配置进行合并。对于全局配置而言有 API 和 XML 两种配置方式。

XML 配置

XML 配置是 Spring 早期提供的支持,和上述 CorsFilter 等价的 CORS 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:cors>
<mvc:mapping path="/*"
allowed-origins="http://hukp.com"
allowed-methods="POST"
allowed-headers="token"
exposed-headers="header1, header2"
max-age="3600"
allow-credentials="true"
/>
</mvc:cors>
</beans>

API 配置

当前注解已经成为 Spring 的主流使用方式,使用 @EnableWebMvc 开启 Web 相关特性后可以通过实现接口 WebMvcConfiger 进行跨域配置,最终这个配置将传递到 AbstractHandlerMapping。和 XML 等价的 API 配置方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/*")
.allowedOrigins("http://hukp.cn")
.allowedMethods("POST")
.allowedHeaders("token")
.exposedHeaders("header1", "header2")
.maxAge(3600)
.allowCredentials(true);
}
}

局部 CORS 配置

除了全局配置,Spring 还可以针对每个处理器做特殊的配置。

API 配置

如果想用一个处理器类处理一个请求,这个处理器类可以实现接口 HttpRequestHandler、Controller 或者 HandlerFunction,如果想要为这个处理器进行 CORS 处理,还需要实现接口 CorsConfigurationSource。以登录场景为例,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LoginHandler implements Controller, CorsConfigurationSource {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 省略相关逻辑
return new ModelAndView();
}

// 获取 CORS 配置
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("http://hukp.cn");
corsConfiguration.addAllowedMethod(HttpMethod.POST);
corsConfiguration.addAllowedHeader("token");
corsConfiguration.setExposedHeaders(Arrays.asList("header1", "header2"));
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
}

注解配置

Spring 添加对注解的支持后,我们使用 @Controller 标注控制器类,然后在类中使用 @RequestMapping 标注处理器方法。针对这种方式,由于请求由方法进行处理,我们没办法实现 CorsConfigurationSource 做跨域配置,但是 Spring 也提供了对应的解决方案。

我们可以使用 @CrossOrigin 注解做跨域配置,可以把这个类加在控制器类或者控制器方法上,控制器类上的 @CrossOrigin 适用于所有的控制器方法,控制器方法上的 @CrossOrigin 适用于自身,如果类和方法上都有 @CrossOrigin 注解,Spring 则会将配置合并。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@CrossOrigin(origins = "http://hukp.cn",
allowedHeaders = "token")
public class LoginController {

@CrossOrigin(methods = RequestMethod.POST,
exposedHeaders = {"header1", "header2"},
maxAge = 3600L,
allowCredentials = "true")
@PostMapping("/login")
public ModelAndView login(HttpServletRequest request) {
// 省略业务逻辑
return new ModelAndView();
}
}

当请求 /login 到达时,将使用类和方法上合并后的 CORS 配置。

Spring Security CORS 处理

Spring Boot 环境下如果引入了 Spring Security,Spring 将自动配置 CorsFilter,此时从CorsConfigurationSource 类型的 bean 中读取 CORS 配置,因此将 CorsConfigurationSource 配置为 bean 即可。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class WebMvcConfig {

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("http://hukp.cn");
corsConfiguration.addAllowedMethod(HttpMethod.POST);
corsConfiguration.addAllowedHeader("token");
corsConfiguration.setExposedHeaders(Arrays.asList("header1", "header2"));
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/*", corsConfiguration);
return corsConfigurationSource;
}
}

SpringCloud设置跨域

在跨域过滤器里配置一下跨域头部,* 是通配符即允许所有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class GatewayCorsConfiguation {

@Bean
public CorsFilter corsFilter(){
// 初始化cors配置对象
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true); // 允许使用cookie,但是使用cookie是addAllowedOrigin必须是具体的地址,不能是*
// configuration.addAllowedOrigin("*");
configuration.addAllowedOrigin("http://manage.leyou.com");
configuration.addAllowedMethod("*"); //允许的请求方式,get,put,post,delete
configuration.addAllowedHeader("*");//允许的头信息

//初始化cors的源对象配置
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**",configuration);

//3.返回新的CorsFilter.
return new CorsFilter(corsConfigurationSource);
}
}