@CrossOrigin 支持正則表達式

背景

最近有個應用被檢測發現有個缺陷,使用 @CrossOrigin 的地方用的都是默認選項(即 origin="*")—— 容許任何網站進行跨域訪問。爲了不可能存在的安全隱患,師兄說 「之葉,你把這個問題解決一下,只容許部分網站的跨域」。javascript

現狀

咱們都知道,@CrossOriginorigin 屬性是能夠自定義的,並且是個數組,意味着能夠本身寫多個域名,好比設置爲 @CrossOrigin(origin={"https://zhiye.com", "https://mizhou.com"}),那麼當 https://zhiye.comhttps://mizhou.com 對當前網站發起跨域請求時,都會被經過。當前咱們如今遇到的問題在於,若是存在二級域名,例如 https://abc.zhiye.comhttps://xyz.zhiye.com,這是不可枚舉的,因此一個一個寫在 origin 的數組裏面並不現實。於是,咱們須要 @CrossOrigin 支持一種限定範圍內的通配方式,例如正則表達式。
必須安排html

設計

源碼之旅

首先咱們得找到 SpringMVC 處理 @CrossOrigin 的源頭,因此咱們先來看下 @CrossOrigin 的源碼(註釋):java

/**
 * Marks the annotated method or type as permitting cross origin requests.
 *
 * <p>By default all origins and headers are permitted, credentials are allowed,
 * and the maximum age is set to 1800 seconds (30 minutes). The list of HTTP
 * methods is set to the methods on the {@code @RequestMapping} if not
 * explicitly set on {@code @CrossOrigin}.
 *
 * <p><b>NOTE:</b> {@code @CrossOrigin} is processed if an appropriate
 * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the
 * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter}
 * pair which are the default in the MVC Java config and the MVC namespace.
 * In particular {@code @CrossOrigin} is not supported with the
 * {@code DefaultAnnotationHandlerMapping}-{@code AnnotationMethodHandlerAdapter}
 * pair both of which are also deprecated.
 *
 * @author Russell Allen
 * @author Sebastien Deleuze
 * @author Sam Brannen
 * @since 4.2
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
  ...
}

理解一下,@CrossOrigin 會被 SpringMVC 配置的某個合適的 HandleMapping-HandlerAdapter 來處理(HandleMapping 用來根據請求找到對應的 HandlerAdapter ,而 HandleAdapter 是用來處理請求的),而後註釋就繼續說了,當前版本的 Spring MVC 默認配置的 HandleMappingRequestMappingHandlerMapping。因此能夠推測,對於 @CrossOrigin 的處理,就在 RequestMappingHandlerMapping 當中。查看 RequestMappingHandlerMapping 的源碼,果真,在其父類 AbstractHandlerMapping 當中,發現了用於跨域處理的 CorsProcessorjquery

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered {
  ...
    
    private CorsProcessor corsProcessor = new DefaultCorsProcessor();
  
  ...
}

給了一個默認的實現 DefaultCorsProcessorDefaultCorsProcessor 代碼看起來比較簡單,即先判斷是否爲跨域請求,是的話再調用 checkOrigin 方法來對請求進行校驗,而 checkOrigin 方法之間委託給了 CorsConfigurationcheckOrigin 方法:ajax

protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
    return config.checkOrigin(requestOrigin);
}

查看 CorsConfiguration 的代碼 —— 可知咱們使用 @CrossOrigin 時配置的那些屬性,都映射爲了 CorsConfigurationCorsConfigurationallowedOrigins 屬性,就是在 @CrssOrigin 中配置的 origin正則表達式

方案

明白了 Spring 執行跨域訪問請求的流程,咱們也就能夠比較容易的設計出讓 @CrossOrigin 支持正則表達式的方案了:json

  1. 自定義 CorsProcessor,覆寫 checkOrigin 方法,支持使用正則的方式來過濾請求源
  2. 自定義 RequestMappingHandlerMapping,設置 CorsProcessor 爲咱們自定義的 CorsProcessor
  3. 使用自定義的 RequestMappingHandlerMapping,替換 SpringMVC 默認的 RequestMappingHandlerMapping

實現

自定義 CorsProcessor

首先咱們實現用 用正則的方式來校驗請求源CorsProcessor,咱們就叫它 RegexCorsProcessor 吧~跨域

/**
 * 自定義跨域處理器,使用正則的方式來校驗請求源是否和 @CrossOrigin 中指定的源匹配
 */
public class RegexCorsProcessor extends DefaultCorsProcessor {

    private static final Map<String, Pattern> PATTERN_MAP = new ConcurrentHashMap<>(1);

    /**
     * 跨域請求,會經過此方法檢測請求源是否被容許
     *
     * @param config        CORS 配置
     * @param requestOrigin 請求源
     * @return 若是請求源被容許,返回請求源;不然返回 null
     */
    @Override
    protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
        // 先調用父類的 checkOrigin 方法,保證原來的方式繼續支持
        String result = super.checkOrigin(config, requestOrigin);
        if (result != null) {
            return result;
        }
      
        // 獲取 @CrossOrigin 中配置的 origins
        List<String> allowedOrigins = config.getAllowedOrigins();
        if (CollectionUtils.isEmpty(allowedOrigins)) {
            return null;
        }

        return checkOriginWithRegex(allowedOrigins, requestOrigin);
    }

    /**
     * 用正則的方式來校驗 requestOrigin
     */
    private String checkOriginWithRegex(List<String> allowedOrigins, String requestOrigin) {
        for (String allowedOrigin : allowedOrigins) {
            Pattern pattern = PATTERN_MAP.computeIfAbsent(allowedOrigin, Pattern::compile);

            if (pattern.matcher(requestOrigin).matches()) {
                return requestOrigin;
            }
        }

        return null;
    }
}

邏輯很簡單,重點在於 checkOriginWithRegex 方法:遍歷 allowedOrigins,而後使用正則的方式來對請求源進行校驗 —— 校驗經過,返回請求源;不然返回 null數組

PATTERN_MAP 的做用在於對正則表達式產生的 Pattern 作一個緩存,由於 Pattern 是一個建立代價較高的對象,每次請求都新建一個 Pattern 會下降效率和加劇 GC 負擔。緩存

自定義 RequestMappingHandlerMapping

這個就更簡單啦,由於咱們只是想要替換 RequestMappingHandlerMappingCorsProcessor 的實現:

public final class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    public CustomRequestMappingHandlerMapping() {
        // 自定義 CORS 跨域處理器
        setCorsProcessor(new RegexCorsProcessor());
    }
}

註冊自定義的 RequestMappingHandlerMapping

經過實現 WebMvcRegistrations 接口,咱們能夠完成 RequestMappingHandlerMapping 的自定義。一如既往的,Spring 爲這個接口提供了一個適配類,WebMvcRegistrationsAdapter,因此咱們只須要繼承這個 WebMvcRegistrationsAdapter 便可:

/**
 * 自定義 WebMvcConfiguration
 */
@Configuration
public class CustomWebMvcConfig extends WebMvcRegistrationsAdapter {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new CustomRequestMappingHandlerMapping();
    }
}

經過繼承 WebMvcRegistrationsAdapter 並覆寫 getRequestMappingHandlerMapping 方法,咱們便完成了自定義

RequestMappingHandlerMapping 的功能。

測試

離大功告成還差一步測試啦 —— 因此先讓咱們來設置幾個測試使用的 host:

127.0.0.1  local.com
127.0.0.1  mizhou.com 

127.0.0.1  zhiye.com 
127.0.0.1  abc.zhiye.com

而後寫個測試的 Controller

@RestController
public class TestController {

    @GetMapping("cors")
    public Map<String, Integer> testCors() {
        Map<String, Integer> map = new LinkedHashMap<>(4);
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        return map;
    }
}

打上 @CrossOrigin 註解:

@RestController
@CrossOrigin(origins = "http(s)?://([-\\w]+\\.)*zhiye\\.com")
public class TestController {
  ...
}

這個正則表示支持 http://zhiye.com 及其全部的二級域名進行跨域訪問。

最後寫個簡單的 AJAX 請求:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Cors 測試</title>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
<button onclick="testCors()">測試 Cors</button>
</body>
<script type="text/javascript">
    function testCors(){
        $.ajax({
            url:"http://local.com/cors",
            type:"get",
            dataType:"json",
            success:function(data) {
                console.log(data);
            },
            error:function(){
                alert('訪問出錯');
            }
        });
    }
</script>
</html>

先使用 http://mizhou.com 來訪問,那麼當前的網站的網址即是 http://mizhou.com,而 AJAX 請求的網址爲 http://local.com —— 顯然,跨域失敗(能夠看到同源策略限制了該跨域訪問):

跨域失敗

同理,再使用 http://zhiye.comhttp://abc.zhiye.com 來進行跨域訪問:

請求源爲 http://zhiye.com
http://zhiye.com

請求源爲 http://abc.zhiye.com
http://abc.zhiye.com

由於 @CrossOrigin 設置的正則表達式和請求源匹配,因此都是跨域成功 —— 大功告成~

齋藤飛鳥1.gif

擴展

使用正則來進行網址的匹配仍是有點奇怪了,多是由於你們平時寫配置文件時候用的都是 Ant 風格的路徑匹配規則 —— 因此咱們能夠建立一個 AntPathCorsProcessor,而後在自定義的 RequestMappingHandlerMapping 作個替換,從而讓 @CrossOrigin 實現 Ant 風格的路徑匹配。固然,我今天很懶,因此這個擴展留給感興趣的你吧。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息