บันทึกการฝ่า CORS และ Proxy ใน Vue 3 + Vite + Spring Boot
เมื่อสร้างแอพที่รวม 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:
- Development: Proxy ผ่าน Vite dev server → ใน development Vite ทำหน้าที่เป็น proxy และ backend ดูเหมือน origin เดียวกัน
- 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 หลัก:
-
Access-Control-Allow-Originถูกตั้งอย่างถูกต้องใน backend response หรือไม่? → Verify ด้วย browser DevTools → Network tab → response headers -
OPTIONS response ถูก return หรือไม่? → ตรวจสอบว่า preflight request (OPTIONS) ก็อยู่ใน Network tab ด้วย
-
allowedOrigins(”*”) + allowCredentials(true)? → ใช้ทั้งสองพร้อมกันเป็นไปไม่ได้
-
Trailing slash ใน URL? →
domain.comและdomain.com/ต่างกัน -
ถ้าใช้ Spring Security CORS setting ถูกเพิ่มฝั่ง Security หรือไม่? → WebMvcConfigurer เพียงอย่างเดียวไม่เพียงพอถ้ามี Security
-
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