1. 程式人生 > 程式設計 >Spring @CrossOrigin 註解原理實現

Spring @CrossOrigin 註解原理實現

現實開發中,我們難免遇到跨域問題,以前筆者只知道jsonp這種解決方式,後面聽說spring只要加入@CrossOrigin即可解決跨域問題。本著好奇的心裡,筆者看了下@CrossOrigin 作用原理,寫下這篇部落格。

先說原理:其實很簡單,就是利用spring的攔截器實現往response裡新增 Access-Control-Allow-Origin等響應頭資訊,我們可以看下spring是怎麼做的

注:這裡使用的spring版本為5.0.6

我們可以先往RequestMappingHandlerMapping 的initCorsConfiguration方法打一個斷點,發現方法呼叫情況如下

Spring @CrossOrigin 註解原理實現

如果controller在類上標了@CrossOrigin或在方法上標了@CrossOrigin註解,則spring 在記錄mapper對映時會記錄對應跨域請求對映,程式碼如下

RequestMappingHandlerMapping
protected CorsConfiguration initCorsConfiguration(Object handler,Method method,RequestMappingInfo mappingInfo) {
 HandlerMethod handlerMethod = createHandlerMethod(handler,method);
 Class<?> beanType = handlerMethod.getBeanType();
    //獲取handler上的CrossOrigin 註解
 CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType,CrossOrigin.class);
    //獲取handler 方法上的CrossOrigin 註解
 CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method,CrossOrigin.class);
    
 if (typeAnnotation == null && methodAnnotation == null) {
      //如果類上和方法都沒標CrossOrigin 註解,則返回一個null
  return null;
 }
    //構建一個CorsConfiguration 並返回
 CorsConfiguration config = new CorsConfiguration();
 updateCorsConfig(config,typeAnnotation);
 updateCorsConfig(config,methodAnnotation);

 if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
  for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
  config.addAllowedMethod(allowedMethod.name());
  }
 }
 return config.applyPermitDefaultValues();
 }

將結果返回到了AbstractHandlerMethodMapping#register,主要程式碼如下

 CorsConfiguration corsConfig = initCorsConfiguration(handler,method,mapping);
  if (corsConfig != null) {
//會儲存handlerMethod處理跨域請求的配置
   this.corsLookup.put(handlerMethod,corsConfig);
  }

當一個跨域請求過來時,spring在獲取handler時會判斷這個請求是否是一個跨域請求,如果是,則會返回一個可以處理跨域的handler

AbstractHandlerMapping#getHandler 
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler,request);
 //如果是一個跨域請求
if (CorsUtils.isCorsRequest(request)) {
    //拿到跨域的全域性配置
  CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
     //拿到hander的跨域配置
  CorsConfiguration handlerConfig = getCorsConfiguration(handler,request);
  CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
      //處理跨域(即往響應頭新增Access-Control-Allow-Origin資訊等),並返回對應的handler物件
  executionChain = getCorsHandlerExecutionChain(request,executionChain,config);
 }

我們可以看下如何判定一個請求是一個跨域請求,

public static boolean isCorsRequest(HttpServletRequest request) {
//判定請求頭是否有Origin 屬性即可
 return (request.getHeader(HttpHeaders.ORIGIN) != null);
 }

再看下getCorsHandlerExecutionChain 是如何獲取一個handler

 protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,HandlerExecutionChain chain,@Nullable CorsConfiguration config) {

 if (CorsUtils.isPreFlightRequest(request)) {
  HandlerInterceptor[] interceptors = chain.getInterceptors();
  chain = new HandlerExecutionChain(new PreFlightHandler(config),interceptors);
 }
 else {
      //只是給執行器鏈添加了一個攔截器
  chain.addInterceptor(new CorsInterceptor(config));
 }
 return chain;
 }

也就是在呼叫目標方法前會先呼叫CorsInterceptor#preHandle,我們觀察得到其也是呼叫了corsProcessor.processRequest方法,我們往這裡打個斷點

processRequest方法的主要邏輯如下

 public boolean processRequest(@Nullable CorsConfiguration config,HttpServletRequest request,HttpServletResponse response) throws IOException {
    //....
    //呼叫了自身的handleInternal方法
 return handleInternal(serverRequest,serverResponse,config,preFlightRequest);
 }


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();

 responseHeaders.addAll(HttpHeaders.VARY,Arrays.asList(HttpHeaders.ORIGIN,HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD,HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));

 if (allowOrigin == null) {
  logger.debug("Rejecting CORS request because '" + 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("Rejecting CORS request because '" + requestMethod + "' request method 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("Rejecting CORS request because '" + requestHeaders + "' request headers 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())) {
  responseHeaders.setAccessControlAllowCredentials(true);
 }

 if (preFlightRequest && config.getMaxAge() != null) {
  responseHeaders.setAccessControlMaxAge(config.getMaxAge());
 }
    //重新整理
 response.flush();
 return true;
 }

至此@CrossOrigin的使命就完成了,說白了就是用攔截器給response新增響應頭資訊而已

到此這篇關於Spring @CrossOrigin 註解原理實現的文章就介紹到這了,更多相關Spring @CrossOrigin 註解內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!