写点什么

SpringBoot 解决 CORS 问题

用户头像
DoneSpeak
关注
发布于: 1 小时前
SpringBoot解决CORS问题

写在前面的话

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

跨域问题的产生

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


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

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


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



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


在浏览器中打开本地的一个 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 只能同源使用的限制。


-- 前端的辅助配置



  • 当 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@EnableWebMvcpublic 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@Configurationpublic 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;
}
复制代码


@Datapublic 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: corsFilterweb.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

@Beanpublic 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@Configurationpublic 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;
...
@Slf4jpublic 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@Configurationpublic 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一起使用。

拓展

获取第三方平台数据

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

参考和其他


源码见:tutorial/cors

发布于: 1 小时前阅读数: 3
用户头像

DoneSpeak

关注

Let the Work That I've Done Speak for Me 2018.05.10 加入

Java后端开发

评论

发布
暂无评论
SpringBoot解决CORS问题