🍏

SpringBoot と Nuxt3 を共存させる

2024/03/29に公開

まえがき

既存の SpringBoot の View を Nuxt3 に移植したくて、でも一気にはできないので、地道にやっていくために、SpringBoot 側の画面を残しつつ、Nuxt3 も表示できるようにしたときの設定メモです。

SpringBoot 側にルートコンテキストをつけることも考えてたのですが、影響範囲が大きいので、SpringBoot 側の URL は変えずに、Nuxt 側でなんとかする方法を取っています。

環境

Java 17
SpringBoot 3.2
Tomcat 10系
Nuxt 3
Node.js v20

SpringBoot 側

認証

認証(ログイン)は SpringBoot 側で行い、Nuxt は SpringBoot が発行した cookie を使用して、認証を突破します。セキュリティとかはクローズドな環境なのでそっとしておきます。
なお、本番時は同一ホストになるはずなので、cookie は Nuxt がちゃんと飛ばせるはずです。

SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 認可の設定
        http.authorizeHttpRequests(requests -> requests
                // セキュリティ設定を無視するリクエスト設定
                // 静的リソース(images、css、javascript)に対するアクセスはセキュリティ設定を無視する
                .requestMatchers("/img/**", "/css/**", "/javascript/**", "/bower_components/**",
                        "/dist/**", "/assets/**", "/webjars/**")
                .permitAll().requestMatchers("/", "/index").permitAll() // indexは全ユーザーアクセス許可
                .requestMatchers("/cook/choco/**").permitAll() // Nuxtの画面へのアクセスは認証不要
                .anyRequest().authenticated()); // それ以外は全て認証無しの場合アクセス不許可
        // SPAの画面からの認証
        http.exceptionHandling(
                exceptionHandling -> exceptionHandling.defaultAuthenticationEntryPointFor(
                        getRestAuthenticationEntryPoint(), new AntPathRequestMatcher("/api/**")));

// 省略

ローカル開発用 Nuxt へ forward させるコントローラ

ちょっとややこしいですが、ローカル開発の場合は localhost:3000 で Nuxt アプリは起動させ、本番環境の場合は同一ホストになるはずなので、Nuxt へforwardさせる方法が変わります。
もっといい方法がある気もしますが、一旦はこれでできているので、目をつぶります。😑

LocalCookController.java
@Controller
@Profile("local")
public class LocalCookController {

    @RequestMapping("/cook/choco/**")
    public RedirectView redirectToNuxt() {
        return new RedirectView("http://localhost:3000/nuxt/cook/choco/");
    }
}

本番用 Nuxt へ forward させるコントローラ

ProductionCookController.java
@Slf4j
@Controller
@Profile("!local")
public class ProductionCookController {

    @RequestMapping("/cook/choco/**")
    public String forwardToNuxt() {
        log.info("Forwarding to /nuxt/index.html");
        return "forward:/nuxt/index.html";
    }

}

Nuxt アプリを war ファイルに同梱する

Nuxt アプリをビルドして、webapp フォルダにコピーすることにより、tomcat が静的ファイルとして認識するようになります。
本当は asset 周りも SpringBoot と共有させたかったのですが、古の遺産により断念。

build.gradle
task buildFrontend(type: Exec) {
    workingDir 'frontend'
    commandLine 'npm', 'install'
    commandLine 'nuxi', 'generate'
}

task cleanFrontend(type: Delete) {
    delete 'src/main/webapp/nuxt'
}

task copyFrontend(type: Copy) {
    from 'frontend/dist/'
    into 'src/main/webapp/nuxt'
    dependsOn cleanFrontend, buildFrontend
}

processResources.dependsOn copyFrontend

Nuxt3 側の設定

tomcatに静的サイトとして認識させるため、SSR でビルドします。
Nuxt側のルートパスは 『/nuxt/』として、ローカル開発時は『/api』 のパスを SpringBoot 側に流します。

nuxt.config.ts
export default defineNuxtConfig({

	ssr: false,
	app: {
		baseURL: '/nuxt/',
        // 省略
    },
    // 省略
	vite: {
		server: {
			proxy: {
				'/api': {
					target: import.meta.env.VITE_BASE_API_URL,
					changeOrigin: true,
					secure: false,
				},
			},
		},
		build: {
			sourcemap: false
		},
		clearScreen: true,
		logLevel: 'info'
	},

バックエンドに横流しする関数

NuxtLink などでパスを指定すると、このままだと『/nuxt』 が自動的に付与されます。
しかし、SpringBoot の View に戻したいときはこのままだと到達しないので、『forward: 'backend'』がある場合は、無理やり『/nuxt』を削除するようにする関数を作成しました。

items: [
  { label: '新規登録', to: '/cock/new', forward: 'backend' },
  // 省略

<NuxtLink v-if="item.to" :to="getLink(item)"
backend.ts
export const apiUrl = import.meta.env.VITE_BASE_API_URL || '<本番のドメイン>';

export const getLink = (item: { forward: string; to: string; }) => {
	if (item.forward === 'backend') {
		const path = item.to.replace('/nuxt', '');
		return `${apiUrl}${path}`;
	} else {
		return item.to;
	}
};

まあ、テンプレートを使っていたのでこうしたんですが、そうじゃなかったら別のやり方があったかも。。

ローカル開発時の SpringBoot のドメインは .env.develop を作成して、自動的に切り替わるようにしています。

.env.develop
VITE_BASE_API_URL=http://localhost:8080

おわりに

Vue/Nuxt 素人頑張りました。。
仕組みはなんとなくはわかってきたので、いい勉強になったなーと思います。
もうやることはないとは思いますが。

Discussion