MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

微服务架构的跨域问题解决方案

2022-11-086.3k 阅读

微服务架构中的跨域问题基础

跨域概念及产生原因

在前端开发中,当一个资源(如 HTML 页面、脚本等)从一个域请求另一个不同域的资源时,就会出现跨域问题。这里的“域”由协议、域名和端口号共同组成,只要这三者中有任何一个不同,就属于不同的域。例如,http://www.example.com:8080https://www.example.com:8081 就是两个不同的域,前者使用 HTTP 协议,后者使用 HTTPS 协议,且端口号也不同;又如,http://www.example.comhttp://api.example.com,虽然协议和端口号相同,但域名不同,同样属于不同的域。

跨域问题产生的根本原因是浏览器的同源策略(Same - Origin Policy)。同源策略是一种安全机制,它限制了从一个源加载的文档或脚本如何与另一个源的资源进行交互。这是为了防止恶意网站通过脚本获取其他网站的敏感信息,如用户的登录凭证、个人数据等。例如,如果没有同源策略,一个恶意网站可以通过 JavaScript 轻松读取用户登录到银行网站后的账户信息,这将带来严重的安全风险。

微服务架构下跨域问题的复杂性

在传统的单体架构中,所有的后端服务都运行在同一个进程空间内,前端与后端交互时通常只涉及一个域,跨域问题相对简单,可能只需在服务器端进行一些简单的配置即可解决。

然而,在微服务架构下,情况变得复杂得多。微服务架构将一个大型应用拆分成多个小型、独立的服务,每个服务都有自己独立的进程和部署环境。这些微服务可能部署在不同的服务器上,甚至可能使用不同的协议和端口号。例如,一个用户服务可能部署在 http://user - service.example.com:8082,而订单服务可能部署在 http://order - service.example.com:8083。当前端需要同时与多个微服务进行交互时,就很容易出现跨域问题。而且,由于微服务之间也可能存在相互调用的情况,例如订单服务可能需要调用用户服务获取用户信息,这就导致跨域问题不仅出现在前端与后端之间,还可能出现在微服务与微服务之间。

前端跨域解决方案

JSONP 跨域

JSONP(JSON with Padding)是一种古老但仍然有效的跨域解决方案,主要用于解决浏览器端的跨域数据请求问题。它利用了 <script> 标签不受同源策略限制的特性。

工作原理

  1. 前端页面创建一个 <script> 标签,其 src 属性指向跨域的 API 接口,并在 URL 中添加一个回调函数名参数。例如:
<script>
    function jsonpCallback(data) {
        console.log(data);
    }
</script>
<script src="http://cross - domain - api.com/api?callback = jsonpCallback"></script>
  1. 后端 API 接收到请求后,将数据以指定的回调函数包裹的形式返回。例如,假设后端数据是 { "message": "Hello, JSONP!" },返回的内容将是 jsonpCallback({ "message": "Hello, JSONP!" });
  2. 浏览器在收到响应后,会将其作为 JavaScript 代码执行,从而调用事先定义好的回调函数,并将数据作为参数传入。

优缺点

  • 优点:兼容性好,几乎所有浏览器都支持;实现简单,前端和后端代码改动较小。
  • 缺点:只支持 GET 请求,因为它是通过 <script> 标签的 src 属性发起请求的,而 src 属性只能使用 GET 方法;安全性相对较低,由于返回的是 JavaScript 代码,如果被恶意篡改,可能会执行恶意脚本。

CORS 前端处理方式

CORS(Cross - Origin Resource Sharing)即跨域资源共享,是一种现代浏览器支持的跨域解决方案,它通过在 HTTP 头信息中添加一些字段来告诉浏览器哪些跨域请求是被允许的。虽然 CORS 主要是在后端进行配置,但前端也需要了解一些相关知识。

前端发起 CORS 请求

在现代浏览器中,当发送跨域请求时,浏览器会自动在请求头中添加一些信息。对于简单请求(满足以下条件:使用 GET、POST 或 HEAD 方法;请求头只包含 AcceptAccept - LanguageContent - LanguageContent - Type,且 Content - Type 的值仅限于 application/x - www - form - urlencodedmultipart/form - datatext/plain),浏览器会直接发送请求,并在收到响应后检查响应头中的 CORS 相关字段。对于复杂请求(不满足简单请求条件的请求,例如使用 PUT、DELETE 方法,或者 Content - Typeapplication/json 等),浏览器会先发送一个预检请求(OPTIONS 请求),询问服务器是否允许该跨域请求。只有当服务器通过预检请求的验证后,浏览器才会发送实际的请求。

例如,使用 fetch 发起一个跨域请求:

fetch('http://cross - domain - api.com/api', {
    method: 'POST',
    headers: {
        'Content - Type': 'application/json'
    },
    body: JSON.stringify({ data: 'example' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

在这个例子中,由于使用了 POST 方法且 Content - Typeapplication/json,所以这是一个复杂请求,浏览器会先发送预检请求。

前端注意事项

前端需要确保请求头中的信息与后端配置的 CORS 规则相匹配。例如,如果后端只允许特定的 Content - Type,前端就不能随意更改。同时,前端也要处理好预检请求可能带来的性能问题,因为预检请求会增加一次额外的请求往返。

后端跨域解决方案 - 基于网关

网关概述

在微服务架构中,网关是系统的入口,它作为前端应用与各个微服务之间的桥梁,负责接收所有来自前端的请求,并将这些请求转发到相应的微服务。常见的网关有 Spring Cloud Gateway、Zuul 等。网关不仅可以实现路由功能,还可以在请求转发过程中进行一系列的处理,如身份验证、权限检查、流量控制等,当然也包括解决跨域问题。

Spring Cloud Gateway 解决跨域

Spring Cloud Gateway 是 Spring Cloud 生态系统中的一个网关组件,它基于 Spring WebFlux 构建,具有高性能、可扩展性强等特点。

配置 CORS

在 Spring Cloud Gateway 中,可以通过配置文件来配置 CORS。首先,在 application.yml 文件中添加如下配置:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
            allowedHeaders: "*"
            allowCredentials: true

在上述配置中,[/**] 表示对所有的请求路径都应用 CORS 配置。allowedOrigins 设置为 * 表示允许所有来源的请求;allowedMethods 列出了允许的 HTTP 方法;allowedHeaders 设置为 * 表示允许所有的请求头;allowCredentials 设置为 true 表示允许携带认证信息(如 Cookie)。

自定义 CORS 过滤器

除了通过配置文件进行简单配置外,还可以自定义 CORS 过滤器来实现更灵活的跨域处理。首先创建一个自定义的 CORS 配置类:

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 CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("http://allowed - origin.com");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", corsConfiguration);

        return new CorsWebFilter(source);
    }
}

在这个配置类中,创建了一个 CorsWebFilter,并对其进行了详细的配置。addAllowedOrigin 方法指定了允许的来源,这里设置为 http://allowed - origin.com,如果需要允许多个来源,可以多次调用该方法;addAllowedMethodaddAllowedHeader 分别设置允许的 HTTP 方法和请求头;setAllowCredentials 用于设置是否允许携带认证信息。

Zuul 解决跨域

Zuul 是 Netflix 开源的一个网关组件,在 Spring Cloud 中也被广泛应用。

配置 CORS

在 Zuul 中配置 CORS 可以通过在 application.yml 文件中添加如下配置:

zuul:
  routes:
    api - gateway:
      path: /api/**
      url: http://backend - service
  servlet:
    filter:
      order: -1
cors:
  allowedOrigins: "*"
  allowedMethods:
    - GET
    - POST
    - PUT
    - DELETE
  allowedHeaders: "*"
  allowCredentials: true

在上述配置中,zuul.routes.api - gateway 配置了路由规则,将 /api/** 的请求转发到 http://backend - servicecors 部分配置了 CORS 相关信息,与 Spring Cloud Gateway 的配置类似,allowedOrigins 设置为 * 允许所有来源,allowedMethodsallowedHeadersallowCredentials 分别设置允许的方法、请求头和是否允许携带认证信息。

自定义 Zuul 过滤器

与 Spring Cloud Gateway 类似,Zuul 也可以通过自定义过滤器来实现更灵活的跨域处理。首先创建一个自定义的 Zuul 过滤器类:

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;

@Component
public class CorsFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletResponse response = ctx.getResponse();
        response.setHeader("Access - Control - Allow - Origin", "*");
        response.setHeader("Access - Control - Allow - Methods", "GET, POST, PUT, DELETE");
        response.setHeader("Access - Control - Allow - Headers", "*");
        response.setHeader("Access - Control - Allow - Credentials", "true");
        return null;
    }
}

在这个自定义过滤器中,filterType 方法返回 pre 表示这是一个前置过滤器,会在请求被转发到后端服务之前执行。filterOrder 方法返回 0 表示该过滤器的执行顺序,数值越小越先执行。shouldFilter 方法返回 true 表示该过滤器始终生效。在 run 方法中,通过 RequestContext 获取响应对象,并在响应头中添加 CORS 相关的字段。

后端跨域解决方案 - 微服务自身配置

Spring Boot 微服务配置 CORS

Spring Boot 是构建微服务的常用框架之一,在 Spring Boot 微服务中配置 CORS 也非常方便。

通过配置类配置

可以创建一个 CORS 配置类,如下所示:

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 corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

        return new CorsFilter(urlBasedCorsConfigurationSource);
    }
}

在这个配置类中,创建了一个 CorsFilter,并配置了允许所有来源、所有方法和所有请求头,同时允许携带认证信息。UrlBasedCorsConfigurationSource 用于注册 CORS 配置,/** 表示对所有请求路径都应用该配置。

在控制器级别配置

除了全局配置外,还可以在控制器级别配置 CORS。例如,在一个控制器类上添加 @CrossOrigin 注解:

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST}, allowedHeaders = "*")
public class ExampleController {

    @GetMapping("/example")
    public String example() {
        return "This is an example response";
    }
}

在上述代码中,@CrossOrigin 注解用于配置 CORS。origins 设置为 * 表示允许所有来源,methods 列出了允许的 HTTP 方法,allowedHeaders 设置为 * 表示允许所有请求头。这样配置后,只有该控制器下的接口会应用此 CORS 规则。

Node.js 微服务配置 CORS

在 Node.js 开发的微服务中,可以使用 cors 中间件来解决跨域问题。

安装和使用 cors 中间件

首先,通过 npm 安装 cors 中间件:

npm install cors

然后,在 Node.js 应用中使用它。假设使用 Express 框架:

const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors());

app.get('/api/example', (req, res) => {
    res.send('This is an example response');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,通过 app.use(cors())cors 中间件应用到整个 Express 应用中,这样所有的接口都允许跨域请求。cors 中间件有很多可配置的选项,例如可以指定允许的来源:

const corsOptions = {
    origin: 'http://allowed - origin.com'
};
app.use(cors(corsOptions));

这样就只允许 http://allowed - origin.com 来源的请求跨域访问。

跨域中的认证与授权问题

认证与跨域的关系

在跨域场景下,认证变得更加复杂。认证是验证用户身份的过程,常见的认证方式有基于 Cookie 和 Session 的认证、JWT(JSON Web Token)认证等。

基于 Cookie 和 Session 的认证

在传统的基于 Cookie 和 Session 的认证方式中,用户登录后,服务器会生成一个 Session 并将 Session ID 以 Cookie 的形式返回给浏览器。后续浏览器每次请求时都会带上这个 Cookie,服务器通过解析 Cookie 中的 Session ID 来验证用户身份。然而,在跨域情况下,由于浏览器的同源策略,不同域之间不能共享 Cookie,这就导致认证信息无法在跨域请求中传递。例如,前端应用部署在 http://frontend.example.com,后端微服务部署在 http://backend.example.com,当用户从前端登录后,后端生成的 Session ID 存储在 http://backend.example.com 的 Cookie 中,前端再次发起跨域请求时,浏览器不会自动带上这个 Cookie,从而导致后端无法验证用户身份。

JWT 认证

JWT 是一种用于在网络应用中传递声明的开放标准(RFC 7519)。它由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。用户登录后,服务器会生成一个包含用户身份信息的 JWT,并返回给前端。前端在后续的请求中,将 JWT 放在请求头(通常是 Authorization 头)中发送给后端。由于 JWT 是包含在请求头中的,不受同源策略限制,所以可以在跨域请求中正常传递。例如:

// 前端发送带有 JWT 的请求
fetch('http://cross - domain - api.com/api', {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer <your - jwt - token>'
    }
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

后端接收到请求后,通过验证 JWT 的签名来确保其真实性和完整性,然后从 JWT 的载荷中获取用户身份信息进行认证。

授权与跨域

授权是在认证通过后,确定用户是否有权限访问特定资源的过程。在跨域场景下,授权同样需要考虑如何在不同域之间传递授权信息。

基于角色的访问控制(RBAC)

RBAC 是一种常见的授权模型,它通过将用户分配到不同的角色,每个角色具有不同的权限,来控制用户对资源的访问。在微服务架构的跨域场景中,当一个微服务接收到跨域请求时,需要根据用户的角色来确定是否授权访问。例如,一个订单微服务可能只允许具有“管理员”或“销售人员”角色的用户访问订单修改接口。为了实现这一点,前端在请求中需要携带足够的授权信息(如包含角色信息的 JWT),微服务在接收到请求后,解析授权信息并根据 RBAC 规则进行授权判断。

细粒度授权

除了基于角色的粗粒度授权外,还可能需要进行细粒度授权,即根据用户的具体属性或请求的具体参数来决定是否授权。例如,在一个文件管理微服务中,可能只允许文件的所有者或具有特定权限的用户删除文件。在跨域场景下,前端需要将相关的授权信息(如文件所有者的标识)传递给后端微服务,后端微服务在接收到跨域请求后,结合这些信息进行细粒度的授权判断。

跨域性能优化

减少跨域请求次数

合并请求

在微服务架构中,前端可能需要从多个不同域的微服务获取数据。为了减少跨域请求次数,可以将多个相关的请求合并为一个。例如,假设前端需要从用户微服务获取用户基本信息,从订单微服务获取用户最近的订单信息。可以在后端创建一个聚合服务,该服务负责调用用户微服务和订单微服务,并将获取到的数据进行整合后返回给前端。这样前端只需要发起一个请求到聚合服务,从而减少了跨域请求次数。

缓存数据

对于一些不经常变化的数据,可以在前端进行缓存。例如,一些静态配置信息、商品分类数据等。当下次需要这些数据时,前端先从本地缓存中获取,如果缓存中没有再发起跨域请求。这样可以避免不必要的跨域请求,提高性能。在后端微服务中,也可以对一些常用的数据进行缓存,如使用 Redis 等缓存工具。当接收到跨域请求时,先检查缓存中是否有数据,如果有则直接返回,减少对数据库等后端存储的访问,从而提高响应速度。

优化预检请求

合理设置预检请求缓存

在复杂请求(如使用 PUT、DELETE 方法或特定 Content - Type 的请求)中,浏览器会先发送预检请求(OPTIONS 请求)。服务器可以通过设置 Access - Control - Max - Age 响应头来告诉浏览器预检请求的结果可以缓存多长时间。例如,在 Spring Boot 微服务中,可以在 CORS 配置中设置:

CorsConfiguration corsConfiguration = new CorsConfiguration();
// 其他 CORS 配置...
corsConfiguration.setMaxAge(3600L); // 设置预检请求结果缓存 1 小时

这样,在 1 小时内,浏览器对于相同的跨域请求不会再次发送预检请求,从而减少了请求次数和响应时间。

避免不必要的预检请求

前端在发起请求时,尽量遵循简单请求的规则,避免触发不必要的预检请求。例如,尽量使用 GET、POST 方法,并且将 Content - Type 设置为 application/x - www - form - urlencodedmultipart/form - datatext/plain 等简单请求支持的类型。如果确实需要使用复杂请求,在后端配置 CORS 时,要精确设置允许的请求头和方法,避免过于宽松的配置导致不必要的预检请求。

跨域安全问题及防范

跨站脚本攻击(XSS)与跨域

XSS 攻击原理

XSS 攻击是指攻击者在网页中注入恶意脚本,当用户访问该网页时,恶意脚本会在用户浏览器中执行,从而获取用户的敏感信息,如 Cookie、会话令牌等。在跨域场景下,XSS 攻击可能会更加隐蔽。例如,一个恶意网站可能通过跨域请求获取到目标网站的页面内容,并在其中注入恶意脚本。当用户访问目标网站时,恶意脚本就会被执行。

防范 XSS 攻击

  1. 输入验证和过滤:后端在接收到用户输入的数据时,要进行严格的验证和过滤,防止恶意脚本注入。例如,使用正则表达式过滤掉包含 <script> 标签等危险字符的输入。在 Java 中,可以使用 StringEscapeUtils 类对输入进行转义:
import org.apache.commons.text.StringEscapeUtils;

String input = "<script>alert('XSS')</script>";
String safeInput = StringEscapeUtils.escapeHtml4(input);
  1. 输出编码:在将数据输出到页面时,要进行编码,确保特殊字符被正确转义。例如,在 HTML 中,将 < 转义为 &lt;> 转义为 &gt; 等。在 JavaScript 中,可以使用 encodeURIComponent 对 URL 进行编码。

跨站请求伪造(CSRF)与跨域

CSRF 攻击原理

CSRF 攻击是指攻击者诱导用户访问一个包含恶意请求的页面,当用户在已登录目标网站的情况下访问该页面时,浏览器会自动带上目标网站的 Cookie 等认证信息,从而使恶意请求以用户的身份在目标网站上执行。在跨域场景下,由于同源策略的限制,CSRF 攻击相对较难直接实施,但攻击者可能会利用一些漏洞或通过其他手段绕过限制。例如,攻击者可能利用一个存在 CORS 配置漏洞的网站,构造一个恶意请求,当用户访问该恶意页面时,请求会以用户的身份发送到存在漏洞的目标网站。

防范 CSRF 攻击

  1. CSRF 令牌:后端在生成页面时,为每个用户生成一个唯一的 CSRF 令牌,并将其存储在用户的 Session 中。同时,在页面的表单或 AJAX 请求中包含这个令牌。当用户提交请求时,后端验证请求中的令牌与 Session 中的令牌是否一致。如果不一致,则认为是 CSRF 攻击,拒绝请求。在 Spring Boot 中,可以通过配置 CsrfFilter 来启用 CSRF 保护:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.filter.CharacterEncodingFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        filter.setEncoding("UTF - 8");
        filter.setForceEncoding(true);
        http.addFilterBefore(filter, CsrfFilter.class)
          .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
          .and()
           // 其他安全配置...
    }
}
  1. SameSite Cookie 属性:设置 Cookie 的 SameSite 属性为 StrictLaxStrict 模式下,Cookie 仅在同站请求中发送;Lax 模式下,Cookie 在跨站 GET 请求中发送,但在跨站 POST、PUT、DELETE 等请求中不发送。这样可以有效防止 CSRF 攻击。例如,在设置 Cookie 时,可以添加 SameSite 属性:
Cookie cookie = new Cookie("sessionId", "123456");
cookie.setSameSite("Lax");
response.addCookie(cookie);

通过以上全面的分析和解决方案,我们可以有效地解决微服务架构中的跨域问题,并处理与之相关的认证、授权、性能和安全等多方面的问题。在实际应用中,需要根据具体的业务需求和系统架构选择合适的解决方案,并不断优化和完善。