You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 20:40:54