@CrossOrigin 支持正则表达式

背景

最近有个应用被检测发现有个缺陷,使用 @CrossOrigin 的地方用的都是默认选项(即 origins="*")—— 允许任何网站进行跨域访问。为了避免可能存在的安全隐患,师兄说 “之叶,你把这个问题解决一下,只允许部分网站的跨域”。

现状

我们都知道,@CrossOriginorigins 属性是可以自定义的,而且是个数组,意味着我们可以写多个域名,比如设置为 @CrossOrigin(origins={"https://zhiye.com", "https://mizhou.com"}),那么当 https://zhiye.comhttps://mizhou.com 对当前网站发起跨域请求时,都会被通过。
然鹅现在遇到的问题在于,我们要支持 https://zhiye.com 所有的二级域名都能够跨域访问(例如 https://abc.zhiye.comhttps://xyz.zhiye.com),可这是不可枚举的,因而一个一个写在 origins 的数组里面并不现实。所以,我们需要 @CrossOrigin 支持一种限定范围内的通配方式,例如正则表达式。

必须安排

设计

看看源码

首先我们得找到 SpringMVC 处理 @CrossOrigin 的源头,所以我们先来看下 @CrossOrigin 的源码(注释):

/**
 * 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 {
  ...
}

(PS:本文使用的 SpringBoot 版本为 1.5.22.RELEASE,SpringMVC 的版本为 4.3.25.RELEASE)

理解一下,@CrossOrigin 会被 SpringMVC 配置的某个合适的 HandleMapping-HandlerAdapter 来处理(在 SpringMVC 中,HandleMapping 用来根据请求找到对应的 HandlerAdapter ,而 HandleAdapter 用来处理请求)。然后注释就继续说了,当前版本的 SpringMVC 默认配置的 HandleMappingRequestMappingHandlerMapping。所以可以推测,对于 @CrossOrigin 的处理,就在 RequestMappingHandlerMapping 当中。查看 RequestMappingHandlerMapping 的源码,果然,在其父类 AbstractHandlerMapping 当中,发现了用于跨域处理的 CorsProcessor

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

AbstractHandlerMapping 给了 CorsProcessor 一个默认的实现 DefaultCorsProcessorDefaultCorsProcessor 代码看起来比较简单,即先判断是否为跨域请求,是的话再调用 checkOrigin 方法来对请求进行校验,而 checkOrigin 方法直接委托给了 CorsConfigurationcheckOrigin 方法:

/**
 * Check the origin and determine the origin for the response. The default
 * implementation simply delegates to
 * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
 */
protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
    return config.checkOrigin(requestOrigin);
}

查看 CorsConfiguration 的代码 —— 可知我们使用 @CrossOrigin 时配置的那些属性,都映射到了 CorsConfiguration 当中 —— 具体的映射方法为 RequestMappingHandlerMappinginitCorsConfiguration,不过这不是本文的重点,就不展开讲了。其中,CorsConfigurationallowedOrigins 属性,就是在 @CrssOrigin 中配置的 origins

设计方案

明白了 Spring 执行跨域访问请求的流程,我们也就可以比较容易的设计出让 @CrossOrigin 支持正则表达式的方案了:

  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 的自定义(参考 WebMvcRegistrations 的文档)。一如既往的,Spring 为这个接口提供了一个适配类,WebMvcRegistrationsAdapter,所以我们只需要继承这个 WebMvcRegistrationsAdapter 即可:

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

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

通过继承 WebMvcRegistrationsAdapter 并覆写 getRequestMappingHandlerMapping 方法,我们便完成了自定义 RequestMappingHandlerMapping 的功能 —— 面向 SpringBoot 编程,真的是 So easy~

嫌弃 SpringBoot 吃内存

测试

离大功告成还差一步测试啦 —— 所以先让我们来设置几个测试使用的 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 及其所有的二级域名进行跨域访问。

接着设置一下服务器的 port:

server.port=80

最后写个简单的 AJAX 请求:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello World</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 来访问,那么当前的网站的请求源(requestOrigin)便是 http://mizhou.com,而 AJAX 请求的网址为 http://local.com —— 显然,跨域失败(可以看到同源策略限制了该跨域访问):

http://mizhou.com 跨域失败

同理,再使用 http://zhiye.comhttp://abc.zhiye.com 来进行跨域访问:

http://zhiye.com 跨域成功
http://abc.zhiye.com 跨域成功

因为请求源和 @CrossOrigin 设置的正则表达式匹配,所以都是跨域成功 —— 大功告成~

开心~

扩展

使用正则来进行网址的匹配还是有点奇怪了,可能是因为大家平时写配置文件时候用的都是 Ant 风格的路径匹配规则 —— 所以我们可以创建一个 AntPathCorsProcessor,然后再通过自定义RequestMappingHandlerMapping 来替换掉 CorsProcessor 的默认实现,从而让 @CrossOrigin 实现 Ant 风格的路径匹配。或者说,本文已经提供了在 SpringMVC 中自定义 CORS 处理的方式,你可以按照你想要的方式来进行 CORS 的处理,例如我们就在 @CrossOriginorigins 配置一级域名,然后自定义 CorsProcessor 用简单的字符串匹配来判断请求源是否为其二级域名,从而判断是否允许跨域。然鹅,我今天很懒,所以这个扩展留给感兴趣的你吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容