Spring Boot与Angular集成时的CORS策略问题求助
问题描述
开发Spring Boot + Angular登录项目,后端采用用户名密码认证,通过后返回JWT令牌。该流程在Postman中测试正常,但Angular发起请求时触发CORS Policy错误。
相关代码如下:
Angular组件代码
import { FormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-loginscreen', templateUrl: './loginscreen.component.html', styleUrls: ['./loginscreen.component.css'] }) export class LoginscreenComponent { constructor(private httpClient: HttpClient) { } public inputUsername: string = ""; public inputPassword: string = ""; login() { const url = 'http://localhost:8080/auth/login'; const dadosDeLogin = { username: this.inputUsername, password: this.inputPassword }; const headers = { 'content-type': 'application/json'} this.httpClient.post(url, dadosDeLogin, { headers }) .subscribe(data => { console.log(data); }) } }
SecurityFilter类代码
@Component public class SecurityFilter extends OncePerRequestFilter { @Autowired TokenService tokenService; @Autowired AdministratorRepository administratorRepository; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { var token = this.recoverToken(request); if(token != null){ var login = tokenService.validateToken(token); UserDetails user = administratorRepository.findByUsername(login); // Requests CORS response.setHeader("Access-Control-Allow-Origin", "http://localhost:4200"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH"); response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String recoverToken(HttpServletRequest request) { var authHeader = request.getHeader("Authorization"); if(authHeader == null) return null; return authHeader.replace("Bearer ", ""); } }
SecurityConfig类代码
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired SecurityFilter securityFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/auth/login")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/auth/register")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/users")).hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
解决方案
问题根源
- 预检请求未处理:Angular跨域POST请求会先发送OPTIONS预检请求,当前
SecurityFilter仅在请求带token时才设置CORS头,OPTIONS请求无token,导致响应缺少CORS头触发错误。 - CORS配置不规范:手动在Filter中设置CORS头的方式覆盖不全,Spring提供了更标准的全局CORS配置方案。
方案1:使用Spring官方全局CORS配置(推荐)
修改SecurityConfig,添加全局CORS规则,替代Filter中手动设置的CORS头:
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired SecurityFilter securityFilter; // 配置CORS源 @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); // 允许的前端域名 configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200")); // 允许的请求方法 configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的请求头 configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization")); // 允许携带凭证 configuration.setAllowCredentials(true); // 应用到所有路径 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity .csrf(csrf -> csrf.disable()) // 启用CORS配置 .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize .requestMatchers(HttpMethod.POST, "/auth/login").permitAll() .requestMatchers(HttpMethod.POST, "/auth/register").permitAll() .requestMatchers(HttpMethod.POST, "/users").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
同时删除SecurityFilter中手动设置CORS头的代码,简化后的SecurityFilter:
@Component public class SecurityFilter extends OncePerRequestFilter { @Autowired TokenService tokenService; @Autowired AdministratorRepository administratorRepository; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { var token = this.recoverToken(request); if(token != null){ var login = tokenService.validateToken(token); UserDetails user = administratorRepository.findByUsername(login); var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String recoverToken(HttpServletRequest request) { var authHeader = request.getHeader("Authorization"); if(authHeader == null) return null; return authHeader.replace("Bearer ", ""); } }
方案2:手动处理OPTIONS请求(不推荐)
若不想使用Spring全局CORS配置,需在SecurityFilter中对所有请求设置CORS头,并放行OPTIONS预检请求:
修改SecurityFilter的doFilterInternal方法:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 对所有请求设置CORS头 response.setHeader("Access-Control-Allow-Origin", "http://localhost:4200"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH"); response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); // OPTIONS预检请求直接返回200 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; } var token = this.recoverToken(request); if(token != null){ var login = tokenService.validateToken(token); UserDetails user = administratorRepository.findByUsername(login); var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); }
同时在SecurityConfig中放行所有OPTIONS请求:
.authorizeHttpRequests(authorize -> authorize .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(HttpMethod.POST, "/auth/login").permitAll() .requestMatchers(HttpMethod.POST, "/auth/register").permitAll() .requestMatchers(HttpMethod.POST, "/users").hasRole("ADMIN") .anyRequest().authenticated() )
额外检查项
- 确认Angular请求URL与后端允许的源完全一致(如均为
http://localhost:4200,不要加结尾斜杠) - 清除浏览器缓存后重新测试,避免旧缓存干扰
内容的提问来源于stack exchange,提问作者vinicius.vaga
相关产品推荐
相关产品推荐

