@CrossOrigin及其实现跨域原理
一、关于什么是跨域及解决方法看这篇文章文章二、关于@CrossOrigin简单请求和非简单请求对于简单请求(GET,HEAD,POST),浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会
一、关于什么是跨域及解决方法看这篇文章
二、关于@CrossOrigin
- 简单请求和非简单请求
- 对于简单请求
(GET,HEAD,POST)
,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。- 非简单请求是
那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为预检请求 (preflight)
- @CrossOrigin源码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
@Deprecated
String[] DEFAULT_ORIGINS = {"*"};
@Deprecated
String[] DEFAULT_ALLOWED_HEADERS = {"*"};
@Deprecated
boolean DEFAULT_ALLOW_CREDENTIALS = false;
@Deprecated
long DEFAULT_MAX_AGE = 1800;
@AliasFor("origins")
String[] value() default {};
@AliasFor("value")
String[] origins() default {};
/**
* @since 5.3
*/
String[] originPatterns() default {};
String[] allowedHeaders() default {};
String[] exposedHeaders() default {};
RequestMethod[] methods() default {};
String allowCredentials() default "";
long maxAge() default -1;
}
可以看见@CrossOrigin注解可以标注在类或者方法上,其中几个常量如DEFAULT_ORIGINS已经在Spring 5.0弃用
,取而代之的是CorsConfiguration#applyPermitDefaultValues方法。
- @CrossOrigin属性介绍
- origins和value
支持的源,origins和value都是相同的配置,互为别名,默认配置是“*”,表示服务器支持所有源的跨域请求,安全信息较低,最好根据实际情况设置对应的信息(协议 + 域名 + 端口)。
- originPatterns
同样表示支持的源,Spring 5.3 引入的属性,默认为空,与origins二选一,支持通配符的形式配置origins,比如https://*.domain1.com,该字段为list,也就是可以配置多个。
- allowedHeaders
允许跨域的请求头信息,默认为“*”表示允许所有的请求头,CORS默认支持的请求头为:Cache-Control、Content-Language、Expires、Last-Modified、Pragma,如果你需要携带其他的请求头需要设置该属性。
- exposedHeaders
服务器允许客户端访问的相应头,默认为空,表示只允许访问:Cache-Control、Content-Language、Expires、Last-Modified、Pragma,如果需要客户端访问其他的相应头需要设置该属性。
- methods
服务器允许的Http Request类型,默认是允许GET、POST、HEAD,根据项目需要自行设置。
- allowCredentials
浏览器是否需要把凭证(如:cookies、CSRF tokens)发送到服务器,默认是关闭的,因为该选项开启后会与配置的源建立高度信任的关系,并且还会暴露一些敏感信息,所以开启该选项时origin不允许设置为“*”。
- maxAge
“预检”结果的缓存时间,单位是秒,默认1800s,在缓存时间内同一请求不需要“预检”请求。
- @CrossOrigin用法
标注在类上,该类的所有方法均会生效
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
同时也可以类和方法结合使用
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@CrossOrigin注解比较适用于较细粒度的跨域控制,对于全局的跨域控制,SpringMVC提供了Global Configuration配置。
- Global Configuration
Spring Mvc对于全局的CORS比较简单,分为两个方案
- 实现WebMvcConfigurer接口
@Configuration
@EnableWebMvc
//创建WebConfig类实现WebMvcConfigurer接口,通过CorsRegistry设置跨域信息
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
- 通过CorsFilter
@Bean
//通过CorsConfiguration设置跨域信息,并将CorsConfiguration通过CorsFilter构造函数传递进去
CorsFilter corsFilter(){
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
三、@CrossOrigin实现跨域原理
- SpringMVC的请求流程的源码中有这么一段,在获取执行链中
@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
//判断请求头中是否有ORIGIN字段
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ?
globalConfig.combine(handlerConfig) : handlerConfig);
//为跨域请求添加额外的interceptor
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
如果该请求是跨域请求,那么回带有ORIGIN字段,那么进行if,执行
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
追踪进去:
//AbstractHandlerMapping.getCorsHandlerExecutionChainc
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
//浏览器将CORS请求分成两类:简单请求和非简单请求。
//浏览器对这两种请求的处理是不一样的,
//非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
return new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
//增加专门处理跨域请求的Interceptor
chain.addInterceptor(new CorsInterceptor(config));
return chain;
}
}
AbstractHandlerMapping类是HanndlerMapping接口的实现类
- 跟踪再进入
chain.addInterceptor(new CorsInterceptor(config));
- CorsProcessor类
上面我们提到当执行getCorsHandlerExecutionChain方法时,若是CORS请求,则在 拦截器处理链链首 添加CORS拦截器。
chain.addInterceptor(new CorsInterceptor(config));
CorsInterceptor拦截器为内部类,其核心方法为preHandle,主要是调用了
this.corsProcessor.processRequest(this.config, request, response)
CorsProcessor只是接口
,其具体实现类为DefaultCorsProcessor
DefaultCorsProcessor.processRequest具体实现如下
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException {
// 对于CORS请求,需要通过CORS相关Header帮助判断是否直接返回缓存即可
Collection<String> varyHeaders = response.getHeaders("Vary");
if (!varyHeaders.contains("Origin")) {
response.addHeader("Vary", "Origin");
}
if (!varyHeaders.contains("Access-Control-Request-Method")) {
response.addHeader("Vary", "Access-Control-Request-Method");
}
if (!varyHeaders.contains("Access-Control-Request-Headers")) {
response.addHeader("Vary", "Access-Control-Request-Headers");
}
// 这部分检测如果不是CORS请求,则直接返回true
if (!CorsUtils.isCorsRequest(request)) {
return true;
} else if (response.getHeader("Access-Control-Allow-Origin") != null) {
// 如果已经存相关HEADER,代表前面已经有Filter/Inteceptor进行了CORS相关设置
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
} else {
// 判断是否为CORS预检请求——OPTIONS方法
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
this.rejectRequest(new ServletServerHttpResponse(response));
return false;
} else {
return true;
}
} else {
// 处理逻辑核心,请看具体实现
return this.handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
}
}
// handleInternal具体实现
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, CorsConfiguration config, boolean preFlightRequest) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = this.checkOrigin(config, requestOrigin); // 判断是否匹配Origin,不匹配则返回null
HttpHeaders responseHeaders = response.getHeaders();
if (allowOrigin == null) { // 这里代表未成功匹配Origin,拒绝请求
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
this.rejectRequest(response);
return false;
} else {
HttpMethod requestMethod = this.getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = this.checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
this.rejectRequest(response);
return false;
} else {
// -----------------!!! 真正添加CORS 相关 Header 参数的位置 !!!-----------------
List<String> requestHeaders = this.getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = this.checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
this.rejectRequest(response);
return false;
} else {
// -----------------!!! 设置Access-Control-Allow-Origin !!!-----------------
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
// -----------------!!! 设置Access-Control-Allow-Methods !!!-----------------
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
// -----------------!!! 设置Access-Control-Expose-Headers !!!-----------------
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
// -----------------!!! 设置Access-Control-Expose-Headers !!!-----------------
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
// -----------------!!! 设置Access-Control-Allow-Credentials !!!-----------------
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
// -----------------!!! 设置Access-Control-Max-Age !!!-----------------
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
}
}
}
- HTTP Header 中 Vary字段作用
- Vary HTTP 响应头
决定如何满足未来的请求头
,以决定一个缓存的响应是否可以使用,而不是请求从源服务器一个新的一个。- Vary的意义在于告诉代理服务器/缓存/CDN,如何判断是否直接返回缓存资源即可。
比如Vary中有User-Agent,那么即使相同的请求,如果用户使用IE打开了一个页面,再用Firefox打开这个页面的时候,CDN/代理会认为是不同的页面,如果Vary中没有User-Agent,那么CDN/代理会认为是相同的页面,直接给用户返回缓存的页面,而不会再去web服务器请求相应的页
面。
- 思考
- Spring Web 5.3.9版本CORS是通过CorsInterceptor实现的,CorsInterceptor实现了HandlerInterceptor接口,因此可知其实际是通过拦截器实现的。那么在Spring Web中,Filter和InterceptorInterceptor的区别是什么?
过滤器: 依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,获取我们想要获取的数据.比如:在过滤器中修改字符编码;在过滤器中修改 HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等
拦截器: 依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用.因此可以使用spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。
- Filter和Interceptor执行顺序?
Interceptor链先执行还是Filter链先执行?
首先,Filter是基于Servlet的,通过即< url-pattern >/hello< /url-pattern >标签产生关联,其他相关标签是< serlvet-mapping >和< filter-mapping标签 >。
<servlet-mapping>
<servlet-name>servlet</servlet-name>
<url-pattern>/hello</url-pattern>
<url-pattern>/world</url-pattern>
<url-pattern>/home1</url-pattern>
</servlet-mapping>
<filter-mapping>
<filter-name>filter</filter-name>
<url-pattern>/hello</url-pattern>
<url-pattern>/home2</url-pattern>
</filter-mapping>
而Interceptor是基于MVC的,相关xml配置。
<!-- 拦截器 -->
<mvc:interceptors>
<!-- 对所有请求都拦截,公共拦截器可以有多个 -->
<bean name="baseInterceptor" class="com.scorpios.interceptor.BaseInterceptor" />
<mvc:interceptor>
<!-- 对/test.html进行拦截 -->
<mvc:mapping path="/test.html"/>
<!-- 特定请求的拦截器只能有一个 -->
<bean class="com.scorpios.interceptor.TestInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
Filter的执行是有先后顺序的,根据在web.xml中配置的先后顺序
。其主要有三个方法init、doFilter、destory,我们主要关注doFilter方法,在这个方法中chain.doFilter会将请求及响应发送给下一个Filter,我们在这句调用前执行前置操作及后置操作即可。
public class LoggerFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化参数,在创建Filter时自动调用,当我们需要设置初始化参数的时候,可以写到该方法中");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String contextPath = request.getServletContext().getContextPath();
System.out.println("执行Filter前置操作");
chain.doFilter(request, response);
System.out.println("执行Filter后置操作");
}
@Override
public void destroy() {
System.out.println("在销毁Filter时自动调用");
}
}
更多推荐
所有评论(0)