首页 » java

SpringBoot-实现CORS跨域原理及解决方案

   发表于:java评论 (0)   热度:332

Cors的原理就不介绍了,直接先上解决方案,

也可以看这个,https://blog.csdn.net/c5113620/article/details/79132968

https://segmentfault.com/a/1190000012364985,https://www.jianshu.com/p/470de10b9eec
 

比较好的方法:(同时设置过滤器顺序order,避免先加载其他filter导致的问题,同时可以解决拦截器与过滤器加载优先级的问题,可查看 
1、https://blog.csdn.net/z69183787/article/details/109483337
2、https://blog.csdn.net/z69183787/article/details/53102398  CorsInterceptor)
如果需要动态从配置中心获取 allowOrigin,可重写cosConfiguration的checkOrigin方法
 

@Configuration
public class CorsConfig {
    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration(){
            //动态匹配
            @Override
            public String checkOrigin(String requestOrigin) {
                //TODO nacos配置中心获取,正则匹配
                return null;
            }
        };
        config.setAllowCredentials(true);
        // 设置你要允许的网站域名,如果全允许则设为 *
        config.addAllowedOrigin("http://localhost");
        // 如果要限制 HEADER 或 METHOD 请自行更改
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        // 这个顺序很重要哦,为避免麻烦请设置在最前
        bean.setOrder(-1);
        return bean;
    }
}

 

第一种办法

(会造成与其他拦截器优先级加载的问题,导致跨域配置失效 https://blog.csdn.net/z69183787/article/details/109483337):

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class CorsConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

这种方式是全局配置的,网上也大都是这种解决办法,但是很多都是基于旧的spring版本,比如:

Spring Boot如何解决前端的Access-Control-Allow-Origin跨域问题

文中WebMvcConfigurerAdapter在spring5.0已经被标记为Deprecated,点开源码可以看到:

/**
 * An implementation of {@link WebMvcConfigurer} with empty methods allowing
 * subclasses to override only the methods they're interested in.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 * @deprecated as of 5.0 {@link WebMvcConfigurer} has default methods (made
 * possible by a Java 8 baseline) and can be implemented directly without the
 * need for this adapter
 */
@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {}

 

像这种过时的类或者方法,spring的作者们一定会在注解上面说明原因,并告诉你新的该用哪个,这是非常优秀的编码习惯,点赞!

spring5最低支持到jdk1.8,所以注释中明确表明,你可以直接实现WebMvcConfigurer接口,无需再用这个适配器,因为jdk1.8支持接口中存在default-method。

第二种办法:

import org.springframework.context.annotation.Configuration;
 
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
//缺省 /*
@WebFilter(filterName = "CorsFilter")
@Configuration
public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin","*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        chain.doFilter(req, res);
    }
}

这种办法,是基于过滤器的方式,方式简单明了,就是在response中写入这些响应头,好多文章都是第一种和第二种方式都叫你配置,其实这是没有必要的,只需要一种即可。

这里吐槽一下,大家不求甚解的精神。

第三种办法:

public class GoodsController {
 
    @CrossOrigin(origins = "http://localhost:4000")
    @GetMapping("goods-url")
    public Response queryGoodsWithGoodsUrl(@RequestParam String goodsUrl) throws Exception {}
}   

没错就是@CrossOrigin注解,点开注解

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {}

从元注解@Target可以看出,注解可以放在method、class等上面,类似RequestMapping,也就是说,整个controller下面的方法可以都受控制,也可以单个方法受控制。

也可以得知,这个是最小粒度的cors控制办法了,精确到单个请求级别。

以上三种方法都可以解决问题,最常用的应该是第一种、第二种,控制在自家几个域名范围下足以,一般没必要搞得太细。

这三种配置方式都用了的话,谁生效呢,类似css中样式,就近原则,懂了吧。

所以在开发新项目时,不需要等联调时候,让前端来找你了,我早就解决了跨域问题。

-----------------------------------------------------------------------------------------------

前戏搞定,现在开始研究第一种方案中,为何继承了WebMvcConfigurer并实现addCorsMapping方法就可以搞定cors

这个问题首先应该从SpringBoot是如何使用SpringMvc的默认配置开始说起:

1、大家都知道,SpringBoot简易化配置的背后是 “自动装配” 在生效,EnableAutoConfiguration 注解带来了自动装配的特性,在这里不深究其源码,最后他是从 

protected static final String PATH = "META-INF/"
			+ "spring-autoconfigure-metadata.properties";

这个文件里获取所有的需要完成自动装配的Configuration类的元数据的,进入这个文件我们发现有1个WebMvc开头的类

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.xxxx

这个就是引导SpringMvc自动配置的Configuration类了,后面的xxx代表自动装配这个配置项需要满足的条件,包含了Bean的前后序,ConditionalOnClass依赖类等等,一般通过以上这种方式,我们可以反查出某些 boot 默认自动配置的Configuration类。

原理其实很简单,在自动装配的过程中,根据带有@Configurtaion注解的配置类与 “PATH” 中匹配的内容进行比较,再经过筛选后才会决定是否加载配置类及内部的带有@Bean注解Bean实例。

2、接下来分析WebMvcAutoConfiguration是如何解决跨域的设置问题的,看一下部分的源码

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

和我们猜想的一样,是一个带有@Configuration注解的配置类,然后找一下我们关心的Cors在哪。

3、其实这个类中还有一个内部类,发现很多AutoConfiguration自动装配类中的类结构都差不多,内嵌一个Adapter类,并且Adapter类中使用@Import注解 引入了另一个 配置主类,如下所示:

内部类:WebMvcAutoConfigurationAdapter:

// Defined as a nested config to ensure WebMvcConfigurer is not read when not
	// on the classpath
	@Configuration
	@Import(EnableWebMvcConfiguration.class)
	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter
			implements WebMvcConfigurer, ResourceLoaderAware {

import类:

/**
	 * Configuration equivalent to {@code @EnableWebMvc}.
	 */
	@Configuration
	public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {

贴一下类的层次结构,可以明显的看到它的继承情况:


可以看到 上面标注了一句,与@EnableWebMvc相同。在这里多说一句,@EnableWebMvc可以显示的申明MVC的配置,用来替代boot默认自动装配的Configuration,其实他们两个的实现是一致的,都是DelegatingWebMvcConfiguration。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

4、接下来言归正传,看看EnableWebMvcConfiguration中有没有和跨域相关的配置类,经过一番查看,发现如下代码应该与CORS有关:RequestMappingHandlerMapping正是处理Request请求相关的处理类,进入父类的父类WebMvcConfigurationSupport查看
 

@Bean
@Primary
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
	// Must be @Primary for MvcUriComponentsBuilder to work
	return super.requestMappingHandlerMapping();
}

发现了这段代码:getCorsConfigurations可以明显感觉到这应该和cors的配置相关

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
	RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
	mapping.setOrder(0);
	mapping.setInterceptors(getInterceptors());
	mapping.setContentNegotiationManager(mvcContentNegotiationManager());
	mapping.setCorsConfigurations(getCorsConfigurations());

我们继续进入这个方法内部一探究竟:

protected final Map<String, CorsConfiguration> getCorsConfigurations() {
	if (this.corsConfigurations == null) {
		CorsRegistry registry = new CorsRegistry();
		addCorsMappings(registry);
		this.corsConfigurations = registry.getCorsConfigurations();
	}
	return this.corsConfigurations;
}

看到了熟悉的addCorsMappings方法,和解决方案中第一个答案的实现方法一模一样。那他们是怎么发生关系的呢?

5、进入addCorsMappings方法,这次WebMvcConfigurationSupport没有对这个方法进行实现,由一级子类DelegatingWebMvcConfiguration进行了实现,我们进入这个类

@Override
protected void addCorsMappings(CorsRegistry registry) {
	this.configurers.addCorsMappings(registry);
}

发现是this.configures调用了这个方法,往上找到configures属性,一切都恍然大悟了

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
 
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
 
 
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
WebMvcConfigurerComposite的关键方法:delegates 保存了WebConfigurer实现类的委托

class WebMvcConfigurerComposite implements WebMvcConfigurer {
 
    private final List<WebMvcConfigurer> delegates = new ArrayList<>();
 
 
    public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.delegates.addAll(configurers);
        }
    }
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        for (WebMvcConfigurer delegate : this.delegates) {
            delegate.addCorsMappings(registry);
        }
    }


明显看到,@Autowired注解将所有实现了WebMvcConfigurer接口的类,包括了方案一中自定义的cors实现类自动注入了到了属性configures中。配合我们在第4步中在 new RequestMappingHandlerMapping 的时候一层层调用,最终注入了cors配置

new RequestMappingHandlerMapping  ->  getCorsConfigurations() -> addCorsMappings()

实际项目运行时,会从CorsRegistry中获取cors配置,并在response时使用。

/**
 * Assists with the registration of global, URL pattern based
 * {@link CorsConfiguration} mappings.
 *
 * @author Sebastien Deleuze
 * @author Rossen Stoyanchev
 * @since 4.2
 * @see CorsRegistration
 */
public class CorsRegistry {
 
    private final List<CorsRegistration> registrations = new ArrayList<>();
 
 
    /**
     * Enable cross-origin request handling for the specified path pattern.
     *
     * <p>Exact path mapping URIs (such as {@code "/admin"}) are supported as
     * well as Ant-style path patterns (such as {@code "/admin/**"}).
     * <p>By default, all origins, all headers, credentials and {@code GET},
     * {@code HEAD}, and {@code POST} methods are allowed, and the max age
     * is set to 30 minutes.
     *
     * <p>The following defaults are applied to the {@link CorsRegistration}:
     * <ul>
     *     <li>Allow all origins.</li>
     *     <li>Allow "simple" methods {@code GET}, {@code HEAD} and {@code POST}.</li>
     *     <li>Allow all headers.</li>
     *     <li>Set max age to 1800 seconds (30 minutes).</li>
     * </ul>
     */
    public CorsRegistration addMapping(String pathPattern) {
        CorsRegistration registration = new CorsRegistration(pathPattern);
        this.registrations.add(registration);
        return registration;
    }
 
    /**
     * Return the registered {@link CorsConfiguration} objects,
     * keyed by path pattern.
     */
    protected Map<String, CorsConfiguration> getCorsConfigurations() {
        Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
        for (CorsRegistration registration : this.registrations) {
            configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
        }
        return configs;
    }
 
}


 

最后,给到几段源码,它们是真正使用CorsRegistry对请求作出处理的实现和过程。再深层次的细节内容大家可以自行阅读源码及DefaultCorsProcessor类,谢谢

//作为RequestMappingHandlerMapping的父类,掌控着cors的请求和响应
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered {
 
//所有的corsRegistry最终都会进入这个属性中
private final UrlBasedCorsConfigurationSource globalCorsConfigSource = new UrlBasedCorsConfigurationSource();
 
//实际拦截请求进行cors处理的地方,
    protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
            HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
        //如果是预检请求
        if (CorsUtils.isPreFlightRequest(request)) {
            HandlerInterceptor[] interceptors = chain.getInterceptors();
            chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
        }
        //正常请求
        else {
            chain.addInterceptor(new CorsInterceptor(config));
        }
        return chain;
    }


可以看到,这种方式下,cors是通过 拦截器的模式 进行跨域配置的,所以会造成与其他拦截器加载顺序导致 失效 ,参照:https://blog.csdn.net/z69183787/article/details/109483337
 

版权声明:本文为CSDN博主「OkidoGreen」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/z69183787/article/details/104558901

(。・v・。)
喜欢这篇文章吗?欢迎分享到你的微博、QQ群,并关注我们的微博,谢谢支持。