Tech Blog

บันทึกการฝ่า CORS และ Proxy ใน Vue 3 + Vite + Spring Boot

by Tech Writer
Vue3 Spring Boot Vite CORS

เมื่อสร้างแอพที่รวม Vue 3 + Vite บน frontend กับ Spring Boot บน backend CORS คือกำแพงแรกที่ต้องเจอ

บทความนี้รวบรวมสิ่งที่ทำเพื่อแก้ปัญหา CORS และ proxy settings เมื่อรวม Vue 3 + Vite (frontend) และ Spring Boot (backend)

CORS คืออะไร?

CORS (Cross-Origin Resource Sharing) คือ security mechanism ของ browser เมื่อ frontend ที่รันที่ http://localhost:5173 พยายาม make request ไปยัง backend ที่รันที่ http://localhost:8080 browser จะ block มันในฐานะ cross-origin request

เพื่ออนุญาตสิ่งนี้ backend ต้อง return Access-Control-Allow-Origin header ใน response

สองแนวทาง

มีสองแนวทางหลักสำหรับการจัดการ CORS ใน Vue 3 + Vite + Spring Boot development:

  1. Development: Proxy ผ่าน Vite dev server → ใน development Vite ทำหน้าที่เป็น proxy และ backend ดูเหมือน origin เดียวกัน
  2. Production: ตั้ง CORS headers บน Spring Boot → ใน production อนุญาต requests จาก deployed frontend URL อย่างชัดเจน

Development: Vite devProxy Settings

ระหว่าง development ตั้งค่า proxy ใน vite.config.ts เพื่อให้ Vite forward API requests ไปยัง backend

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:8080",
        changeOrigin: true,
      },
    },
  },
});

ด้วย setting นี้ requests ไปยัง /api/** จาก frontend จะไปที่ http://localhost:8080/api/** จากมุมมองของ browser เนื่องจาก requests ทั้งหมดไปที่ localhost:5173 จึงไม่มีปัญหา cross-origin

Spring Boot CORS Settings

ใน production ต้องการ CORS settings อย่างชัดเจนฝั่ง Spring Boot

Global Configuration ด้วย WebMvcConfigurer

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/api/**")
        .allowedOrigins("https://your-production-domain.com")
        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
        .allowedHeaders("*")
        .allowCredentials(true)
        .maxAge(3600);
  }
}

Per-Controller Configuration ด้วย @CrossOrigin

@RestController
@CrossOrigin(origins = "https://your-production-domain.com")
@RequestMapping("/api/films")
public class FilmController {
  // ...
}

Preflight OPTIONS Handling

ใน CORS ก่อน requests บางประเภท (ที่มี custom headers หรือ methods อื่นนอกจาก GET/POST) browser จะส่ง OPTIONS request (preflight request) ก่อนเพื่อตรวจสอบว่า actual request ถูกอนุญาตหรือไม่

ถ้า backend ไม่ response อย่างถูกต้องต่อ OPTIONS นี้ แม้ว่า GET/POST จะทำงานได้ requests ที่มี authentication headers จะ fail

ด้วย Spring Boot’s WebMvcConfigurer CORS configuration OPTIONS responses จะถูกจัดการอัตโนมัติ อย่างไรก็ตามถ้าใช้ Spring Security สิ่งสำคัญคือต้องเพิ่ม CORS setting ที่นั่นด้วย

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        .csrf(csrf -> csrf.disable())
        // ... other settings
    return http.build();
  }

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("https://your-production-domain.com"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(List.of("*"));
    configuration.setAllowCredentials(true);
    return new UrlBasedCorsConfigurationSource() {{
      registerCorsConfiguration("/api/**", configuration);
    }};
  }
}

allowedOrigins(”*”) และ allowCredentials(true) อยู่ด้วยกันไม่ได้

นี่คือจุดสำคัญที่เข้าใจผิดได้ง่าย

เมื่อตั้ง allowCredentials(true) ไม่สามารถใช้ wildcard (*) ใน allowedOrigins ได้

// ❌ สิ่งนี้ไม่ทำงาน
registry.addMapping("/api/**")
    .allowedOrigins("*")           // Wildcard
    .allowCredentials(true);       // และ allowCredentials → Error

// ✅ นี่คือวิธีที่ถูก
registry.addMapping("/api/**")
    .allowedOrigins("https://your-domain.com")  // URL ที่เฉพาะเจาะจง
    .allowCredentials(true);

Error message เมื่อสิ่งนี้เกิดขึ้นคือ When allowCredentials is true, allowedOrigins cannot contain the special value "*" เมื่อเห็น message นี้ ให้ verify ว่ามีการระบุ URL ที่เฉพาะเจาะจงใน allowedOrigins

Trailing Slash Trap ใน Production URLs

เมื่อระบุ production URLs ใน CORS settings ต้องระวัง trailing slashes

// ❌ ระวัง trailing slash
.allowedOrigins("https://your-domain.com/")  // Trailing slash คือ URL ที่ต่างกัน

// ✅ ไม่มี trailing slash
.allowedOrigins("https://your-domain.com")

URL https://your-domain.com และ https://your-domain.com/ ถูก browser รู้จักเป็น origin ที่ต่างกัน แม้ดูเป็นความแตกต่างเล็กน้อย แต่อาจเป็นสาเหตุของ “ทำงานได้บางครั้ง ล้มเหลวบางครั้ง”

Environment Variable Switching

สำหรับการจัดการ production URLs ใน CORS settings การใช้ environment variables สะดวกกว่า

Spring Boot: อ่านผ่าน @Value

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Value("${app.cors.allowed-origins}")
  private String allowedOrigins;

  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/api/**")
        .allowedOrigins(allowedOrigins.split(","))
        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
        .allowCredentials(true);
  }
}
# application-local.yml
app:
  cors:
    allowed-origins: http://localhost:5173

# application-aws.yml
app:
  cors:
    allowed-origins: https://your-production-domain.com

Frontend: จัดการด้วย Vite Environment Variables

// ใช้ prefix VITE_ เพื่อ expose ใน browser
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
# .env.development
VITE_API_BASE_URL=http://localhost:8080

# .env.production
VITE_API_BASE_URL=https://your-production-api.com

สรุปจุดที่ต้องยืนยัน

เมื่อ debug CORS จุดต่อไปนี้คือ checkboxes หลัก:

  1. Access-Control-Allow-Origin ถูกตั้งอย่างถูกต้องใน backend response หรือไม่? → Verify ด้วย browser DevTools → Network tab → response headers

  2. OPTIONS response ถูก return หรือไม่? → ตรวจสอบว่า preflight request (OPTIONS) ก็อยู่ใน Network tab ด้วย

  3. allowedOrigins(”*”) + allowCredentials(true)? → ใช้ทั้งสองพร้อมกันเป็นไปไม่ได้

  4. Trailing slash ใน URL?domain.com และ domain.com/ ต่างกัน

  5. ถ้าใช้ Spring Security CORS setting ถูกเพิ่มฝั่ง Security หรือไม่? → WebMvcConfigurer เพียงอย่างเดียวไม่เพียงพอถ้ามี Security

  6. Vite proxy ทำงานใน development หรือไม่? → ตรวจสอบว่า requests ไปที่ localhost:8080 ผ่าน Network tab

สรุป

จุดที่ CORS มักติดขัดใน Vue 3 + Vite + Spring Boot คือ:

  • Development: ใช้ Vite devProxy, production: configure backend CORS แยกต่างหาก
  • เมื่อใช้ allowCredentials ระบุ specific origin (wildcard NG)
  • ระวัง trailing slash ใน production URLs
  • ถ้าใช้ Spring Security เพิ่ม CORS ฝั่ง Security ด้วย

เมื่อเข้าใจจุดเหล่านี้ CORS errors กลายเป็นสิ่งที่ trace และแก้ไขได้ เริ่มต้นด้วยการตรวจสอบ network requests ใน browser DevTools

ส่งข้อความได้ตามสบาย

กรุณาส่งข้อความ หากมีคำปรึกษาด้านเทคนิค ความคิดเห็น หรือคำถาม