上集:一个前端的血泪复仇记 —— 静态部署的胜利
“我只是想部署一个小项目,结果干翻了Spring Security、Next.js 和 AI —— 三个加起来至少值我两天命。”
话说有一天,我朋友搭了一个小系统,前后端分离,说大不大,说小不小。后端Spring Boot 3.4.5,前端Next.js 15。开发时他们自我感觉良好,分层清晰,接口明了,Node起服务,Spring管API,一切皆优雅。
然而,当要部署上线时,他们懵了。
Azure上已经通过GitHub Actions成功部署好了后端jar包,但前端还在本地嗷嗷叫。老板催着要结果,可老板哪会折腾前端部署?你还得教他怎么写Actions脚本、怎么设置Node环境、怎么指定输出目录……天知道你一说完他会不会当场升天。
这时候我一拍大腿:
“何必呢?这项目没几个页面,又不需要SSR,咱就静态导出,然后把前端生成的HTML+JS直接丢进Spring Boot的static目录不就完了?jar包一启动,前后端全搞定,老板连指都不用动!”
第一难关:Spring Security 发疯了!
你以为这样就结束了?不,地狱的大门这才刚刚打开。
我照着Next.js的文档老老实实 npx next export
,拷进resources/static/
目录,打jar,跑起来,打开浏览器:403 Forbidden。
不是404,不是500,是403。意思是你这请求我知道,但我就是不让你进。
我立刻意识到:是Spring Security出手了。
点开日志,一行熟悉的输出迎面而来:
Securing GET /
Http403ForbiddenEntryPoint: Pre-authenticated entry point called. Rejecting access
这熟悉的中世纪防火墙味道……果然是你,Security!
于是我翻出SecurityConfig
,一看默认策略是 .anyRequest().authenticated()
,只放行了/api/**
和 /public/**
。而我那可怜的index.html,连个像样的api都不是,当然被一棍子拍死在门外。
我没有头铁上去一个个添加 /index.html
、/*.js
、/*.css
……那样太低效。正确方式其实就两个字:放行。
于是我重新配置了SecurityConfig.java
,使用精简清晰的策略:只对 /api/**
做权限控制,其余一律放行。
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
配合上另一个核心配置——WebConfig.java
中精细处理静态资源和前端路径:
if (resourcePath.contains(".")) return null; // 静态资源请求
if (resourcePath.startsWith("api/")) return null; // 后端接口
return new ClassPathResource("/static/index.html"); // 其他全部交给前端路由
部署、启动、访问:Boom,终于进来了。
第二难关:前端路由全军覆没
我刚准备喝口水庆祝一下,结果点击跳转到 /admin/login
页面时,界面啪地一下变白了。
刷新页面,仍然白屏。
看着那沉默如谜的浏览器窗口,我仿佛听见了它在说:
“你静态部署的SPA里根本没这个路径。”
于是我开始搜索、试验,AI这时候又开始滔滔不绝:
“写个FrontendController吧,用@Forward转发所有路径到index.html!”
我信了。然后一秒钟内,部署的服务陷入了前端路由的死循环:什么都跳转,什么都不对。
点击任何路由按钮,都不是你想去的页面,而是index本尊笑着再次出现。
于是我彻底扔掉了FrontendController那一套。
你用静态导出的Next.js,其实每一个页面路径,都已经被导出成了真正的HTML文件!比如 /admin/login
对应的就是 admin/login/index.html
,这些文件真实地存在于 out/
目录中。只要你把它们原封不动地复制进Spring Boot的 static/
目录中,路径匹配得上,页面自然就出来了,根本不需要靠 index.html 来做路由跳转!
AI死活跟我说,“你这个前端还是SPA,要靠 index.html 来初始化页面、再用JS切换路由”,我反复回怼:“你是不是没搞清楚静态导出是什么意思?”
最后我干脆截图,把 Next.js 导出的每个页面的 index.html 全部拍给它看,我说你看,是不是每个页面都有?每个URL其实就是对应一个静态路径,压根不需要搞什么index跳转那一套。
它这才哑口无言,终于认同我的方案,把 WebConfig 改成现在这个样子 —— 静态资源自己找,找不到再跳 index,而不是一上来就全部丢给 index 去兜底。
所以问题的根源是:不该试图用Controller去理解前端的路由,也不能拿SPA框架的假设来套用静态导出逻辑。
正确姿势是:
- 让 WebConfig 的
ResourceHandler
处理静态文件匹配 - 只放过那些真的访问不到的页面给 index.html
- 更重要的是:你要相信文件系统,而不是JS路由器!
配置定了,页面能跳,静态能拿,接口能调,世界终于恢复了秩序。
小结:写给还在挣扎的小项目开发者
- 前后端分离是一种美梦,但对小项目而言,它可能是场噩梦
- 能静态部署的,就别搭两个服务了,咱不卷
- AI是个很棒的助手,但它不懂项目上下文,更不会给你抹眼泪
- 不要让AI拉着你进坑,它会笑着劝你用Controller转发一切,然后带你走向死循环
- 最靠谱的办法,永远是你亲眼看清 log,亲手写对 config,亲自试出通路
下集我会继续讲完整的构建流程、打包命令、静态导出配置、最终Security+WebConfig的全代码。并带你避开更多AI建议里那些"听起来合理,做出来崩盘"的经典误区。
别急,下一章我们见真章。