SpringBoot+Angular JWT认证遇Invalid CORS请求问题求助
问题:SpringBoot + Angular JWT认证的CORS错误
我正在实现基于SpringBoot、Angular(搭配Docker部署的PostgreSQL数据库)的JWT认证功能,当前使用HTTP而非HTTPS,不确定是否会引发问题。后端认证逻辑看似正常,但从浏览器发起登录请求时,服务器返回200状态码,却提示“Invalid CORS request”。另外,我通过后端服务器在同一端口提供前端服务。
后端WebSecurityConfig配置
package fr.makizart.restserver; import org.springframework.beans.factory.annotation.Autowired; 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.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; @Configuration @EnableWebSecurity public class WebSecurityConfig { @Autowired private CustomUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); return authenticationManagerBuilder.build(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager); http .cors(cors -> cors.configurationSource(corsConfigurationSource())) // Updated CORS configuration usage .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth // .requestMatchers("/login", "/api/login").permitAll() // Autoriser l'accès public aux endpoints de login .requestMatchers("/api/login").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().permitAll() ) .formLogin(form -> form .loginPage("/#/login") .defaultSuccessUrl("/#/admin", true) .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/#/login") .permitAll() ) .httpBasic(httpBasic -> httpBasic.realmName("MyApp")) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } // CORS configuration bean @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); // Configuration des origines autorisées configuration.setAllowedOrigins(Arrays.asList( // "http://localhost:4200", // Localhost pour Angular // "http://172.18.0.1:4200", // IP de votre front-end Angular "http://172.18.0.1:8080" // IP de votre back-end Spring Boot )); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); configuration.setExposedHeaders(Arrays.asList("Authorization")); configuration.setAllowCredentials(true); // Autorise les cookies et les headers comme les tokens UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
JwtAuthenticationFilter实现
package fr.makizart.restserver; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; private final String secretKey = "YourSecretKey"; // Replace with your secret key public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { // Extract username and password from request Map<String, String> credentials = new ObjectMapper().readValue(request.getInputStream(), HashMap.class); String username = credentials.get("username"); String password = credentials.get("password"); // Authenticate the user UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); return authenticationManager.authenticate(authenticationToken); } catch (IOException e) { throw new RuntimeException(e); } } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { // Generate JWT token User user = (User) authResult.getPrincipal(); String token = Jwts.builder() .setSubject(user.getUsername()) .setExpiration(new Date(System.currentTimeMillis() + 864_000_000)) // 10 days .signWith(SignatureAlgorithm.HS512, secretKey.getBytes()) .compact(); // Create response body Map<String, String> responseBody = new HashMap<>(); responseBody.put("token", "Bearer " + token); responseBody.put("message", "Authentication successful"); System.out.println("Token généré : " + token); // Set response content type to JSON response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); // Write response body response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody)); } }
Angular端auth.service.ts代码
// `makizapp/WebView/site/src/app/services/auth.service.ts` import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthService { private loginUrl = 'http://172.18.0.1:8080/api/login'; // URL of the backend API constructor(private http: HttpClient) {} login(username: string, password: string): Observable<any> { return this.http.post<any>(this.loginUrl, { username, password }, { headers: new HttpHeaders({ 'Content-Type': 'application/json', }), withCredentials: true, // Inclut les credentials dans les requêtes }).pipe( tap((response: any) => { if (response && response.token) { localStorage.setItem('token', response.token); } else { console.error('Token non reçu dans la réponse'); } }), catchError((error) => { console.error('Erreur lors du login :', error); return throwError(error); }) ); } isLoggedIn(): boolean { return !!localStorage.getItem('token'); } logout(): void { localStorage.removeItem('token'); } getAuthHeaders(): HttpHeaders { const token = localStorage.getItem('token'); return new HttpHeaders({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }); } }
解决方案
1. 修正CORS允许来源配置
你的CORS配置仅添加了后端自身地址,未包含Angular前端的实际访问地址,导致跨域校验失败。需要将前端地址加入允许列表:
configuration.setAllowedOrigins(Arrays.asList( "http://localhost:4200", // 本地开发Angular地址 "http://172.18.0.1:4200", // Docker环境下Angular容器IP "http://172.18.0.1:8080" // 后端自身地址 ));
开发阶段可临时用*测试(生产环境禁止,需固定具体地址):
configuration.setAllowedOrigins(Arrays.asList("*"));
2. 放行OPTIONS预检请求
浏览器会先发送OPTIONS预检请求校验跨域权限,需确保Spring Security不会拦截该请求:
导入org.springframework.http.HttpMethod后,在权限配置中添加:
.authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/api/login").permitAll() .requestMatchers("/api/login").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().permitAll() )
3. 同一端口服务的跨域优化
既然后端和前端共用同一端口,建议前端使用相对路径发起请求,避免浏览器判定为跨域:
private loginUrl = '/api/login'; // 替换硬编码的IP地址
4. HTTP的安全风险说明
HTTP不会直接导致CORS错误,但生产环境必须切换为HTTPS,否则JWT令牌在传输过程中易被窃取,存在严重安全隐患。开发环境可临时使用HTTP,上线前务必替换。
5. 修复Angular代码语法问题
你的Angular代码存在导入缺失和注释残留问题,需补充必要导入并清理无效代码:
- 确保导入
catchError和throwError:
import { tap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs';
内容的提问来源于stack exchange,提问作者Hanae Melehi
相关产品推荐
相关产品推荐

