Tech Blog

Vue 3 + Vite + Spring Boot でCORSとProxyを突破するまでの記録

Vue3 Spring Boot Vite CORS

はじめに

フロントエンドをVue 3 + Vite(ポート5173)、バックエンドをSpring Boot(ポート8082)で分離して開発していると、
最初に必ずぶつかるのがCORSエラーです。

Access to XMLHttpRequest at 'http://localhost:8082/api/films'
from origin 'http://localhost:5173' has been blocked by CORS policy

この記事は、このエラーを突破するまでの記録と、環境ごとの正しい対処法をまとめたものです。


そもそもCORSとは

CORS(Cross-Origin Resource Sharing)は、ブラウザのセキュリティ機能です。

http://localhost:5173 からのリクエストが http://localhost:8082 に行くのは別オリジンなので、ブラウザがブロックする」という話です。

重要なポイント:CORSはブラウザが行う制限です。
サーバー間通信(curlやPostmanのようなツール)ではCORSは発生しません。


開発時の解決策:Vite の devProxy

開発時は Viteのプロキシ機能 を使うのが最もシンプルです。

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

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8082',
        changeOrigin: true,
        // rewriteは今回不要(バックエンドも /api プレフィックスを使うため)
      }
    }
  }
})

この設定により:

  • ブラウザは http://localhost:5173/api/films にリクエストを送る
  • Viteサーバーが受け取り、http://localhost:8082/api/films に転送する
  • ブラウザから見ると「同一オリジン(ポート5173)への通信」なのでCORSが発生しない

フロントエンド側のAPI呼び出し

// api/films.ts
export async function fetchFilms(): Promise<Film[]> {
  // 環境によってベースURLを切り替え
  const baseUrl = import.meta.env.VITE_API_BASE_URL ?? ''
  const res = await fetch(`${baseUrl}/api/films`)
  if (!res.ok) throw new Error('API error')
  return res.json()
}

開発時は VITE_API_BASE_URL を設定しないので空文字になり、/api/films → Viteプロキシ経由でSpring Bootへ届きます。


本番時の解決策:Spring Boot の CORS 設定

本番環境では、フロントとバックが別ドメイン(または別ポート)になる可能性があります。

方法1:グローバルCORS設定

@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)
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}
# application.yml
app:
  cors:
    allowed-origins: http://localhost:5173

# application-prod.yml
app:
  cors:
    allowed-origins: https://your-production-frontend.com

方法2:@CrossOrigin アノテーション(コントローラ単位)

@RestController
@CrossOrigin(origins = "${app.cors.allowed-origins}")
@RequestMapping("/api/films")
public class FilmController {
    // ...
}

つまずいたポイント

① プリフライトリクエスト(OPTIONS)を忘れる

CORSでは、実際のリクエストの前に OPTIONS メソッドで「このリクエストは許可されていますか?」という事前確認が来ます。

Spring Securityを使っている場合、OPTIONS リクエストが認証に弾かれることがあります。

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())  // REST APIならCSRF無効化
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()  // OPTIONSは全許可
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }
}

allowedOrigins("*")allowCredentials(true) は共存できない

ワイルドカードと認証情報の送信は同時に使えません。

When allowCredentials is true, allowedOrigins cannot contain the special value "*"

認証情報(セッションCookieなど)を使う場合は、具体的なオリジンを指定してください。

③ 本番URLのスラッシュ末尾に注意

# これはNGになることがある
allowed-origins: https://example.com/

# 末尾スラッシュなし
allowed-origins: https://example.com

環境変数による切り替え

# .env(開発時)
VITE_API_BASE_URL=

# .env.production(本番ビルド時)
VITE_API_BASE_URL=https://api.example.com

本番ビルドでは VITE_API_BASE_URL に本番APIのURLを入れると、プロキシ不要で直接バックエンドを呼びます。


まとめ

環境解決策
開発(localhost)Viteの server.proxy 設定
本番(同一ドメイン)nginx等のリバースプロキシで /api を振り分け
本番(別ドメイン)Spring BootのCORS設定 + allowed-origins 本番URL指定

CORSエラーは「ブラウザのセキュリティ機能」なので、誤魔化すのではなく正しく許可することが重要です。
開発時はViteプロキシで、本番時はSpring Boot側で設定するのが責務的に正しい分割です。


このアプリでの実装

vite.config.ts(実際のコード)

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

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8082',  // Spring Bootのポート
        changeOrigin: true
        // rewrite は不要(パスはそのまま転送)
      }
    }
  }
})

/api から始まるリクエストはすべて http://localhost:8082 へ転送されます。 changeOrigin: true によりリクエストの Origin ヘッダーがターゲットに合わせて書き換えられます。

CorsConfig.java(実際のコード)

// config/CorsConfig.java
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /**
     * ローカル開発画面向けに API 配下の CORS ルールを登録する。
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:5173")
                .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

ポイント: allowCredentials(true) を指定しているため、allowedOrigins("*") は使えません。 具体的なオリジン http://localhost:5173 を指定しています。

開発では両方設定している理由: Viteプロキシは「ブラウザ → Vite → Spring Boot」のフローで CORSを回避しますが、Spring Boot 側にも CORS 設定があることで、 将来的にフロントとバックを別オリジンで動かすケースや、Swagger UI 等の直接アクセスにも対応できます。


このシリーズの記事マップ

dvdrental 管理アプリと対になるエンドユーザー向けDVDレンタルアプリを作っている話 — Vue 3 + Spring Boot の全体構成と記事マップ

気軽にメッセージください

技術相談・ご感想・ご質問があればメッセージをお願いします。