在作先後端分離的開發或者前端調用第三方平臺的接口時常常會遇到跨域的問題,前端老是但願可以經過各類方法解決跨域的問題。但事實上跨域問題是安全問題。這篇文章將會講解一些爲何會有跨域問題,並提供一個方便的解決方法。爲了閱讀的流暢,相關的參考連接均會在文章末尾給出。本文使用的springboot版本爲2.1.6.RELEASE
,相應的spring版本爲5.1.8.RELEASE
。html
跨域問題的產生是由於瀏覽器的同源策略。同源策略將協議+域名+端口
構成的三元做爲一個總體,只有三者均相同的狀況下才屬於一個源。跨域問題也就是不一樣源之間訪問致使的問題。前端
同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。瀏覽器的同源策略 @developer.mozilla.orgjava
下表給出了相對http://store.company.com/dir/page.html
同源檢測的示例:git
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 ) |
對於跨域的請求,服務器能夠接受到請求,但瀏覽器不會出來請求的返回結果。web
在瀏覽器中打開本地的一個html文件,在客戶端中輸入一下的內容能夠模擬跨域的請求。(爲了防止由於https的限制而沒法發送請求,能夠本身啓動一個前端服務,而後再進行試驗。)spring
var xhttp = new XMLHttpRequest(); xhttp.open("GET", "http://192.168.20.185:8080/users/12345678", true); xhttp.send();
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。segmentfault
它容許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。後端
-- 前端的輔助配置api
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 | 預檢請求有效時長,單位爲秒 |
*
經過過濾器處理請求,對origin進行判斷,並添加必要的Headers。跨域
範圍:單個類
單個Path
@RestController @RequestMapping("/account") public class AccountController { @CrossOrigin @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // ... } }
可類級別配置,也可方法級別配置。默認:
@CrossOrigin
支持各個值的配置。@CrossOrigin
雖然提供了簡單的配置,但須要重複爲不一樣的類和方法進行配置,重複麻煩。若是對於某些請求有特定的配置須要可使用。
註解@CrossOrigin
會成爲CorsConfiguration
的一部分,可與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的任意端口的檢驗。
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。
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來處理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
一塊兒使用。
若是想要獲取第三方平臺的數據,能夠採用服務器代理的方式進行處理。由於直接干涉第三平臺的服務器的配置,並且同源策略也只有在瀏覽器中有效。於是能夠將本身的服務器訪問第三方平臺的數據再返回給本身的客戶端。
源碼見:tutorial/cors