为什么会出现跨域

出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。

举个栗子:从 blog.mzli.club 调用 api.mzli.club 的时候,如果api.mzli.club没有正确配置允许跨域。就会导致前端调用的时候报跨域错误。具体表现如下

什么是跨域

当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域

当前页面url

被请求页面url

是否跨域

原因

http://www.test.com/

http://www.test.com/index.html

同源(协议、域名、端口号相同)

http://www.test.com/

https://www.test.com/index.html

跨域

协议不同(http/https)

http://www.test.com/

http://www.baidu.com/

跨域

主域名不同(test/baidu)

http://www.test.com/

http://blog.test.com/

跨域

子域名不同(www/blog)

http://www.test.com:8080/

http://www.test.com:7001/

跨域

端口号不同(8080/7001)

常见的跨域解决方案

  1. CORS ( Cross-Origin Resource Sharing ,跨源资源共享):使用自定义的 HTTP 头部让浏览器 与服务器进行沟通。 通常在服务器端设置 Access-Control-Allow-Origin 头部,指定允许的来源域名,即可实现跨域请求 的许可。

  2. JSONP ( JSON with Padding ):利用 script 标签的跨域特性,通过动态创建 script 标签并设 置其 src 属性为跨域的 URL ,服务器端返回的响应数据需要用特定的格式包裹起来,并通过回调 函数返回给客户端。 只支持 GET 数据请求,不支持 POET 数据请求。

  3. 代理服务器(如 nginx 反向代理):在同源策略限制下,可以通过在同域名下的服务器上设置一个 代理服务器,将客户端请求转发到目标服务器,再将相应的结果返回给客户端。 客户端只需要与代理服务器通信,而不是直接与目标服务器通信,间接实现了跨域请求。

跨域问题实战

首先我们来模拟下跨域,写一个index.html。请求下本地后端接口。我这里用自己已经有的后端端口进行实战

结果如下:

报跨域了。原因是啥?端口不同,导致跨域。

通过注解解决

@RestController
@RequestMapping("/captcha/v1")
@CrossOrigin("*")
public class CaptchaController {

    private final CaptchaService captchaService;

    public CaptchaController(CaptchaService captchaService) {
        this.captchaService = captchaService;
    }

    @GetMapping("/get")
    public ResponseEntity<CaptchaEntity> getVerifyCode() {
        return ResponseEntity.success(captchaService.generate());
    }
}

添加@CrossOrigin("*") 针对全部origin

可以看到,get、post请求也没有报跨域了。

通过拦截器解决

@Component
public class CorsInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 设置其他跨域相关的响应头
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Max-Age", "86400");
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
}
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CorsInterceptor()).addPathPatterns("/**");
    }
}

定义一个拦截器,并在WebMvcConfigurer中进行注册。

再进行测试会发现

get、post请求也是没有报跨域的。

带Cookie请求跨域问题

上面的例子都是前端没带上Cookie请求给后端。但是一旦前端带上了Cookie给后端

那么上面的请求还是会跨域,具体表现如下。

大概意思就是当带上了凭证,那么Access-Control-Allow-Origin就不能是"*"。

这点在后端通过注解的配置,在idea中启动该程序也可以看出

可以看到,我在注解里同时配置了Access-Control-Allow-Origin:"*"和allowCredentials:"true",程序启动直接就报错了。

那这个时候注解肯定就行不通了,要怎么办呢。要么就是不使用"*", 直接指定origin:"localhost:8080"。那很多人就会说,我一个接口好多业务方调用呢,怎么搞?那就只能通过注解

@Component
public class CorsInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求头参数
        String origin = request.getHeader("Origin");

        // 设置其他跨域相关的响应头
        response.setHeader("Access-Control-Allow-Origin", origin);
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Max-Age", "86400");
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
}

先获取请求头中的origin,再设置到响应头中。

如果担心安全问题。可以对origin进行管理,匹配到的进行设置,匹配不上的直接就拦截。