后端跨域问题的处理

问题描述

在做前后端分离的项目时,很有可能会遇到这样一种情况:

就是在游览器中请求后端的接口,出现了 CORS error 错误

在这里插入图片描述

报错信息如下:

Access to XMLHttpRequest at 'http://localhost:8860/user/auth/login' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

大概的意思就是:

跨源资源共享(CORS)策略阻止了来自端口 5173 的前端应用对运行在端口 8860 的后端服务的 XMLHttpRequest 请求


原因分析

它的产生并不是因为后端代码出错了,也不是因为前端调用时有问题,而是因为游览器的安全机制(同源策略限制)所导致的,是一个很典型的跨域问题

那游览器的同源策略是什么?

浏览器的 同源策略(Same-origin policy) 是一种安全策略,它规定了从同一个 源(origin) 加载的文档或脚本如何与来自另一个源的资源进行交互。这是浏览器提供的一个重要的安全机制,用于隔离潜在恶意文档,减少可能被攻击的媒介。

同源是指 协议 + 域名 + 端口 三者相同,也就是说,如果两个页面的协议、域名或端口中有任何一个不相同,那么它们就是不同源的。

同源策略主要限制了以下几个方面:

  • Cookie、LocalStorage 和 IndexedDB:无法读取不同源的 Cookie、LocalStorageIndexedDB,这防止了恶意网站窃取或篡改用户的敏感数据
  • DOM:无法操作不同源的页面的 DOM。这确保了恶意网站不能篡改或窃取其他网站的内容
  • AJAX 请求:默认情况下,无法发送 AJAX 请求到不同源的服务器,这防止了恶意网站发起跨站请求伪造( CSRF )攻击

什么是跨域?

跨域就是违法了同源策略,当一个页面去请求另外一个 URL 时,两者 URL协议 + 域名 + 端口 出现不一致

回到上述案例,从控制台中打印的错误信息就能看出,在 http://localhost:5173 的服务中访问了 http://localhost:8860 下的 /user/auth/login 接口,其端口就出现了不一致,出现了跨域


解决方案

现在知道了这种错误是因为跨域问题造成的,那么如何解决这种跨域问题呢?

解决跨域的方法有很多,这里简单介绍以下三种方法:

  • 方案一:Jsonp

    这种方法是早期的一种跨域解决方案,它对各个游览器的版本兼容性做得比较好,但是实现需要前端跟后端都去写相应的代码来进行支持,耦合度高,而且仅支持 GET 请求,所有不常用目前也不推荐用

    比如说在前端使用 ajax 去发送一个请求,需要指定 dataTypeJsonp,这时请求就会自动加上 callback=xxx 这样的参数,xxx 会作为密钥的形式传到后端 ,后端再将 xxx 返回给前端,这就相当于前后端做了一个校验,使得游览器认可这种跨域请求,就可以进行跨域访问了

    前端代码示范:

    $.ajax((
    	url: 'http://localhost:8860/user/auth/login',
    	dataTpe: "jsonp",
    	// Jsonp: 'callback', // 不指定默认 callback
    	// JsonpCallBack: "xxx", // 不指定自动生成
    	type: 'GET',
    	success: function(result) {
    		alert(result.data)
    	}
    ));
    

    后端代码示范:

    	@GetMapping("/jsonp/{id}")
    	public JSONPObject getUser(@PathVariable Integer id, String callback) {
    		return new JSONPObject(callback, new Result<>(200, "success", data))
    	}
    
  • 方式二:Proxy 代理

    使用代理服务器处理跨域问题的基本思路是,在前端和后端服务之间设置一个代理服务器。这个代理服务器位于前端应用所在的域上,因此可以绕过浏览器的同源策略限制。前端应用向代理服务器发送请求,代理服务器再将这些请求转发给实际的后端服务。后端服务处理请求后,将响应返回给代理服务器,代理服务器再将响应转发给前端应用。这样,前端应用看起来就像是直接与后端服务通信,但实际上所有的通信都是通过代理服务器进行的

    现在前端的框架一般都带有 proxy 反向代理功能,就是前端开发的时候一般会在本地启动一个 node.js 服务,让 node 服务去请求接口,然后再把数据传给游览器,就相当于在本地起了一个服务作为中转站

    示例:在 vite.config.ts 文件中配置代理

    export default defineConfig({
      plugins: [vue()],
      server: {
       // 设置项目端口
       port: 5173,
      // 运行时自动打开游览器
      open: true,
      proxy: {
        '/api': {
           // 目标服务器地址
           target: 'http://localhost:8860',
           // 是否改变源地址
           changeOrigin: true,
           // 重写路径
           rewrite: (path) => path.replace(new RegExp('^' + '/api'), ''),
         },
       },
      },
    })
    
  • 方式三:跨域请求 CORS

    CORS(Cross-Origin Resource Sharing) 是一种计算机安全机制,它允许不同源(即协议、域名和端口不同)的 Web 站点进行资源共享。由于浏览器的同源安全策略,通常情况下,一个域的脚本无法直接访问另一个域的资源。CORS 通过在服务器端设置特定的 HTTP 头部,来告诉浏览器:“我允许这个域的脚本访问我的资源”。这样,浏览器就会解除对跨域请求的限制,从而允许前端应用从不同的域请求资源。

    在这里插入图片描述

    CORS 主要有两种模型:

    • 简单模型: 支持 GET、POST、PUT、DELETE 请求,但不允许自定义 header 且会忽略 cookiesPOST 数据格式也有限制,主要支持 text/plain、application/x-www-form-urlencodedmultipart/form-data。其中 text/plain 是默认支持的,后两者需要与服务器进行预检请求和协商
    • 协商模型/预检请求: 当发出如 POST 请求时,浏览器会首先发出一个 OPTIONS 请求进行预检。如果服务器返回 Access-Control-Allow-OriginAccess-Control-Allow-Methods 等头部,并同意来自某个域的请求,浏览器就会继续发出真正的请求

    CORS 从具体的代码实现上来说还是比较方便的,前端不需要编写任何代码,主要是靠服务端进行配置

    CORS 需要游览器和服务器同时支持,目前几乎所有的游览器都支持该功能,IE 游览器不能低于 IE 10

    同时 CORS 也是比较推荐的一种处理跨域请求的做法,具体实现请看后文


代码实现

上述的三种解决跨域的方案中 CORS 是可以完全由后端去实现的,以下内容我就以 Spring BootSpring Cloud 为例概述如何解决跨域问题

这里我在后台启动一个 Spring Cloud 的项目,其中有个 Gateway 服务和一个 User 服务,用户服务中有一个接口 /auth/loign

在这里插入图片描述

如果仅仅看 User 服务它就是一个 Spring Boot 的项目,访问该接口的 URL 为:POST http://localhost:8868/auth/login

Spring Cloud 项目的话一般请求都是走网关的,这里正确的请求 URL 为:POST http://localhost:8860/user/auth/login

前端发送请求代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>发送请求</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.7.2/axios.js"></script>
</head>

<body>

    <button id="bootNpde">Spring Boot 跨域请求</button>
    <button id="cloudNode">Spring Cloud 跨域请求</button>


    <script>

        bootNpde.onclick = function () {
            const p = axios.post("http://localhost:8868/auth/login", {
                username: "admin",
                password: "admin"
            });
            console.log(p);
            p.then(result => {
                console.log(result);
                // 获取服务器端的响应数据
                const { data } = result;
                console.log(data);
            }).catch(err => {
                console.log(err);
            })
        }

        cloudNode.onclick = function () {
            const p = axios.post("http://localhost:8860/user/auth/login", {
                username: "admin",
                password: "admin"
            });
            console.log(p);
            p.then(result => {
                console.log(result);
                // 获取服务器端的响应数据
                const { data } = result;
                console.log(data);
            }).catch(err => {
                console.log(err);
            })
        }

    </script>

</body>

</html>

目前项目没有做任何的跨域处理,所以访问这两个 URL 的时候均会出现跨域错误

在这里插入图片描述

(1)Spring Boot 跨域处理

  • 方式一:在接口或者方法上通过注解 @CrossOrigin 来解决跨域问题

@CrossOrigin 注解是 Spring 框架中的一个注解,它的主要作用是处理跨域资源共享(CORS)问题

使用 @CrossOrigin 注解,可以在控制器类或具体处理方法上指定哪些来源可以访问该类或方法,从而实现对 CORS 的简化配置。当在类或方法上使用此注解时,Spring 会自动在 HTTP 响应头中添加适当的 CORS 相关头部信息,如 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 等,从而允许浏览器执行跨域请求

@CrossOrigin 注解支持多种配置选项,例如允许特定来源、允许所有来源、指定请求头和响应头等。例如,可以在注解中指定 origins 属性来限制可以访问的域名列表,或使用 methods 属性来指定允许的 HTTP 请求方法

代码示例:

@Slf4j
@RestController
@RequestMapping("/auth")
@Api(tags = "【登录认证】")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@CrossOrigin(origins = "*", methods = {RequestMethod.POST, RequestMethod.GET})
public class AuthController {

    private final AuthService authService;

    @PostMapping(value = "/login")
    @ApiOperation(value = "用户登录")
    //@CrossOrigin(origins = "http://localhost:5173", methods = {RequestMethod.POST, RequestMethod.GET})
    public ResponseBean<LoginResp> login(@RequestBody @Validated LoginReq req) {
        return ResponseBean.success(authService.login(req));
    }
}

测试:

在这里插入图片描述

  • 方式二:配置全局 CORS 映射

如果你希望全局地配置 CORS 策略,可以在 Spring Boot 配置类中添加一个 CORS 配置 bean

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 {

    @Bean
    public CorsFilter corsFilter() {

        CorsConfiguration config = new CorsConfiguration();
        // 允许向该服务器提交请求的 URI,* 表示全部允许,在 SpringMVC 中,如果设成 *,会自动转成当前请求头中的 Origin
        //config.addAllowedOrigin("*");
        // 指定允许跨域的域名
        //config.addAllowedOrigin("http://localhost:5173");
        // springboot2.4.2 中 addAllowedOrigin 不允许设置为 *,要改成使用 AllowedOriginPattern
        config.addAllowedOriginPattern("*");
        // 允许访问的头信息,* 表示全部
        config.addAllowedHeader("*");
        // 允许提交请求的方法,*表示全部允许
        config.addAllowedMethod("*");
        // 允许 cookies 跨域
        config.setAllowCredentials(true);
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(18000L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

或者写成:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry
                        // 映射到所有路径
                        .addMapping("/**")
                        // 允许所有域名进行跨域调用
                        //.allowedOrigins("*")
                        // springboot2.4.2 后 addAllowedOrigin 不允许设置为 *,要改成使用 AllowedOriginPattern
                        .allowedOriginPatterns("*")
                        // 允许所有请求方式跨域调用
                        .allowedMethods("*")
                        // 放行全部原始头信息
                        .allowedHeaders("*")
                        // 允许携带 Cookie 信息
                        .allowCredentials(true)
                        // 预检请求的缓存时间
                        .maxAge(18000L);
            }
        };
    }

}

测试:

在这里插入图片描述

(2)Spring Cloud 跨域处理

  • 方式一:配置全局 CORS 映射

Spring boot 配置全局 CORS 差不多,不过是在 Gateway 服务的配置类中添加一个 CORS 配置 bean

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * 跨域处理
 */
@Configuration
public class GlobalCorsConfiguration {

    @Bean
    public CorsWebFilter corsFilter() {
        // 配置跨域信息
        CorsConfiguration config = new CorsConfiguration();
        // 允许跨域的请求来源:允许所有域名进行跨域调用
        config.addAllowedOriginPattern("*");
        // 允许跨域的头:放行全部原始头信息
        config.addAllowedHeader("*");
        // 允许跨域的请求方式:允许所有请求方式跨域调用
        config.addAllowedMethod("*");
        // 允许携带 Cookie 信息
        config.setAllowCredentials(true);

        // 添加映射路径
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        // 任意 url 都要进行跨域配置
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }

}

在这里插入图片描述

  • 方式二:在配置文件中设置 CORS 策略

Gateway 服务的配置文件 application.yml 中设置 CORS 策略

示例:

spring:
  cloud:
    gateway:
      # 全局的跨域处理
      globalcors: 
        # 解决 options 请求被拦截问题
        add-to-simple-url-handler-mapping: true 
        cors-configurations:
          '[/**]':
            allowedOrigins:  # 允许哪些网站的跨域请求
              - "http://localhost:5173"
            allowedMethods:  # 允许的跨域 ajax 的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            # 允许在请求中携带的头信息  
            allowedHeaders: "*"
            # 是否允许携带 cookie
            allowCredentials: true
            # 这次跨域检测的有效期
            maxAge: 36000

需要注意的问题:

对于 Spring Cloud Gateway,你可以通过全局 CORS 配置或者特定路由的 CORS 配置来设置 CORS。确保没有其他过滤器或配置在之后覆盖了你的 CORS 设置

也就是说最好只配置一个 CORS,比如你在配置文件中配置了全局跨域处理,允许 http://localhost:5173 进行跨域,那么就不要用 @CrossOrigin 在接口上设置了 CORS

例如:

@Slf4j
@RestController
@RequestMapping("/auth")
@Api(tags = "【登录认证】")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@CrossOrigin(origins = "*", methods = {RequestMethod.POST, RequestMethod.GET})
public class AuthController {

    private final AuthService authService;

    @PostMapping(value = "/login")
    @ApiOperation(value = "用户登录")
    //@CrossOrigin(origins = "http://localhost:5173", methods = {RequestMethod.POST, RequestMethod.GET})
    public ResponseBean<LoginResp> login(@RequestBody @Validated LoginReq req) {
        return ResponseBean.success(authService.login(req));
    }
}

不然就会因为响应头 Access-Control-Allow-Origin 包含多个值时,被浏览器拒绝该响应

在这里插入图片描述

在这里插入图片描述

报错信息:

Access to XMLHttpRequest at 'http://localhost:8860/user/auth/login' from origin 'http://localhost:5173' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:5173, *', but only one is allowed.

这样就导致了即使你配置了跨域,但是还是会出现跨域的问题的

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值