SpringBoot解决CORS问题

栏目: IT技术 · 发布时间: 4年前

内容简介:在做前后端分离的开发或者前端调用第三方平台的接口时经常会遇到跨域的问题,前端总是希望能够通过各种方法解决跨域的问题。但事实上跨域问题是安全问题。这篇文章将会讲解一些为什么会有跨域问题,并提供一个方便的解决方法。为了阅读的流畅,相关的参考链接均会在文章末尾给出。本文使用的springboot版本为跨域问题的产生是因为同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

在做前后端分离的开发或者前端调用第三方平台的接口时经常会遇到跨域的问题,前端总是希望能够通过各种方法解决跨域的问题。但事实上跨域问题是安全问题。这篇文章将会讲解一些为什么会有跨域问题,并提供一个方便的解决方法。为了阅读的流畅,相关的参考链接均会在文章末尾给出。本文使用的springboot版本为 2.1.6.RELEASE ,相应的spring版本为 5.1.8.RELEASE

跨域问题的产生

跨域问题的产生是因为 浏览器的同源策略 。同源策略将 协议+域名+端口 构成的三元作为一个整体,只有三者均相同的情况下才属于一个源。跨域问题也就是不同源之间访问导致的问题。

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

浏览器的同源策略 @developer.mozilla.org

下表给出了相对 http://store.company.com/dir/page.html 同源检测的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 成功 只有路径不同
http://store.company.com/dir/inner/another.html 成功 只有路径不同
https://store.company.com/secure.html 失败 不同协议 ( https和http )
http://store.company.com:81/dir/etc.html 失败 不同端口 ( http:// 80是默认的)
http://news.company.com/dir/other.html 失败 不同域名 ( news和store )

对于跨域的请求,服务器可以接受到请求,但浏览器不会出来请求的返回结果。

在浏览器中打开本地的一个html文件,在客户端中输入一下的内容可以模拟跨域的请求。(为了防止因为https的限制而无法发送请求,可以自己启动一个前端服务,然后再进行试验。)

var xhttp = new XMLHttpRequest();
xhttp.open("GET", "http://192.168.20.185:8080/users/12345678", true);
xhttp.send();

跨域资源共享 CORS

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

– 前端的辅助配置

Header 值示例 描述
Access-Control-Allow-Credentials true 是否允许发送Cookie,默认false
Access-Control-Allow-Headers Authorization,Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers 后端接受的请求头。除了Accept,Accept-Language,Content-Language,Last-Event-ID和Content-Type外的附加请求头
Access-Control-Allow-Methods GET,POST,HEAD,OPTIONS,PUT,DELETE,PATCH 后端接受的请求方法。除了HEAD,GET,POST外的请求方法
Access-Control-Allow-Origin http://localhost:4000 请求的来源,一般为当前页面所在的源。要么为准确值,要么为 * .
Access-Control-Expose-Headers Access-Control-Allow-Origin,Access-Control-Allow-Credentials 暴露给客户端的请求头。除了Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma之外的附加响应头
Access-Control-Max-Age 86400 预检请求有效时长,单位为秒
  • 当Access-Control-Allow-Credentials为true时 ,不可以设置Access-Control-Allow-Origin为 *
  • 减少预检请求(Option) 通过延长预检请求的有效期,可以减少对同一个源的Option请求的数量。如设置为86400,则24小时内无需在对同一个源发送Option请求。

Spring Web解决方法

通过过滤器处理请求,对origin进行判断,并添加必要的Headers。

@CrossOrigin

范围: 单个类 单个Path

@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }
}

可类级别配置,也可方法级别配置。默认:

  • 所有origins
  • 所有headers
  • 所有http方法

@CrossOrigin 支持各个值的配置。 @CrossOrigin 虽然提供了简单的配置,但需要重复为不同的类和方法进行配置,重复麻烦。如果对于某些请求有特定的配置需要可以使用。

注解 @CrossOrigin 会成为 CorsConfiguration 的一部分,可与 WebMvcConfigurer#addCorsMappings(CorsRegistry) 一起使用,为并列关系。

WebMvcConfigurer#addCorsMappings(CorsRegistry)

范围: 全局 单个Path

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("https://domain1.com, https://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}

效果相同的XML配置:

<mvc:cors>

    <mvc:mapping path="/api/**"
        allowed-origins="https://domain1.com, https://domain2.com"
        allowed-methods="PUT,DELETE"
        allowed-headers="header1, header2, header3"
        exposed-headers="header1, header2" allow-credentials="true"
        max-age="3600" />

</mvc:cors>

真的很喜欢用JavaConfig进行配置,灵活方便。在 WebConfig.java 的实现中,可以设置一个 CorsPropertiesList.java 类来做将配置移到 .properties 配置文件中,可以得到如下的实现:

@EnableConfigurationProperties
@Configuration
public class CorsConfig {

    /**
     * 可与 @CrossOrigin 联用
     */
    @Configuration
    @EnableWebMvc
    @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "webMvc")
    public class WebConfig implements WebMvcConfigurer {

        @Autowired
        private CorsPropertiesList corsPropertiesList;

        @Override
        public void addCorsMappings(CorsRegistry registry) {
            System.out.println("config cors with " + corsPropertiesList.toString());
            for(CorsProperties corsProperties: corsPropertiesList.getList()) {
                addCorsMappings(registry, corsProperties);
            }
        }

        private void addCorsMappings(CorsRegistry registry, CorsProperties corsProperties) {
            for(String pathPattern: corsProperties.getPathPatterns()) {
                CorsRegistration registration = registry.addMapping(pathPattern);
                registration.allowedOrigins(corsProperties.getAllowedOrigins());
                registration.allowedMethods(corsProperties.getAllowedMethods());
                registration.allowedHeaders(corsProperties.getAllowedHeaders());
                registration.allowCredentials(corsProperties.getAllowedCredentials());
                registration.exposedHeaders(corsProperties.getExposedHeaders());
                registration.maxAge(corsProperties.getMaxAge());
            }
        }

        ...
    }
}
@Data
@NoArgsConstructor
@Component
@ConfigurationProperties("corses")
public class CorsPropertiesList {

    private List<CorsProperties> list;

}
@Data
public class CorsProperties {
    // Ant-style path patterns
    private String[] pathPatterns;
    private String[] allowedOrigins;
    private String[] allowedMethods;
    private String[] allowedHeaders;
    private Boolean allowedCredentials;
    private String[] exposedHeaders;
    private Long maxAge;

    public void setPathPatterns(String[] pathPatterns) {
        this.pathPatterns = pathPatterns;
    }

    public void setPathPatterns(String pathPatterns) {
        this.pathPatterns = StringUtils.split(pathPatterns, ",");
    }

    public void setAllowedOrigins(String[] allowedOrigins) {
        this.allowedOrigins = allowedOrigins;
    }

    public void setAllowedOrigins(String allowedOrigins) {
        this.allowedOrigins = StringUtils.split(allowedOrigins, ",");
    }

    public void setAllowedMethods(String[] allowedMethods) {
        this.allowedMethods = allowedMethods;
    }

    public void setAllowedMethods(String allowedMethods) {
        this.allowedMethods = StringUtils.split(allowedMethods, ",");
    }

    public void setAllowedHeaders(String[] allowedHeaders) {
        this.allowedHeaders = allowedHeaders;
    }

    public void setAllowedHeaders(String allowedHeaders) {
        this.allowedHeaders = StringUtils.split(allowedHeaders, ",");
    }

    public void setExposedHeaders(String[] exposedHeaders) {
        this.exposedHeaders = exposedHeaders;
    }

    public void setExposedHeaders(String exposedHeaders) {
        this.exposedHeaders = StringUtils.split(exposedHeaders, ",");
    }
}

application.yml

# web.config.cors: sourceConfig
# web.config.cors: customFilter
# web.config.cors: corsFilterRegistration
# web.config.cors: corsFilter
web.config.cors: webMvc

corses.list:
  -
    path-patterns:
      - /**
    allowed-origins:
      - http://localhost:*
    allowed-methods: GET,POST,HEAD,OPTIONS,PUT,DELETE,PATCH
    allowed-headers:
      - Authorization
      - Content-Type
      - X-Requested-With
      - accept,Origin
      - Access-Control-Request-Method
      - Access-Control-Request-Headers
    allowed-credentials: true
    exposed-headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials
    max-age: 86400

通过该方法的配置,可以实现全局的跨域设置,但无法修改到对origin的判断规则,比如无法实现实现对一个域名的子域名或者一个ip的任意端口的检验。

使用Spring的 CrosFilter

@Bean
public 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);

    CorsFilter filter = new CorsFilter(source);
}

到这里已经可以完成全局CORS的配置了。为了能够使用AntPathMatcher匹配origin,可以重写 CorsConfiguration#checkOrigin(String) 方法。

package io.gitlab.donespeak.tutorial.cors.config.support;

import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;

/**
 * @date 2019/12/03 00:04
 */
public class AntPathMatcherCorsConfiguration extends CorsConfiguration {

    private PathMatcher pathMatcher = new AntPathMatcher();

    @Nullable
    @Override
    public String checkOrigin(@Nullable String requestOrigin) {
        System.out.println(requestOrigin);
        if (!StringUtils.hasText(requestOrigin)) {
            return null;
        }
        if (ObjectUtils.isEmpty(this.getAllowedOrigins())) {
            return null;
        }

        if (this.getAllowedOrigins().contains(ALL)) {
            if (!Boolean.TRUE.equals(this.getAllowCredentials())) {
                // ALL 和 TRUE不是不能同时出现吗?
                return ALL;
            }
            else {
                return requestOrigin;
            }
        }

        String lowcaseRequestOrigin = requestOrigin.toLowerCase();
        for (String allowedOrigin : this.getAllowedOrigins()) {
            System.out.println(allowedOrigin + ": " + pathMatcher.match(allowedOrigin.toLowerCase(), lowcaseRequestOrigin));
            if (pathMatcher.match(allowedOrigin.toLowerCase(), lowcaseRequestOrigin)) {
                return requestOrigin;
            }
        }
        return null;
    }
}

相应的可配置 CorsFilter 如下:

@EnableConfigurationProperties
@Configuration
public class CorsConfig {

    /**
     * 不可与 @CrossOrigin 联用
     */
    @Configuration
    @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "corsFilterRegistration")
    public static class CorsFilterRegistrationConfig {

        @Bean
        public FilterRegistrationBean corsFilterRegistration(CorsPropertiesList corsPropertiesList) {
            System.out.println("create bean FilterRegistrationBean with " + corsPropertiesList);
            FilterRegistrationBean bean = new FilterRegistrationBean(createCorsFilter(corsPropertiesList));
            bean.setOrder(0);
            return bean;
        }
    }

    /**
     * 不可与 @CrossOrigin 联用
     */
    @Configuration
    @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "corsFilter")
    public static class CorsFilterConfig {

        @Bean(name = "corsFilter")
        public CorsFilter corsFilter(CorsPropertiesList corsPropertiesList) {
            System.out.println("init bean CorsFilter with " + corsPropertiesList);
            return createCorsFilter(corsPropertiesList);
        }
    }

    private static CorsFilter createCorsFilter(CorsPropertiesList corsPropertiesList) {
        return new CorsFilter(createCorsConfigurationSource(corsPropertiesList));
    }

    private static CorsConfigurationSource createCorsConfigurationSource(CorsPropertiesList corsPropertiesList) {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        for(CorsProperties corsProperties: corsPropertiesList.getList()) {
            // 路径也是 AntPathMarcher
            for(String pathPattern: corsProperties.getPathPatterns()) {
                source.registerCorsConfiguration(pathPattern, toCorsConfiguration(corsProperties));
            }
        }
        return source;
    }

    private static CorsConfiguration toCorsConfiguration(CorsProperties corsProperties) {
        CorsConfiguration corsConfig = new AntPathMatcherCorsConfiguration();
        corsConfig.setAllowedOrigins(Arrays.asList(corsProperties.getAllowedOrigins()));
        corsConfig.setAllowedMethods(Arrays.asList(corsProperties.getAllowedMethods()));
        corsConfig.setAllowedHeaders(Arrays.asList(corsProperties.getAllowedHeaders()));
        corsConfig.setAllowCredentials(corsProperties.getAllowedCredentials());
        corsConfig.setMaxAge(corsProperties.getMaxAge());
        corsConfig.setExposedHeaders(Arrays.asList(corsProperties.getExposedHeaders()));

        return corsConfig;
    }
    ...
}

通过 @Configuration 注解的配置类,添加的 CorsFilter 实例无法和 @CrossOrigin 一起使用,一旦 CorsFilter 校验不通过,请求就会被Rejected。

直接使用 CorsConfigurationSource

public class CorsConfig {

    @Configuration
    @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "sourceConfig")
    public static class CorsConfigurationSourceConfig {
        
        @Bean
        public CorsConfigurationSource corsConfigurationSource(CorsPropertiesList corsPropertiesList) {
            System.out.println("init bean CorsConfigurationSource with " + corsPropertiesList);
            return createCorsConfigurationSource(corsPropertiesList);
        }
    }
    ...
}

自定义 Filter

当然,你也可以自定义一个Filter来处理CORS,但既然有CorsFilter了,除非有什么特别的情况,否则无需自己实现一个Filter来处理CORS问题。如下给出一个大概的思路,可自行完善拓展。

package io.gitlab.donespeak.tutorial.cors.filter;

import io.gitlab.donespeak.tutorial.cors.config.properties.CorsProperties;
import io.gitlab.donespeak.tutorial.cors.config.properties.CorsPropertiesList;

...

@Slf4j
public class CustomCorsFilter implements Filter {

    private CorsPropertiesList corsPropertiesList;
    private AntPathMatcher antPathMatcher = new AntPathMatcher();
    private static final String ALL = "*";

    private Map<String, CorsProperties> corsPropertiesMap = new LinkedHashMap<>();

    public CustomCorsFilter(CorsPropertiesList corsPropertiesList) {
        this.corsPropertiesList = corsPropertiesList;
        for(CorsProperties corsProperties: corsPropertiesList.getList()) {
            for(String pathPattern: corsProperties.getPathPatterns()) {
                corsPropertiesMap.put(pathPattern, corsProperties);
            }
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest)request;
        HttpServletResponse servletResponse = (HttpServletResponse)response;

        String origin = servletRequest.getHeader("Origin");
        List<CorsProperties> corsPropertiesList = getCorsPropertiesMatch(servletRequest.getServletPath());
        if(log.isDebugEnabled()) {
            log.debug("Try to check origin: " + origin);
        }
        CorsProperties originPassCorsProperties = null;
        for(CorsProperties corsProperties: corsPropertiesList) {
            if (corsProperties != null && isOriginAllowed(origin, corsProperties.getAllowedOrigins())) {
                originPassCorsProperties = corsProperties;
                break;
            }
        }
        if (originPassCorsProperties != null) {
            servletResponse.setHeader("Access-Control-Allow-Origin", origin);
            servletResponse.setHeader("Access-Control-Allow-Methods",
                StringUtils.arrayToCommaDelimitedString(originPassCorsProperties.getAllowedMethods()));
            servletResponse.setHeader("Access-Control-Allow-Headers",
                StringUtils.arrayToCommaDelimitedString(originPassCorsProperties.getAllowedHeaders()));
            servletResponse.addHeader("Access-Control-Expose-Headers",
                StringUtils.arrayToCommaDelimitedString(originPassCorsProperties.getExposedHeaders()));
            servletResponse.addHeader("Access-Control-Allow-Credentials",
                String.valueOf(originPassCorsProperties.getAllowedCredentials()));
            servletResponse.setHeader("Access-Control-Max-Age", String.valueOf(originPassCorsProperties.getMaxAge()));
        } else {
            servletResponse.setHeader("Access-Control-Allow-Origin", null);
        }

        if ("OPTIONS".equals(servletRequest.getMethod())) {
            servletResponse.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(servletRequest, servletResponse);
        }
    }

    private List<CorsProperties> getCorsPropertiesMatch(String path) {
        List<CorsProperties> corsPropertiesList = new ArrayList<>();
       for(Map.Entry<String, CorsProperties> entry: corsPropertiesMap.entrySet()) {
           if(antPathMatcher.match(entry.getKey(), path)) {
               corsPropertiesList.add(entry.getValue());
           }
       }
       return corsPropertiesList;
    }

    private boolean isOriginAllowed(String origin, String[] allowedOrigins) {
        if (StringUtils.isEmpty(origin) || (allowedOrigins == null || allowedOrigins.length == 0)) {
            return false;
        }
        for (String allowedOrigin : allowedOrigins) {
            if (ALL.equals(allowedOrigin) || isOriginMatch(origin, allowedOrigin)) {
                return true;
            }
        }
        return false;
    }

    private boolean isOriginMatch(String origin, String originPattern) {
        return antPathMatcher.match(originPattern, origin);
    }
}

相关的配置如下:

@EnableConfigurationProperties
@Configuration
public class CorsConfig {

    /**
     * 可与 @CrossOrigin 联用
     */
    @Configuration
    @ConditionalOnProperty(prefix = "web.config", name = "cors", havingValue = "customFilter")
    public static class CustomCorsFilterConfig {

        @Bean
        public CustomCorsFilter customCorsFilter(CorsPropertiesList corsPropertiesList) {
            System.out.println("init bean CustomCorsFilter with " + corsPropertiesList);
            return new CustomCorsFilter(corsPropertiesList);
        }
    }
}

因为 @CrossOrigin 并非通过Filter进行的处理,这里的 CustomCorsFilter 仅仅做添加Header的操作,如果没有校验成功,不回结束FilterChain,因而可以和 @CrossOrigin 一起使用。

拓展

获取第三方平台数据

如果想要获取第三方平台的数据,可以采用服务器代理的方式进行处理。因为直接干涉第三平台的服务器的配置,而且同源策略也只有在浏览器中有效。因而可以将自己的服务器访问第三方平台的数据再返回给自己的客户端。


以上所述就是小编给大家介绍的《SpringBoot解决CORS问题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

如何不在网上虚度人生

如何不在网上虚度人生

[美] 肯尼思·戈德史密斯 / 刘畅 / 北京联合出版公司 / 2017-9 / 39.80元

我们平时上网多大程度上是浪费时间,多大程度是在学习、关心社会、激发创造力?我们真能彻底断网,逃离社交网络吗? 手机把都市人变成一群电子僵尸,是福是祸? 浏览记录就是我们将来的回忆录吗?文件归档属于一种现代民间艺术? 不自拍、P图、发朋友圈,我还是我吗? 美国知名概念艺术家戈德史密斯认为:上网绝不是浪费时间,而是一种创造性的活动。在本书中他以跨学科角度、散文式语言进行论证,涉及大众传播学、计算......一起来看看 《如何不在网上虚度人生》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器