在使用 Spring Boot 和 Vue 开发前后端分离的项目时,跨域资源共享(CORS)问题是一个常见的挑战。接下来,我将分享我是如何一步步解决这个问题的,包括中间的一些试错过程,希望能够帮助到正在经历类似问题的你。
1. 问题描述
在我们开发的过程中,Vue 前端需要与 Spring Boot 后端通信。如果后端没有正确配置 CORS,浏览器会进行跨域检查并阻止请求,报错信息如下:
Access to XMLHttpRequest at 'http://localhost:8789/auth/register' from origin 'http://localhost:8081' 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.
2. 解决方案概述
为了解决这个问题,我们需要在 Spring Boot 应用中配置 CORS。这个过程包括创建一个 CORS 配置类,并在 Spring Security 配置类中应用这个配置。
3. 试错过程
3.1 初步尝试:简单的 CORS 配置
我首先尝试在 Spring Boot 中添加一个简单的 CORS 配置类:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
然后,在 WebSecurityConfig
中应用这个配置:
java
http.cors().configurationSource(corsConfigurationSource());
结果,前端依旧报错,没有任何变化。
3.2 细化 Security 配置
我接着尝试在 WebSecurityConfig
中进一步细化 CORS 配置:
java
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(corsConfigurationSource())
.and().csrf().disable();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
然而,前端还是无法正常发起跨域请求,这让我非常困惑。
3.3 尝试代理配置
为了确保开发过程中跨域请求能正确代理到后端,我在 Vue 项目中添加了代理配置:
首先,确保项目使用 vue-cli
创建,并确保有 vue.config.js
文件。然后添加如下代理配置:
javascript
let proxyObj = {};
proxyObj['/'] = {
target: 'http://localhost:8789/',
changeOrigin: true,
pathRewrite: {
'^/': ''
}
}
module.exports = {
devServer: {
open: true,
host: 'localhost',
port: 8081,
proxy: proxyObj,
},
}
这种配置可以使前端的跨域请求通过代理转发到后端。不过,这只是开发环境下的解决方案,并没有真正解决后端的 CORS 配置问题。
3.4 最终解决方案:完善的 CORS 和 Security 配置
经过几次尝试和查阅资料后,我最终找到了一个有效的解决方案,结合之前的经验,创建了一个完善的 CORS 和 Security 配置。
CorsConfig.java
java
package cn.techfanyi.fanyi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.addAllowedOriginPattern("*"); // 允许任何源
corsConfig.addAllowedMethod("*"); // 允许任何HTTP方法
corsConfig.addAllowedHeader("*"); // 允许任何HTTP头
corsConfig.setAllowCredentials(true); // 允许证书(cookies)
corsConfig.setMaxAge(3600L); // 预检请求的缓存时间(秒)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig); // 对所有路径应用这个配置
return source;
}
}
WebSecurityConfig.java
java
package cn.techfanyi.fanyi.config;
import cn.techfanyi.fanyi.filter.JwtRequestFilter;
import cn.techfanyi.fanyi.security.CustomAccessDeniedHandler;
import cn.techfanyi.fanyi.security.CustomAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
public WebSecurityConfig(JwtRequestFilter jwtRequestFilter,
CustomAuthenticationEntryPoint customAuthenticationEntryPoint,
CustomAccessDeniedHandler customAccessDeniedHandler) {
this.jwtRequestFilter = jwtRequestFilter;
this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
this.customAccessDeniedHandler = customAccessDeniedHandler;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeRequests(authorizedRequests ->
authorizedRequests.requestMatchers("/**").permitAll()
.anyRequest().authenticated())
.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler))
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
private CorsConfigurationSource corsConfigurationSource() {
return new CorsConfig().corsConfigurationSource();
}
}
但是又出现以下错误:
java
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.
这个错误信息表明,在 Spring Boot 的 CORS 配置中,当 allowCredentials
设置为 true
时,allowedOrigins
不能包含特殊值 "*"
, 因为浏览器不允许在 Access-Control-Allow-Origin
响应头中设置 "*"
, 同时还允许凭证(如 cookies)。此时应该使用 allowedOriginPatterns
来代替 allowedOrigins
。
具体的错误原因如下:
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.
这意味着当 allowCredentials
设置为 true
时,不能将 allowedOrigins
设置为 "*"
, 因为它不能在响应头中设置 Access-Control-Allow-Origin
为 "*"
, 同时还允许凭证。为了解决这个问题,您需要将 allowedOrigins
改为使用 allowedOriginPatterns
。
修改 CorsConfigurationSource
如下:
java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.addAllowedOriginPattern("*"); // 使用 allowedOriginPatterns 代替 allowedOrigins
corsConfig.addAllowedMethod("*");
corsConfig.addAllowedHeader("*");
corsConfig.setAllowCredentials(true);
corsConfig.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return source;
}
通过以上配置,可以解决 allowCredentials
和 allowedOrigins
中 "*"
冲突的问题,使得您的 Spring Boot 应用可以正确处理跨域请求。
通过以上配置,前端请求终于可以成功与后端通信,CORS 问题不再出现。
4. 为什么要这样修改
在 Spring Security 6 中,安全配置的方式有所变化。与之前版本相比,Spring Security 6 更加灵活和模块化。为了使 CORS 配置生效,我们需要:
- 明确指定 CORS 配置源 :在
securityFilterChain
方法中,通过http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
明确指定使用我们自定义的CorsConfigurationSource
。 - 禁用默认的 CSRF 保护 :对于大多数 API 项目,特别是无状态的 RESTful 服务,禁用 CSRF 是常见的做法。通过
http.csrf().disable()
来实现。 - 配置异常处理和会话管理:确保我们的应用是无状态的,并且正确处理认证和授权异常。
5. 结果
经过这些配置,前端可以顺利地与后端通信,避免了 CORS 错误。整个过程让我对 CORS 配置有了更深入的理解。