部署在Azure上的Spring Boot+React应用CORS认证问题求助
部署Azure后Spring Boot+React应用OAuth2登录及CORS问题排查求助
问题背景
前端部署为Azure Static Web App,后端Spring Boot部署为Azure Web App。本地测试所有功能正常,但部署到Azure后登录相关功能异常:
- 应用核心功能(产品添加至订单、查看/编辑订单)需认证,点击添加产品按钮可正常跳转至Google OAuth2登录页面
- 选择账号登录后,无法查看订单,系统持续要求登录
- 直接在浏览器中访问后端已部署的
/api/clients/me/端点,能正常显示刚登录的用户信息;但前端通过axios发起相同请求时,触发CORS错误
具体错误信息
Access to XMLHttpRequest at 'https://backendurl.azurewebsites.net/api/clients/me/' from origin 'https://mango.5.azurestaticapps.net' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.
已尝试方案
- 临时禁用后端CSRF防护,问题仍未解决
相关代码
后端SecurityConfig.java
package com.xavier.client_backend.config; import com.xavier.client_backend.domain.entities.ClientEntity; import com.xavier.client_backend.repositories.ClientRepository; 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.configurers.AbstractHttpConfigurer; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import java.util.List; @Configuration @EnableWebSecurity public class SecurityConfig { private final ClientRepository clientRepository; public SecurityConfig(ClientRepository clientRepository) { this.clientRepository = clientRepository; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .cors(cors -> { CorsConfigurationSource source = request -> { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("<redacted_url>")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); return configuration; }; cors.configurationSource(source); }) .authorizeHttpRequests(auth -> { auth.requestMatchers("/api/products/**").permitAll(); auth.anyRequest().authenticated(); }) .oauth2Login(oauth2 -> oauth2.successHandler(successHandler())) .logout(logout -> logout .logoutUrl("/logout") .invalidateHttpSession(true) .clearAuthentication(true) .deleteCookies("JSESSIONID") .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) ); return http.build(); } @Bean public AuthenticationSuccessHandler successHandler() { return (request, response, authentication) -> { OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); String email = oAuth2User.getAttribute("email"); ClientEntity client = clientRepository.findByEmail(email); if (client == null) { client = new ClientEntity(); client.setName(oAuth2User.getAttribute("name")); client.setEmail(email); clientRepository.save(client); } SimpleUrlAuthenticationSuccessHandler handler = new SimpleUrlAuthenticationSuccessHandler(); handler.setDefaultTargetUrl("<redacted_url>"); handler.onAuthenticationSuccess(request, response, authentication); }; } }
前端ProductList.js
import React, { useEffect, useState } from "react"; import axios from "axios"; const ProductList = ({ isAuthenticated }) => { const [products, setProducts] = useState([]); const [client, setClient] = useState(null); const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(""); useEffect(() => { const fetchClientAndProducts = async () => { try { const clientResponse = await axios.get( `https://backendurl.azurewebsites.net/api/clients/me/`, { withCredentials: true } ); if (clientResponse.status === 200) { setClient(clientResponse.data); const productsResponse = await axios.get( `https://backendurl.azurewebsites.net/api/products` ); const productsData = await productsResponse.data; setProducts(productsData); } else { setClient(null); } } catch (error) { console.error("Error fetching client or products:", error); setClient(null); } }; fetchClientAndProducts(); }, []); const handleAddToOrder = async (productId, productName) => { if (!isAuthenticated) { window.location.href = "https://backendurl.azurewebsites.net/oauth2/authorization/google"; return; } try { // Check if there is an existing order for the client const ordersResponse = await axios.get( `https://backendurl.azurewebsites.net/api/orders/client/${client.id}`, { withCredentials: true } ); const orders = ordersResponse.data; if (orders.length > 0) { // There is an existing order, update it const existingOrder = orders[0]; // Assuming only one open order at a time const updatedOrderItems = [...existingOrder.orderItems]; const existingItem = updatedOrderItems.find( (item) => item.productId === productId ); if (existingItem) { existingItem.quantity += 1; } else { updatedOrderItems.push({ productId, quantity: 1 }); } const updatedOrder = { ...existingOrder, orderItems: updatedOrderItems, }; await axios.put( `https://backendurl.azurewebsites.net/api/orders/${existingOrder.id}`, updatedOrder, { withCredentials: true } ); setToastMessage(`${productName} added to existing order`); setShowToast(true); setTimeout(() => setShowToast(false), 3000); // Hide toast after 3 seconds } else { // No existing order, create a new one const newOrder = { clientId: client.id, orderDate: new Date().toISOString(), orderItems: [{ productId, quantity: 1 }], }; const response = await axios.post( `https://backendurl.azurewebsites.net/me/api/orders`, newOrder, { withCredentials: true, } ); if (response.status === 201) { setToastMessage(`${productName} added to new order`); setShowToast(true); setTimeout(() => setShowToast(false), 3000); // Hide toast after 3 seconds } else { console.error("Failed to add product to new order"); } } } catch (error) { console.error("Error adding product to order:", error); } }; return ( <div className="container mt-4"> <div className="row"> {products.map((product) => ( <div className="col-md-3 mb-4" key={product.id}> <div className="card h-100 text-center"> <div className="card-body"> <h5 className="card-title">{product.name}</h5> <p className="card-text">Price: R{product.price.toFixed(2)}</p> <button className="btn btn-primary" onClick={() => handleAddToOrder(product.id, product.name)} > Add to Order </button> </div> </div> </div> ))} </div> <div className="toast-container position-fixed bottom-0 end-0 p-3" style={{ zIndex: 9999 }} > <div className={`toast ${showToast ? "show" : "hide"}`} role="alert" aria-live="assertive" aria-atomic="true" > <div className="toast-header"> <strong className="me-auto">Notification</strong> <button type="button" className="btn-close" onClick={() => setShowToast(false)} ></button> </div> <div className="toast-body">{toastMessage}</div> </div> </div> </div> ); }; export default ProductList;
烦请帮忙排查问题,感谢!
内容的提问来源于stack exchange,提问作者XavierReynolds
相关产品推荐
相关产品推荐

