前后端分离的系统,如果“前端页面的域名”与“后端接口的域名”不相同(即不同的源),前端页面通过ajax调用后端接口时,就会发生跨域问题。如果“同源”则不会有跨域问题,“同源” 是指协议(https/http)、域名和端口都相同。
(1)浏览器会给跨域的ajax请求自动设置Origin请求头,这个请求头的值就是当前页面的完整域名,包括协议(https或http)、域名和端口。比如在前端页面为 https://www.front.com/index.html ,则:origin=https://www.front.com
(2)请求完成之后,浏览器会取响应头Access-Control-Allow-Origin的值(这个值由后端设置),与当前域名(即请求头中的Origin)做对比,如果发现不相等,则拒绝将服务端返回的数据给到ajax。实际上跨域时服务端是没有阻止你的,只是浏览器拿到服务端返回的数据后不把数据给你,你用抓包工具是可以看到服务端返回的数据。
(1)根据上述原理,是否允许跨域就取决于服务端响应头中的Access-Control-Allow-Origin值,只要把这个值设置为前端的完整域名即可,即Access-Control-Allow-Origin=https://www.front.com 。
(2)由于Access-Control-Allow-Origin请求头只能设置一个值,如果有多个前端域名怎么解决呢? 动态设置Access-Control-Allow-Origin即可,你根据请求头中的origin来设置,当然不是每一个origin都设置进去,这样就没有安全性可言了,服务端应该有一个域名的白名单列表,如果发现请求头的origin与白名单匹配则设置到Access-Control-Allow-Origin响应头中。
这种不属于业务层面的功能,可以通过Spring提供的拦截器统一处理。
(1)配置文件 application.properties
# 考虑到可能有三级域名,这里使用通配符“*”号 ,多个用英文逗号“,”分隔 config.allowOrigins=*.abc.com,*.xyz.com(2)拦截器 CorsInterceptor .java
import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; @Slf4j @Component public class CorsInterceptor implements HandlerInterceptor { //从上述配置文件中读出allowOrigins @Value("${config.allowOrigins}") private String allowOrigins; //正则匹配符集合 private Set<Pattern> allowOriginPatterns; //根据配置初始化白名单的正则表达式 @PostConstruct private void init(){ allowOriginPatterns =new HashSet<>(); log.debug("allowOrigins = {}", allowOrigins); if(StringUtils.isBlank(allowOrigins)){ return; } String[] origins=allowOrigins.split(","); for (String origin : origins) { if(StringUtils.isBlank(origin)){ continue; } //将开头第一个星号*替换为.*,将所有的点号配置为\.,方便做正则表达式匹配 //由于在正在表达式中“.”和“*”都是特殊字符,因此需要转义 origin=origin.trim().replace("\\.","\\\\.").replace("*",".*"); allowOriginPatterns.add(Pattern.compile(origin)); } log.debug("allowOriginPatterns = {}",allowOriginPatterns); } /** * 返回true则会继续执行拦截器链中的后续拦截器, 否则不往后执行后续拦截器。 * * 详细说明: * 在业务处理器Ccontroller处理请求之前被调用。 * * (1)按拦截器链中的顺序执行所有拦截器的preHandle()方法,直到所有拦截器执行完为止(或者到该方法返回false的拦截器为止); * (2)然后执行被拦截的Controller。 * (3)往回执行所有已执行过preHandle()方法的拦截器的postHandle()方法,与第(1)步中的执行方向相反。 * (4)渲染ModelView(如果Controller返回ModelView,比如jsp页面),前后端分离的忽略该步骤。 * (5)往回执行所有已执行过postHandle()方法的拦截器的afterCompletion()方法,与第(1)步中的执行方向相反。 * */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){ log.debug(" cors processing begin "); //获取请求头中的 origin String headerOrigin=request.getHeader("Origin"); log.debug("request url : {} , header origin : {}",request.getRequestURL(),headerOrigin); if(StringUtils.isBlank(headerOrigin)){ return true; } for (Pattern pattern : allowOriginPatterns) { //白名单匹配 if(pattern.matcher(headerOrigin).matches()){ log.debug("set '{}' to 'Access-Control-Allow-Origin' for response header ",headerOrigin); //允许跨域配置:http://www.ruanyifeng.com/blog/2016/04/cors.html //Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。 response.setHeader("Access-Control-Allow-Origin", headerOrigin); response.setHeader("Access-Control-Allow-Methods", "GET"); //Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。 response.setHeader("Access-Control-Allow-Credentials","true"); //response.setHeader("Access-Control-Max-Age", "3600"); //response.setHeader("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept"); break; } } log.debug(" cors processing end "); return true; } /** * 在业务处理器处理请求执行完成后,生成视图之前执行的动作 * @param request * @param response * @param handler * @param modelAndView */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.debug("postHandle()"); } /** * * 在DispatcherServlet完全处理完请求后被调用, * 会从当前拦截器往回执行所有的拦截器的afterCompletion() * * @param request * * @param response * * @param handler * */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { log.debug("afterCompletion()"); } }(3)配置拦截器 MyWebMvcConfig.java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class MyWebMvcConfig implements WebMvcConfigurer { @Autowired private CorsInterceptor corsInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //将CorsInterceptor拦截器添加进来 registry.addInterceptor(corsInterceptor).addPathPatterns("/**"); } }跨域资源共享 CORS 详解
更多文章请关注公众号 薛定谔的雄猫