那就谈谈跨域问题

从前端、后台的角度谈论跨域

什么是跨域

  • 浏览器存在着域这个概念,这个概念是为了安全而定义的,理论上来讲,为了保证请求是安全的,应该采用同域部署的方式,但是随着互联网发展,大型的分布式架构出现之后,同域从某种层面上来讲的拓展性不好,也不方便测试,因此,有了跨域这个问题。
  • 浏览器对跨域默认是不允许的,因为不可能说你拿到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
发布了57 篇原创文章 · 获赞 32 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/rekingman/article/details/100389516