CORS跨域处理心得随笔

什么是CORS跨域请求

现如今大多数项目都是前后端开发的,前端项目和后端接口服务部署在不同的机器下,且一般部署机器的ip也不相同。那么在大环境的驱使下跨域问题就成为了前后端开发必须面对的首要问题。那到底什么是跨域请求呢,又要如何解决跨域问题呢?

名词解释

同源策略:

同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。

跨域

CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。判断标准为协议、IP、端口三者中的任意一个不相同,则两个资源地址就不在同一个域下的。

如何处理跨域请求

基本思路

解决跨域问题的思路大致分为两种,第一种就是将两个域下的资源通过反向代理服务器(Nginx)代理到同一个ip下。第二种就是通过后端代码中进行请求配置。两种方式具体实现方式如下:

Nginx反向代理
先假定项目部署如下:

部署服务部署机器地址
前端服务http://front/
后端服务http://api/

前端服务中配置的接口请求基础地址为:http:api/
可以看到前后端服务部署在不同的域下,此时如果不进行任何处理的话前端将提示跨域,不能获取接口返回值。

使用nginx进行反向代理的具体配置如下:

location /api {
    proxy_pass http:://api;
}

location /front{
    proxy_pass http:://front/;
}

假定nginx部署的服务器ip和端口为:http://nginx/,那么前端服务配置的接口访问基础地址为:http://nginx/api/,此时浏览器访问http://nginx/front就不会跨域了。

后端接口程序配置
后端程序可以通过设置响应信息中的Access-Control-Allow-Origin头为“*”来解决跨域问题。在Springboot项目中具体配置如下。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {

    @Autowired
    private CorsCacheProcessor corsCacheProcessor;

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        // #允许访问的头信息,*表示全部
        config.addAllowedHeader("*");
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(18000L);
        // 允许提交请求的方法,*表示全部允许
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        // 允许Get的请求方法
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        return corsFilter;
    }
}

到此跨域问题得到了解决,但是上面的方案就是最终方案吗?答案是:NO。

是的,上述方案并不是最终的方案,因为我们使用了config.addAllowedHeader("*");这个配置。这个配置的含义是允许所有域的跨域请求通过,往往这种配置在安全上是不通过的,如果您的公司有严格的安全测试,您的测试小伙伴肯定会告知您需要修复这个安全隐患。

对此我们很容易想到一个解决方案就是白名单配置,我们在配置文件中增加允许跨域请求的域,然后在上述代码中将配置文件中的配置值注入进行,并且通过addAllowedHeader方法将这些白名单中的域加入进去。具体代码实现如下:

#假设这是允许跨域请求的前端域
allowed:
	origin: http://www.baidu.com,http://www.zhihu.com
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Arrays;
@Configuration
public class CorsConfig {

    @Value("${allowed.origin}")
    private String allowedOrigin;

    private static final String SEPARATORCHAR = ",";

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        // 允许cookies跨域
        config.setAllowCredentials(true);
        // #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
        if(StringUtils.isNotBlank(allowedOrigin)) {
            if(allowedOrigin.contains(SEPARATORCHAR)) {
                String[] split = allowedOrigin.split(SEPARATORCHAR);
                Arrays.asList(split).stream().forEach(config::addAllowedOrigin);
            } else {
                config.addAllowedOrigin(allowedOrigin);
            }
        } else {
            // 没有配置就允许所有跨域
            config.addAllowedOrigin("*");
        }
        // #允许访问的头信息,*表示全部
        config.addAllowedHeader("*");
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(18000L);
        // 允许提交请求的方法,*表示全部允许
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        // 允许Get的请求方法
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

}

到这里我们可以通过配置白名单的方式控制能跨域请求后端接口的域,但是这种方式还是有缺陷,假设我们的项目是一个上线了很久并且与多方进行过交互的系统,我们不能第一时间获取有多少域是需要配置白名单的。如果这个功能上线到生产后,后续发现有域漏配置了,我们需要在配置文件中加上配置然后重启服务才行。这十分的不优雅,那有没有方法可以进行动态配置呢,我们添加配置后不用重启服务就能生效。

如何动态添加白名单

经过对上述java跨域配置源码的分析,我们不难发现CorsConfiguration 该配置类中可以通过setCorsProcessor方法设置跨域处理器,如果你不设置它就使用默认的DefaultCorsProcessor处理器,并且DefaultCorsProcessor中的handleInternal就针对跨域问题进行了处理。具体的处理逻辑大家可以看源码进行分析。

于是我们想到可以通过自定义跨域处理器进行动态白名单的加载。具体的代码如下

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.DefaultCorsProcessor;

import java.io.IOException;
import java.util.List;

/**
 * @ProjectName
 * @ClassName CorsCacheProcessor
 * @ClassDescription:
 * @Author huangliang
 * @CreatedDate 2022年09月01日 11:00
 */
@Component
@Slf4j
public class CorsCacheProcessor extends DefaultCorsProcessor {

    @Autowired
    // redis客户端,本案例中比提供实现
    private RedisClient redisClient;

	private String allowedOrigin;
	
    private static final String SEPARATORCHAR = ",";


    @Override
    protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
                                     CorsConfiguration config, boolean preFlightRequest) throws IOException {
        log.info("CorsCacheProcessor start");
        // 动态设置跨域地址
        setAllowedOrigins(config);
        return super.handleInternal(request,response,config,preFlightRequest);
    }

    private void setAllowedOrigins(CorsConfiguration config) {
        config.setAllowedOrigins(null);
        // #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
        if (StringUtils.isEmpty(allowedOrigin)) {
            // 没有配置就允许所有跨域
            config.addAllowedOrigin("*");
        } else {
            if(allowedOrigin.contains(SEPARATORCHAR)) {
                String[] split = allowedOrigin.split(SEPARATORCHAR);
                Arrays.asList(split).stream().forEach(config::addAllowedOrigin);
            } else {
                config.addAllowedOrigin(allowedOrigin);
            }
        }
    }


    /**
     * 启动执行一次,之后十分钟执行一次
     */
    @Scheduled(fixedDelay = 1000 * 60 * 10)
    public void execute() {
        try {
            // #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
            log.info("跨域白名单数据拉取开始");
            allowedOrigin= redisClient.get(ZUUL_CONFIG_CACHE_KEY);
            log.info("跨域白名单数据拉取结束");
        } catch (Exception e) {
            log.error("", e);
        }
    }
}

跨域配置类也进行相应的修改

@Configuration
public class CorsConfig {

    @Autowired
    private CorsCacheProcessor corsCacheProcessor;

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        // #允许访问的头信息,*表示全部
        config.addAllowedHeader("*");
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(18000L);
        // 允许提交请求的方法,*表示全部允许
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        // 允许Get的请求方法
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        // 通过配置自定义处理器来动态控制跨域的域名
        corsFilter.setCorsProcessor(corsCacheProcessor);
        return corsFilter;
    }

}

我们通过往redis缓存中配置白名单每隔十分钟就会刷新到服务内存中让跨域配置生效。到此本次的跨域问题就完美解决了。

思考:至于为什么不直接读取redis缓存而是每隔十分钟从redis缓存中读取数据刷新到jvm内存中是考虑到对redis性能的损耗。因为如果直接读取redis数据的话,那么以后的没一次请求都会去redis中读取去配置信息,为了这点实时性增添很多的不必要的消耗,并且这个白名单配置也不是经常变更的配置项,所以实时性要求没有要么高。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值