什么是 CORS?
CORS,全称是“跨源资源共享”(Cross-Origin Resource Sharing),是一种Web应用程序的安全机制,用于控制不同源的资源之间的交互。
在Web应用程序中,CORS定义了一种机制,通过该机制,浏览器能够限制哪些外部网页可以访问来自不同源的资源。源由协议、域名和端口组成。当一个网页请求另一个网页上的资源时,浏览器会检查请求是否符合CORS规范,以确定是否允许该请求。
CORS的工作原理是:当浏览器发送一个跨域请求时,它会附加一些额外的头部信息到请求中,这些头部信息包含了关于请求的来源和目的的信息。服务器可以检查这些头部信息并决定是否允许该请求。如果服务器允许请求,它会返回一个响应,其中包含一个名为“Access-Control-Allow-Origin”的头部信息,该信息指定了哪些源可以访问该资源。浏览器会检查返回的“Access-Control-Allow-Origin”头部信息,以确定是否允许该跨域请求。
通过使用CORS,开发人员可以控制哪些外部网页可以访问他们的资源,从而提高应用程序的安全性。
Spring Boot 如何配置 CORS?
Spring Boot对于跨域请求的支持可以通过两种配置方式来实现:
- 注解配置:可以使用@CrossOrigin注解来启用CORS。例如,在需要支持跨域请求的方法上添加@CrossOrigin注解,并配置好origins和maxAge等参数。
- 全局配置:可以通过实现WebMvcConfigurer接口并注册一个WebMvcConfigurer bean来配置CORS的全局设置。在实现类中覆盖addCorsMappings方法,通过CorsRegistry对象添加映射规则。默认情况下,所有方法都支持跨域,并且GET、POST和HEAD请求是被允许的。如果需要自定义,可以配置CorsRegistry对象来指定允许的域名、端口和请求方法等。
- 过滤器配置:可以通过
CorsFilter
bean来配置CORS的过滤器。这种方式可以更加灵活地控制CORS的配置,例如只允许特定的域名进行跨域访问等。
前端代码
request.ts
import axios from "axios";
const myAxios = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL,
timeout: 10000,
// 携带cookie
withCredentials: true,
});
// 添加请求拦截器
myAxios.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 添加响应拦截器
myAxios.interceptors.response.use(
function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
}
);
export default myAxios;
user.ts
import myAxios from "@/utils/request";
/**
* 获取用户列表
* @param username
*/
export const searchUsers = async () => {
return myAxios.request({
url: `/admin/findAll`,
method: "GET",
});
};
};
vue页面使用
<script setup lang="ts">
import { searchUsers } from "@/api/user";
searchUsers().then((res) => {
console.log(res);
});
</script>
一、 在 Controller 上添加 @CrossOrigin 注解
这种方式适合只有一两个rest接口需要跨域或者没有网关的情况下
@RestController
@CrossOrigin(origins = {"http://127.0.0.1:9527"}, allowCredentials = "true")
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello";
}
}
@CrossOrigin
注解有几个属性,允许你更精细地控制跨域行为:
* origins: 允许的源列表,可以是域名、IP 或其他标识符。多个源可以使用逗号分隔。
* methods: 允许的 HTTP 方法列表。例如,只允许 GET 请求。
* allowedHeaders: 允许的请求头列表。默认情况下,允许所有请求头。
* allowCredentials:是否允许携带cookie;值为true、false的字符串
* maxAge: 预检请求的缓存时间(以秒为单位)。默认是 86400 秒(24小时)
二、增加 WebMvcConfigurer 全局配置
如果有大量的rest接口的时候,显然第一种方案已经不适合了,工作量大,也容易出错,那就通过全局配置的方式,允许SpringBoot端所有的rest接口都支持跨域访问
addCorsMappings
是 Spring Boot 中用于配置跨域请求的方法。它允许你指定哪些路径的请求需要进行跨域处理,以及如何处理这些请求。
@Configuration
public class CrosConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许所有请求路径跨域访问
registry.addMapping("/**")
// 是否携带Cookie,默认false
.allowCredentials(true)
// 允许的请求头类型
.allowedHeaders("*")
// 预检请求的缓存时间(单位:秒)
.maxAge(3600)
// 允许的请求方法类型
.allowedMethods("*")
// 允许哪些域名进行跨域访问
.allowedOrigins("http://127.0.0.1:5500");
}
}
假如我们配置了 addCorsMappings ,项目里还用了 Interceptor,此时就会发生问题!
例如部分接口需要 jwt 权限验证:
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/user/**").
excludePathPatterns("/login");
}
正常请求是没问题的,但是部分请求没带 token 的话,此时浏览器直接报跨域问题,好像 addCorsMappings 失效了一样。
主要原因是因为请求顺序导致的,请求会先进入拦截器,默认配置了 addCorsMappings 操作后,实际上会加了一个 CorsInterceptor,但是这个拦截器的优先级在最后。
所以,如果一个请求被前面的拦截器拦截后,直接返回,就不会经过 CorsInterceptor,这样一来返回的响应头上就不包含跨域的相关信息,因此浏览器就会继续报跨域错误!
此时有一种方法,就是在 JwtInterceptor 内直接放行预检请求,让它能访问到 CorsInterceptor 添加响应头。
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
还有一种更优雅的方式就是使用 CorsFilter
Filter 是过滤器,它是属于 servlet,而 Interceptor 是属于 Spring 的,因此 Filter 执行的优先级高于 Interceptor。
三、过滤器配置
在Spring Boot中,CorsFilter用于处理跨域请求。它是一个过滤器,用于在Spring应用程序中启用CORS(跨源资源共享)支持。
/**
* 简单跨域就是GET,HEAD和POST请求,但是POST请求 的"Content-Type"只能是
* application/x-www-form-urlencoded, multipart/form-data 或 text/plain
* 反之,就是非简单跨域,此跨域有一个预检机制,会发两次请求,一次OPTIONS请求,一次真正的请求,
* OPTIONS请求服务器确认允许的请求方法,请求域等信息,符合要求发真正的请求,不符合报 cors 错误
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter(){
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOriginPattern("http://127.0.0.1:5500");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",config);
return new CorsFilter(source);
}
注意事项
当我们没有配置跨域的时候会提示:
Access to XMLHttpRequest at 'http://localhost:8080/hello' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
当我们前端开启 withCredentials:true
的时候,后端没有配置allowCredentials为true
会提示:
Access to XMLHttpRequest at 'http://localhost:8080/hello' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
当我们在后端配置了allowCredentials(true)
那么就不能配置allowedOrigins("*")
,必须指定来源 或者使用 allowedOriginPatterns(*)
jakarta.servlet.ServletException: Request processing failed: java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
以上这种方案如果微服务多的话,需要在每个服务的主类上都加上这么段代码,增加了维护量。
以上三种方案都是在SpringBoot的基础上实现的解决方案,在模块较多或者接口较多的情况下不易维护。
既然SpringCloud自带Gateway,下面就讲讲使用Gateway的跨域解决方案。
Spring cloud Gateway 配置CORS
过滤器配置
这种方案跟方案三有些类似,只不过是放到了Gateway端,对于有多个微服务模块的情况下,就大大减少了SpringBoot模块端的代码量,让各个模块更集中精力做业务逻辑实现。这个方案只需要在Gateway里添加Filter代码类即可。
public class CorsWebFilter implements WebFilter {
private static final String ALL = "*";
private static final String MAX_AGE = "18000L";
@Override
public Mono<Void> filter(ServerWebExchange ctx, WebFilterChain chain) {
ServerHttpRequest request = ctx.getRequest();
String path = request.getPath().value();
ServerHttpResponse response = ctx.getResponse();
if ("/favicon.ico".equals(path)) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(ctx);
}
HttpHeaders requestHeaders = request.getHeaders();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, ALL);
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
}
}
或者这样
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Configuration
public class BeanConfig {
/**
* 跨域配置
*
* @return
*/
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(ctx);
}
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
String origin = requestHeaders.getOrigin();
// 设置允许跨域
headers.setAccessControlAllowCredentials(true);
headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
if (requestMethod != null) {
headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
headers.set(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "180");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
};
}
}
Gateway配置文件
修改配置文件即可,结合配置中心使用,可以实现动态修改。
application.yml
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "http://domain.com"
allowedMethods:
- GET
- POST