最近有个应用被检测发现有个缺陷,使用 @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 风格的路径匹配。固然,我今天很懒,因此这个扩展留给感兴趣的你吧。