最近有個應用被檢測發現有個缺陷,使用 @CrossOrigin
的地方用的都是默認選項(即 origin="*"
)—— 容許任何網站進行跨域訪問。爲了不可能存在的安全隱患,師兄說 「之葉,你把這個問題解決一下,只容許部分網站的跨域」。javascript
咱們都知道,@CrossOrigin
的 origin
屬性是能夠自定義的,並且是個數組,意味着能夠本身寫多個域名,好比設置爲 @CrossOrigin(origin={"https://zhiye.com", "https://mizhou.com"})
,那麼當 https://zhiye.com
和 https://mizhou.com
對當前網站發起跨域請求時,都會被經過。當前咱們如今遇到的問題在於,若是存在二級域名,例如 https://abc.zhiye.com
、 https://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 默認配置的 HandleMapping
是 RequestMappingHandlerMapping
。因此能夠推測,對於 @CrossOrigin
的處理,就在 RequestMappingHandlerMapping
當中。查看 RequestMappingHandlerMapping
的源碼,果真,在其父類 AbstractHandlerMapping
當中,發現了用於跨域處理的 CorsProcessor
:jquery
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered { ... private CorsProcessor corsProcessor = new DefaultCorsProcessor(); ... }
給了一個默認的實現 DefaultCorsProcessor
。DefaultCorsProcessor
代碼看起來比較簡單,即先判斷是否爲跨域請求,是的話再調用 checkOrigin
方法來對請求進行校驗,而 checkOrigin
方法之間委託給了 CorsConfiguration
的 checkOrigin
方法:ajax
protected String checkOrigin(CorsConfiguration config, String requestOrigin) { return config.checkOrigin(requestOrigin); }
查看 CorsConfiguration
的代碼 —— 可知咱們使用 @CrossOrigin
時配置的那些屬性,都映射爲了 CorsConfiguration
。CorsConfiguration
的 allowedOrigins
屬性,就是在 @CrssOrigin
中配置的 origin
。正則表達式
明白了 Spring 執行跨域訪問請求的流程,咱們也就能夠比較容易的設計出讓 @CrossOrigin
支持正則表達式的方案了:json
CorsProcessor
,覆寫 checkOrigin
方法,支持使用正則的方式來過濾請求源RequestMappingHandlerMapping
,設置 CorsProcessor
爲咱們自定義的 CorsProcessor
RequestMappingHandlerMapping
,替換 SpringMVC 默認的 RequestMappingHandlerMapping
首先咱們實現用 用正則的方式來校驗請求源 的 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
中 CorsProcessor
的實現:
public final class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { public CustomRequestMappingHandlerMapping() { // 自定義 CORS 跨域處理器 setCorsProcessor(new RegexCorsProcessor()); } }
經過實現 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.com
和 http://abc.zhiye.com
來進行跨域訪問:
請求源爲 http://zhiye.com
:
請求源爲 http://abc.zhiye.com
:
由於 @CrossOrigin
設置的正則表達式和請求源匹配,因此都是跨域成功 —— 大功告成~
使用正則來進行網址的匹配仍是有點奇怪了,多是由於你們平時寫配置文件時候用的都是 Ant 風格的路徑匹配規則 —— 因此咱們能夠建立一個 AntPathCorsProcessor
,而後在自定義的 RequestMappingHandlerMapping 作個替換,從而讓 @CrossOrigin
實現 Ant 風格的路徑匹配。固然,我今天很懶,因此這個擴展留給感興趣的你吧。