背景
最近有个应用被检测发现有个缺陷,使用 @CrossOrigin
的地方用的都是默认选项(即 origin="*"
)—— 允许任何网站进行跨域访问。为了避免可能存在的安全隐患,师兄说 “之叶,你把这个问题解决一下,只允许部分网站的跨域”。
现状
我们都知道,@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 支持一种限定范围内的通配方式,例如正则表达式。
设计
源码之旅
首先我们得找到 SpringMVC 处理 @CrossOrigin 的源头,所以我们先来看下 @CrossOrigin 的源码(注释):
/**
* Marks the annotated method or type as permitting cross origin requests.
*
* 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}.
*
*
NOTE: {@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
:
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered {
...
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
...
}
给了一个默认的实现 DefaultCorsProcessor
。DefaultCorsProcessor
代码看起来比较简单,即先判断是否为跨域请求,是的话再调用 checkOrigin
方法来对请求进行校验,而 checkOrigin
方法之间委托给了 CorsConfiguration
的 checkOrigin
方法:
protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
return config.checkOrigin(requestOrigin);
}
查看 CorsConfiguration
的代码 —— 可知我们使用 @CrossOrigin
时配置的那些属性,都映射为了 CorsConfiguration
。CorsConfiguration
的 allowedOrigins
属性,就是在 @CrssOrigin
中配置的 origin
。
方案
明白了 Spring 执行跨域访问请求的流程,我们也就可以比较容易的设计出让 @CrossOrigin
支持正则表达式的方案了:
- 自定义
CorsProcessor
,覆写checkOrigin
方法,支持使用正则的方式来过滤请求源 - 自定义
RequestMappingHandlerMapping
,设置CorsProcessor
为我们自定义的CorsProcessor
- 使用自定义的
RequestMappingHandlerMapping
,替换 SpringMVC 默认的RequestMappingHandlerMapping
实现
自定义 CorsProcessor
首先我们实现用 用正则的方式来校验请求源 的 CorsProcessor
,我们就叫它 RegexCorsProcessor
吧~
/**
* 自定义跨域处理器,使用正则的方式来校验请求源是否和 @CrossOrigin 中指定的源匹配
*/
public class RegexCorsProcessor extends DefaultCorsProcessor {
private static final Map 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 allowedOrigins = config.getAllowedOrigins();
if (CollectionUtils.isEmpty(allowedOrigins)) {
return null;
}
return checkOriginWithRegex(allowedOrigins, requestOrigin);
}
/**
* 用正则的方式来校验 requestOrigin
*/
private String checkOriginWithRegex(List 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
这个就更简单啦,因为我们只是想要替换 RequestMappingHandlerMapping
中 CorsProcessor
的实现:
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 testCors() {
Map 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 请求:
Cors 测试
先使用 http://mizhou.com
来访问,那么当前的网站的网址便是 http://mizhou.com
,而 AJAX 请求的网址为 http://local.com
—— 显然,跨域失败(可以看到同源策略限制了该跨域访问):
同理,再使用 http://zhiye.com
和 http://abc.zhiye.com
来进行跨域访问:
因为 @CrossOrigin
设置的正则表达式和请求源匹配,所以都是跨域成功 —— 大功告成~
扩展
使用正则来进行网址的匹配还是有点奇怪了,可能是因为大家平时写配置文件时候用的都是 Ant 风格的路径匹配规则 —— 所以我们可以创建一个 AntPathCorsProcessor
,然后在自定义的 RequestMappingHandlerMapping 做个替换,从而让 @CrossOrigin
实现 Ant 风格的路径匹配。当然,我今天很懒,所以这个扩展留给感兴趣的你吧。