从前端、后台的角度谈论跨域
什么是跨域
- 浏览器存在着域这个概念,这个概念是为了安全而定义的,理论上来讲,为了保证请求是安全的,应该采用同域部署的方式,但是随着互联网发展,大型的分布式架构出现之后,同域从某种层面上来讲的拓展性不好,也不方便测试,因此,有了跨域这个问题。
- 浏览器对跨域默认是不允许的,因为不可能说你拿到JS、页面等代码之后,就可以代替一个网站发送请求,这样是不安全的。就例如:网站的地址为www.test.com,而你拿到页面代码之后,部署到的网站为www.fake.com,然后通过www.fake.com进行请求后台,这样子默认情况下是不被允许的。
- 跨域的三种条件(任一即可)
- 跨协议,如HTTP与HTTPS
- 跨域名,www.A.com与www.B.com
- 跨端口,从443到80
以上比较通俗讲解了跨域,如果有兴趣了解详细,可以去WIKI上看看。
跨域带来的常见问题
在过去的时间里,有师弟师妹们遇到这些问题,但是看他们处理起来比较乱;哪怕是我身边的朋友,遇到跨域有时候也会有点束手无措,在我看来,原因无非两个:要么对跨域理解不透,要么就是后台对前端的理解不够,前端对后台理解也不深,导致前端以为是后台问题,后台以为是前端问题。
好了,讲了这么多,就来讲讲有哪些常见问题:
1、403禁止访问
-
问题定位
- 后台没有允许跨域,因此请求会禁止
-
解决
- 后台开启允许跨域,以SpringMVC来讲,在控制器上添加如下注解即可
@CrossOrigin//允许所有的域
2、使用Cookie(session)丢失
在前后端分离之后,后台采用session进行身份验证的情况居多,因此,这个问题也是比较常见的。
-
问题定位
- 后台是否有指定特定域,并且允许Cookie
- 前端是否有发送跨域请求,并且携带Cookie
-
解决
- 后台需要指定特定的域,并且允许Cookie
- 如果只是允许Cookie会无效,因为必须在指定origins之后才能响应Cookie
@CrossOrigin(origins = "http://localhost:10987", allowCredentials = "true", maxAge = 3600L) @RestController //origins指定特定域,allowCredentials允许Cookie public class AllowCORSController { @GetMapping("/test") public String login(HttpServletRequest request) { request.getSession().setAttribute("User", "OK"); return "login success!"; } @GetMapping("/op") public String ops(HttpServletRequest request){ if (request.getSession().getAttribute("User")!=null){ return "OK"; }else { return "ERROR"; } } }
-
前端解决
- Ajax
$('#'+'op')[0].onclick=(event)=>{ $.ajax({ url: 'http://'+ 'localhost' +':10086/op', type: 'get', data: JSON.stringify(null), dataType: 'json', crossDomain: true, //添加以下这个部分 xhrFields: { withCredentials: true }, processData: false, contentType: 'application/json', success: function(responseObj) { console.log($.cookie('access_token')); showMessage(responseObj); }, error: function() { // 请求失败时要干什么 //showMessage('请求失败'); } }); }
- 如果在添加上述部分之后也无效,那么,就应该获取后台响应头的set-Cookie,然后再下一次请求,添加到请求头,Cookie中去。(针对小程序)
从原理上解析
- 后台
以SpringBoot为例,跨域的解决方式就是添加@CrossOrigin注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
@AliasFor("origins")
String[] value() default {};
//指定域
@AliasFor("value")
String[] origins() default {};
//允许的头部
String[] allowedHeaders() default {};
//暴露头部
String[] exposedHeaders() default {};
//跨域允许的方法
RequestMethod[] methods() default {};
//是否允许Cookie
String allowCredentials() default "";
//Cookie存留在服务端的时间,如果为-1,即浏览器关闭即销毁
long maxAge() default -1;
}
读了以上源码之后,其实还挺简单的,那么,接下来就直接进入重头戏,看看SpringMVC是怎么处理跨域的。
public class DefaultCorsProcessor implements CorsProcessor {
private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
//对请求进行处理,config其实就是我们上面配置的@CrossOrigin注解。
//还是Servlet架构
@Override
@SuppressWarnings("resource")
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
if (!CorsUtils.isCorsRequest(request)) {
//如果不是跨域,即跳过;判断代码实现:
//其实判断是否跨域只需要判断是否具有origin请求头部
//public static boolean isCorsRequest(HttpServletRequest request) {
//return (request.getHeader(HttpHeaders.ORIGIN) != null);
// }
return true;
}
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
if (responseHasCors(serverResponse)) {
//判断是否服务器有响应给浏览器说允许跨域
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
//判断是否是与指定域相同的域
if (WebUtils.isSameOrigin(serverRequest)) {
logger.trace("Skip: request is from same origin");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(serverResponse);
return false;
}
else {
return true;
}
}
return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
}
private boolean responseHasCors(ServerHttpResponse response) {
try {
return (response.getHeaders().getAccessControlAllowOrigin() != null);
}
catch (NullPointerException npe) {
// SPR-11919 and https://issues.jboss.org/browse/WFLY-3474
return false;
}
}
/**
* Invoked when one of the CORS checks failed.
* The default implementation sets the response status to 403 and writes
* "Invalid CORS request" to the response.
*/
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);//返回403,不允许跨域
response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
}
/**
* 这是跨域处理的核心
*/
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean preFlightRequest) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
HttpHeaders responseHeaders = response.getHeaders();
//这个其实就是处理Options请求。
responseHeaders.addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
//这个地方就是withAllow...参数。
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
/**
* Check the origin and determine the origin for the response. The default
* implementation simply delegates to
* {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
*/
@Nullable
protected String checkOrigin(CorsConfiguration config, @Nullable String requestOrigin) {
return config.checkOrigin(requestOrigin);
}
/**
* Check the HTTP method and determine the methods for the response of a
* pre-flight request. The default implementation simply delegates to
* {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
*/
@Nullable
protected List<HttpMethod> checkMethods(CorsConfiguration config, @Nullable HttpMethod requestMethod) {
return config.checkHttpMethod(requestMethod);
}
@Nullable
private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) {
return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod());
}
/**
* Check the headers and determine the headers for the response of a
* pre-flight request. The default implementation simply delegates to
* {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
*/
@Nullable
protected List<String> checkHeaders(CorsConfiguration config, List<String> requestHeaders) {
return config.checkHeaders(requestHeaders);
}
private List<String> getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) {
HttpHeaders headers = request.getHeaders();
return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.keySet()));
}
}
以上就是后台处理跨域的核心逻辑。
那么前端呢?前端的话是比较简单的,因为大多数东西由浏览器承包了,因此,以下讲以下过程。
- 第一次请求,满足后台的跨域指定条件,会返回set-Cookie响应头
- 前端如果允许了跨域,那么下次请求就会带上Cookie请求头,其中包含SessionID