创新编码

不说话,装高手。

Maintain silence and pretend to be an experta

简单聊聊跨域问题

2024-08-17 18:23:50碎碎念

跨域问题

什么是跨域

页面域名是 AAA,请求的接口地址是 BBB。这就是跨域,就是跨越也不同的域名来请求资源

这是一种只在浏览器环境中才会发生的现象,浏览器会阻止给和页面地址不同的域名发送请求,这跟语言无关,这是浏览器下的一个安全策略

解决方法

  • Nginx 解决跨域

    前端请求接口还是使用网页的域名,使用 nginx 转发到其他服务端的地址,配置文件中添加大概如下代码

    折叠代码 复制代码
    server {
      listen       80;
      server_name  your_domain.com;
      location /api {
        # 允许跨域请求的域名,* 表示允许所有域名访问
        add_header 'Access-Control-Allow-Origin' '*';
        # 允许跨域请求的方法
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        # 允许跨域请求的自定义 Header
        add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept';
        # 允许跨域请求的 Credential
        add_header 'Access-Control-Allow-Credentials' 'true';
        # 预检请求的存活时间,即 Options 请求的响应缓存时间
        add_header 'Access-Control-Max-Age' 3600;
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
        	return 204;
        }
      }
      # 其他配置...
    }
    
  • 后端解决,这里我以 Nestjs 为例

    1. 全局启用 CORS

      折叠代码 复制代码
      import { NestFactory } from '@nestjs/core';
      import { AppModule } from './app.module';
      
      async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      
      // 启用CORS
      app.enableCors();
      
      await app.listen(3000);
      }
      bootstrap();
      
    2. 使用 @UseCors 装饰器为特定路由启用 CORS

      折叠代码 复制代码
      import { Controller, Get, UseCors } from '@nestjs/common';
      
      @Controller('user')
      export class CatsController {
        @Get()
        @UseCors({
         origin: '<http://example.com>',
        })
        getList() {
         // ...
        }
      }
      
    3. 通过中间件开设置更复杂的 CORS

      折叠代码 复制代码
      import { Injectable, NestMiddleware, HttpStatus, RequestMethod} from '@nestjs/common';
      import { Request, Response, NextFunction } from 'express';
      import { isDevEnv } from 'src/app.environment';
      
      @Injectable()
      export class CorsMiddleware implements NestMiddleware {
        use(req: Request, res: Response, next: NextFunction) {
          const getMethod = (method) => RequestMethod[method];
          const origins = req.headers.origin;
          const origin = (Array.isArray(origins) ? origins[0] : origins) || '';
          // 允许通过请求的域名
          const allowedOrigins = ['http://127.0.0.1:3002']; 
          // 允许通过请求的方法
          const allowedMethods = [
            RequestMethod.GET,
            RequestMethod.HEAD,
            RequestMethod.PUT,
            RequestMethod.PATCH,
            RequestMethod.POST,
            RequestMethod.DELETE,
          ];
          // 允许通过请求的请求头字段
          const allowedHeaders = [
            'Authorization',
            'Origin',
            'No-Cache',
            'X-Requested-With',
            'If-Modified-Since',
            'Pragma',
            'Last-Modified',
            'Cache-Control',
            'Expires',
            'Content-Type',
            'X-E4M-With',
            'Sentry-Trace',
            'Baggage',
            'x-access-token',
          ];
      
          if (!origin || allowedOrigins.includes(origin) || isDevEnv) {
          	res.setHeader('Access-Control-Allow-Origin', origin || '*');
          }
      
          res.header('Access-Control-Allow-Credentials', 'true');
          res.header('Access-Control-Allow-Headers', allowedHeaders.join(','));
          res.header(
            'Access-Control-Allow-Methods',
            allowedMethods.map(getMethod).join(','),
          );
          res.header('Access-Control-Max-Age', '1728000');
          res.header('Content-Type', 'application/json; charset=utf-8');
      
          // 处理预请求
          if (req.method === getMethod(RequestMethod.OPTIONS)) {
            if (req.method === 'OPTIONS') {
              return res.status(204).send();
            } else {
              next();
            }
            return res.sendStatus(HttpStatus.NO_CONTENT);
          } else {
            return next();
          }
        }
      }
      
  • 通过网关处理跨域

    Java 中的 SpringCloud Gateway 可以通过修改配置文件或者通过 CorsWebFilter 过滤器来实现跨域

    1. Gateway 服务的 application.yml 文件中,添加如下配置,确保允许 Options 请求,因为浏览器在进行跨域请求是会先发送一个 Options 请求来验证是否允许跨域

      折叠代码 复制代码
      spring:
        cloud:
          gateway:
          globalcors: # 全局的跨域处理
            add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
            corsConfigurations:
              '[/**]':
                allowedOrigins: # 允许哪些网站的跨域请求
                  - "http://localhost:8090"
                  - "http://localhost:8081"
                allowedMethods: # 允许的跨域ajax的请求方式
                  - "GET"
                  - "POST"
                  - "DELETE"
                  - "PUT"
                  - "OPTIONS"
                allowedHeaders: "*" # 允许在请求中携带的头信息
                allowCredentials: true # 是否允许携带cookie
                maxAge: 360000 # 这次跨域检测的有效期
      
    2. CorsWebFilter 过滤器

      折叠代码 复制代码
      @Configuration
      public class GlobalCorsConfig {
          @Bean
          public CorsWebFilter corsWebFilter() {
            CorsConfiguration config = new CorsConfiguration();
            // 这里仅为了说明问题,配置为放行所有域名,生产环境请对此进行修改
            config.addAllowedOriginPattern("*");
            // 放行的请求头
            config.addAllowedHeader("*");
            // 放行的请求类型,有 GET, POST, PUT, DELETE, OPTIONS
            config.addAllowedMethod("*"); 
            // 暴露头部信息
            config.addExposedHeader("*"); 
            // 是否允许发送 Cookie
            config.setAllowCredentials(true); 
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", config);
            return new CorsWebFilter(source);
          }
      }
      
  • 通过框架处理跨域

    一般来说 Vue、**React **等前端框架都会帮我们把这个问题处理好

    1. Vue2.0 vue.config.js

      折叠代码 复制代码
      module.exports = {
        devServer:{
          proxy:{
            '/api':{ //  /api表示拦截以/api开头的请求路径
              target: 'http://localhost:3000/api/', //跨域的域名
              changeOrigin:true,//是否开启跨域
              pathRewrite:{ // 重写路径
                '^/api':'' // 把/api变为空字符
              }
            }
          }
        }
      }
      
    2. Vue3.0 vite.config.js

      折叠代码 复制代码
      server: {
        host: "127.0.0.1", // 开发服务器的地址
        port: 8000,        // 开发服务器的端口号
        proxy: {
          "/api": {
            target: "https://www.360.com", // 目标地址
            changeOrigin: true,            // 是否换源
            rewrite: (path) => path.replace(/^\/api/, ""), 
          },
        },
      },
      
  • JSONP跨域方案(一般很少用,列举两种记录一下)

    1. 原生 js 实现

      折叠代码 复制代码
      // 前端
      <script>
      var script = document.createElement('script')
      script.src = 'http://www.domain2.com:8081/user?userid=1001'
      document.body.append(script)
      
      var handlerRes = function(res) {
      	console.log(res)
      }
      
      // 发送到服务端
      handlerRes("JSONP跨域脚本")
      </script>
      
      // nodejs 后端
      const express = require('express')
      
      const ser = express()
        ser.get('/user', (req, res) => {
        var id = req.query.userid
        console.log(id) // 打印:1001
      
        // 发送到客户端
        res.send('handlerRes(' + id +')')
      })
      
      ser.listen(8081, () => {
      	console.log('create3')
      })
      
    2. JQuery Ajax 实现

      折叠代码 复制代码
      $.ajax({
        url: 'http://www.domain2.com:8080/login',
        type: 'get',
        dataType: 'jsonp',  // 请求方式为jsonp
        jsonpCallback: "handleCallback",  // 自定义回调函数名
        data: {}
      });
      
  • 关闭浏览器的安全策略

    这里以谷歌浏览器 Chrome 为例

    1. mac

      寻找合适位置创建一个 myChromeDevData目录,在终端中执行一下命令,会自动调起关闭安全策略的浏览器

      open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=/Users/Documents/myChromeDevData

    2. windows

      寻找合适位置创建一个 myChromeDevData目录,新建一个 chrome 快捷图标,右键属性,

      在目标处最后面加入

      --disable-web-security --user-data-dir=D:\tmp\myChromeDevData --user-data-dir

结语

跨域问题在我的理解中最好是在网关层、代理层解决因为越靠前覆盖范围就越大,解决跨域问题就越容易,优先级:网关层 > 代理层 > 应用层

记录别人问过的一些问题

  1. 为什么开发环境不产生跨域

    这边排除那些前后端一起干的同学,因为他们本地都是 localhost,自然不会产生跨域

    另外前端开发时本地页面地址一般都是 localhost,接口地址肯定是其他的,为什么不会跨域?

    这个问题在上面已经说过,我们平时开发所用的脚手架已经帮我们默默处理掉了跨域,那自然不会产生跨域

  2. 前端可以处理掉跨域。那我打包部署时部署一样就好了?

    因为前端项目构建打包后,得到的只有一堆静态资源,他不像以前的模版引擎例如 jsp、php、ejs 等,需要依赖服务器来跑,我们的构建产物只需要一个 nginx 来提供静态文件的访问能力就行

  3. 为什么会发 option 请求

    这是浏览器的安全策略,如果浏览器从来都没有请求过 BBB 域名,他怎么知道页面当前域名在不在BBB 域名的名单中,所以就有 option 请求,一个不带任何数据的请求,就是问一下 BBB 域名的服务器,我有什么权限,能否反问你

  4. 为什么我们在网站中可以加载各种域名的图片,他们不跨域吗?

    图片也会触发跨域,只不过对于图片这类资源限制没那么严格,现在可以通过 Content-Security-Policy 内容安全政策响应标头来控制给定页面加载的资源