前后端分离的系统,如果“前端页面的域名”与“后端接口的域名”不相同(即不同的源),前端页面通过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 详解